Merge from vscode 7eaf220cafb9d9e901370ffce02229171cbf3ea6

This commit is contained in:
ADS Merger
2020-09-03 02:34:56 +00:00
committed by Anthony Dresser
parent 39d9eed585
commit a63578e6f7
519 changed files with 14338 additions and 6670 deletions

View File

@@ -3,24 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionTipsService, IExtensionManagementService, ILocalExtension, IConfigBasedExtensionTip } 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 { IExtensionTipsService, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
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';
import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { distinct } from 'vs/base/common/arrays';
import { Emitter } from 'vs/base/common/event';
export class ConfigBasedRecommendations extends ExtensionRecommendations {
private importantTips: IConfigBasedExtensionTip[] = [];
private otherTips: IConfigBasedExtensionTip[] = [];
private _onDidChangeRecommendations = this._register(new Emitter<void>());
readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;
private _otherRecommendations: ExtensionRecommendation[] = [];
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherRecommendations; }
@@ -30,24 +27,16 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return [...this.importantRecommendations, ...this.otherRecommendations]; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@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);
super(promptedExtensionRecommendations);
}
protected async doActivate(): Promise<void> {
await this.fetch();
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
this.promptWorkspaceRecommendations();
}
private async fetch(): Promise<void> {
@@ -70,54 +59,13 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations {
this._importantRecommendations = this.importantTips.map(tip => this.toExtensionRecommendation(tip));
}
private async promptWorkspaceRecommendations(): Promise<void> {
if (this.hasToIgnoreRecommendationNotifications()) {
return;
}
if (this.importantTips.length === 0) {
return;
}
const local = await this.extensionManagementService.getInstalled();
const { uninstalled } = this.groupByInstalled(distinct(this.importantTips.map(({ extensionId }) => extensionId)), local);
if (uninstalled.length === 0) {
return;
}
const importantExtensions = this.filterIgnoredOrNotAllowed(uninstalled);
if (importantExtensions.length === 0) {
return;
}
for (const extension of importantExtensions) {
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);
}
}
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<string>());
recommendationsToSuggest.forEach(id => {
if (installedExtensionsIds.has(id.toLowerCase())) {
installed.push(id);
} else {
uninstalled.push(id);
}
});
return { installed, uninstalled };
}
private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise<void> {
if (event.added.length) {
const oldImportantRecommended = this.importantTips;
await this.fetch();
// Suggest only if at least one of the newly added recommendations was not suggested before
if (this.importantTips.some(current => oldImportantRecommended.every(old => current.extensionId !== old.extensionId))) {
return this.promptWorkspaceRecommendations();
this._onDidChangeRecommendations.fire();
}
}
}

View File

@@ -11,13 +11,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags';
import { isNumber } from 'vs/base/common/types';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
type DynamicWorkspaceRecommendationsClassification = {
count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -34,19 +30,15 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IFileService private readonly fileService: IFileService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
}
protected async doActivate(): Promise<void> {

View File

@@ -5,17 +5,12 @@
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 { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } 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, optional } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { optional } from 'vs/platform/instantiation/common/instantiation';
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';
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
type ExeExtensionRecommendationsClassification = {
@@ -25,30 +20,24 @@ type ExeExtensionRecommendationsClassification = {
export class ExeBasedRecommendations extends ExtensionRecommendations {
private _otherTips: IExecutableBasedExtensionTip[] = [];
private _importantTips: IExecutableBasedExtensionTip[] = [];
private readonly _otherRecommendations: ExtensionRecommendation[] = [];
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherRecommendations; }
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]; }
private readonly tasExperimentService: ITASExperimentService | undefined;
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@optional(ITASExperimentService) tasExperimentService: ITASExperimentService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
this.tasExperimentService = tasExperimentService;
/*
@@ -58,27 +47,35 @@ 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();
}
private _importantExeBasedRecommendations: Promise<IStringDictionary<IExecutableBasedExtensionTip>> | undefined;
private async fetchImportantExeBasedRecommendations(): Promise<IStringDictionary<IExecutableBasedExtensionTip>> {
private _importantExeBasedRecommendations: Promise<Map<string, IExecutableBasedExtensionTip>> | undefined;
private async fetchImportantExeBasedRecommendations(): Promise<Map<string, IExecutableBasedExtensionTip>> {
if (!this._importantExeBasedRecommendations) {
this._importantExeBasedRecommendations = this.doFetchImportantExeBasedRecommendations();
}
return this._importantExeBasedRecommendations;
}
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;
});
private async doFetchImportantExeBasedRecommendations(): Promise<Map<string, IExecutableBasedExtensionTip>> {
const importantExeBasedRecommendations = new Map<string, IExecutableBasedExtensionTip>();
this._importantTips = await this.extensionTipsService.getImportantExecutableBasedTips();
this._importantTips.forEach(tip => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip));
return importantExeBasedRecommendations;
}
@@ -86,39 +83,45 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
const importantExeBasedRecommendations = await this.fetchImportantExeBasedRecommendations();
const local = await this.extensionManagementService.getInstalled();
const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), local);
const { installed, uninstalled } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], 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!) });
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
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!) });
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
}
}
this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations);
}
private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary<IExecutableBasedExtensionTip>): Promise<void> {
if (this.hasToIgnoreRecommendationNotifications()) {
private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: Map<string, IExecutableBasedExtensionTip>): Promise<void> {
if (this.promptedExtensionRecommendations.hasToIgnoreRecommendationNotifications()) {
return;
}
recommendations = this.filterIgnoredOrNotAllowed(recommendations);
recommendations = this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(recommendations);
if (recommendations.length === 0) {
return;
}
const recommendationsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations[extensionId];
let tips = recommendationsByExe.get(tip.exeFriendlyName);
if (!tips) {
tips = [];
recommendationsByExe.set(tip.exeFriendlyName, tips);
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
let tips = recommendationsByExe.get(tip.exeFriendlyName);
if (!tips) {
tips = [];
recommendationsByExe.set(tip.exeFriendlyName, tips);
}
tips.push(tip);
}
tips.push(tip);
}
for (const [, tips] of recommendationsByExe) {
@@ -127,22 +130,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 the recommended extensions for it?", tips[0].exeFriendlyName);
this.promptedExtensionRecommendations.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`);
}
}

View File

@@ -3,16 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/contrib/experiments/common/experimentService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
export class ExperimentalRecommendations extends ExtensionRecommendations {
@@ -20,16 +14,10 @@ export class ExperimentalRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExperimentService private readonly experimentService: IExperimentService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
}
/**

View File

@@ -15,7 +15,7 @@ import { isPromiseCanceledError } from 'vs/base/common/errors';
import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { domEvent } from 'vs/base/browser/event';
import { append, $, addClass, removeClass, finalHandler, join, toggleClass, hide, show, addDisposableListener, EventType } from 'vs/base/browser/dom';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@@ -25,7 +25,7 @@ import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions';
import { /*RatingsWidget, InstallCountWidget, */RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { EditorOptions } from 'vs/workbench/common/editor';
import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@@ -165,7 +165,7 @@ interface IExtensionEditorTemplate {
header: HTMLElement;
}
export class ExtensionEditor extends BaseEditor {
export class ExtensionEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.extension';
@@ -315,8 +315,8 @@ export class ExtensionEditor extends BaseEditor {
return disposables;
}
async setInput(input: ExtensionsInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
await super.setInput(input, options, token);
async setInput(input: ExtensionsInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await super.setInput(input, options, context, token);
if (this.template) {
await this.updateTemplate(input, this.template, !!options?.preserveFocus);
}
@@ -927,6 +927,7 @@ export class ExtensionEditor extends BaseEditor {
this.renderLocalizations(content, manifest, layout),
renderDashboardContributions(content, manifest, layout), // {{SQL CARBON EDIT}}
this.renderCustomEditors(content, manifest, layout),
this.renderAuthentication(content, manifest, layout),
];
scrollableContent.scanDomNode();
@@ -1174,6 +1175,32 @@ export class ExtensionEditor extends BaseEditor {
return true;
}
private renderAuthentication(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean {
const authentication = manifest.contributes?.authentication || [];
if (!authentication.length) {
return false;
}
const details = $('details', { open: true, ontoggle: onDetailsToggle },
$('summary', { tabindex: '0' }, localize('authentication', "Authentication ({0})", authentication.length)),
$('table', undefined,
$('tr', undefined,
$('th', undefined, localize('authentication.label', "Label")),
$('th', undefined, localize('authentication.id', "Id"))
),
...authentication.map(action =>
$('tr', undefined,
$('td', undefined, action.label),
$('td', undefined, action.id)
)
)
)
);
append(container, details);
return true;
}
private renderColorThemes(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean {
const contrib = manifest.contributes?.themes || [];
if (!contrib.length) {

View File

@@ -8,19 +8,27 @@ 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 { ExtensionRecommendationSource, IExtensionRecommendationReson } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { EnablementState, ExtensionRecommendationSource, IExtensionRecommendationReson, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionsConfiguration, ConfigurationKey, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IAction } from 'vs/base/common/actions';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { CancellationToken } from 'vs/base/common/cancellation';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
type ExtensionRecommendationsNotificationClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
extensionId?: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};
type ExtensionWorkspaceRecommendationsNotificationClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
const ignoreImportantExtensionRecommendation = 'extensionsAssistant/importantRecommendationsIgnore';
const choiceNever = localize('neverShowAgain', "Don't Show Again");
@@ -36,16 +44,9 @@ export abstract class ExtensionRecommendations extends Disposable {
protected abstract doActivate(): Promise<void>;
constructor(
protected readonly isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IConfigurationService protected readonly configurationService: IConfigurationService,
@INotificationService protected readonly notificationService: INotificationService,
@ITelemetryService protected readonly telemetryService: ITelemetryService,
@IStorageService protected readonly storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
protected readonly promptedExtensionRecommendations: PromptedExtensionRecommendations,
) {
super();
storageKeysSyncRegistryService.registerStorageKey({ key: ignoreImportantExtensionRecommendation, version: 1 });
}
private _activationPromise: Promise<void> | null = null;
@@ -57,47 +58,63 @@ export abstract class ExtensionRecommendations extends Disposable {
return this._activationPromise;
}
private runAction(action: IAction) {
try {
action.run();
} finally {
action.dispose();
}
}
export class PromptedExtensionRecommendations extends Disposable {
constructor(
private readonly isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@INotificationService private readonly notificationService: INotificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super();
storageKeysSyncRegistryService.registerStorageKey({ key: ignoreImportantExtensionRecommendation, version: 1 });
}
protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string): void {
async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string): Promise<void> {
if (this.hasToIgnoreRecommendationNotifications()) {
return;
}
const extensions = await this.getInstallableExtensions(extensionIds);
if (!extensions.length) {
return;
}
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(SearchExtensionsAction, searchValue));
await Promise.all(extensions.map(async extension => {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id });
this.extensionsWorkbenchService.open(extension, { pinned: true });
await this.extensionManagementService.installFromGallery(extension.gallery!);
}));
}
}, {
label: extensionIds.length === 1 ? localize('moreInformation', "More Information") : localize('showRecommendations', "Show Recommendations"),
run: () => {
for (const extensionId of extensionIds) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId });
}
if (extensionIds.length === 1) {
this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionAction, extensionIds[0]));
} else {
this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL));
label: localize('show recommendations', "Show Recommendations"),
run: async () => {
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id });
this.extensionsWorkbenchService.open(extension, { pinned: true });
}
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
}
}, {
label: choiceNever,
isSecondary: true,
run: () => {
for (const extensionId of extensionIds) {
this.addToImportantRecommendationsIgnore(extensionId);
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId });
for (const extension of extensions) {
this.addToImportantRecommendationsIgnore(extension.identifier.id);
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id });
}
this.notificationService.prompt(
Severity.Info,
@@ -115,20 +132,78 @@ export abstract class ExtensionRecommendations extends Disposable {
{
sticky: true,
onCancel: () => {
for (const extensionId of extensionIds) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId });
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id });
}
}
}
);
}
protected hasToIgnoreRecommendationNotifications(): boolean {
async promptWorkspaceRecommendations(recommendations: string[]): Promise<void> {
if (this.hasToIgnoreWorkspaceRecommendationNotifications()) {
return;
}
let installed = await this.extensionManagementService.getInstalled();
installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
recommendations = recommendations.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
if (!recommendations.length) {
return;
}
const extensions = await this.getInstallableExtensions(recommendations);
if (!extensions.length) {
return;
}
const searchValue = '@recommended ';
this.notificationService.prompt(
Severity.Info,
localize('workspaceRecommended', "Do you want to install the recommended extensions for this repository?"),
[{
label: localize('install', "Install"),
run: async () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' });
await Promise.all(extensions.map(async extension => {
this.extensionsWorkbenchService.open(extension, { pinned: true });
await this.extensionManagementService.installFromGallery(extension.gallery!);
}));
}
}, {
label: localize('showRecommendations', "Show Recommendations"),
run: async () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' });
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
}
}, {
label: localize('neverShowAgain', "Don't Show Again"),
isSecondary: true,
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' });
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
}
}],
{
sticky: true,
onCancel: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' });
}
}
);
}
hasToIgnoreRecommendationNotifications(): boolean {
const config = this.configurationService.getValue<IExtensionsConfiguration>(ConfigurationKey);
return config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand;
}
protected filterIgnoredOrNotAllowed(recommendationsToSuggest: string[]): string[] {
hasToIgnoreWorkspaceRecommendationNotifications(): boolean {
return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false);
}
filterIgnoredOrNotAllowed(recommendationsToSuggest: string[]): string[] {
const importantRecommendationsIgnoreList = (<string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendation, StorageScope.GLOBAL, '[]'))).map(e => e.toLowerCase());
return recommendationsToSuggest.filter(id => {
if (importantRecommendationsIgnoreList.indexOf(id) !== -1) {
@@ -141,6 +216,27 @@ export abstract class ExtensionRecommendations extends Disposable {
});
}
private async getInstallableExtensions(extensionIds: string[]): Promise<IExtension[]> {
const extensions: IExtension[] = [];
if (extensionIds.length) {
const pager = await this.extensionsWorkbenchService.queryGallery({ names: extensionIds, pageSize: extensionIds.length, source: 'install-recommendations' }, CancellationToken.None);
for (const extension of pager.firstPage) {
if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) {
extensions.push(extension);
}
}
}
return extensions;
}
private async runAction(action: IAction): Promise<void> {
try {
await action.run();
} finally {
action.dispose();
}
}
private addToImportantRecommendationsIgnore(id: string) {
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendation, StorageScope.GLOBAL, '[]'));
importantRecommendationsIgnoreList.push(id.toLowerCase());

View File

@@ -22,12 +22,12 @@ import { ExperimentalRecommendations } from 'vs/workbench/contrib/extensions/bro
import { WorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/workspaceRecommendations';
import { FileBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/fileBasedRecommendations';
import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/keymapRecommendations';
import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions';
import { StaticRecommendations } from 'sql/workbench/contrib/extensions/browser/staticRecommendations';
import { ScenarioRecommendations } from 'sql/workbench/contrib/extensions/browser/scenarioRecommendations';
import { ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations';
import { StaticRecommendations } from 'sql/workbench/contrib/extensions/browser/staticRecommendations';
import { ScenarioRecommendations } from 'sql/workbench/contrib/extensions/browser/scenarioRecommendations';
import { ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions';
type IgnoreRecommendationClassification = {
recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -40,6 +40,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
declare readonly _serviceBrand: undefined;
private readonly promptedExtensionRecommendations: PromptedExtensionRecommendations;
// Recommendations
private readonly fileBasedRecommendations: FileBasedRecommendations;
private readonly workspaceRecommendations: WorkspaceRecommendations;
@@ -54,7 +56,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
// Ignored Recommendations
private globallyIgnoredRecommendations: string[] = [];
public loadWorkspaceConfigPromise: Promise<void>;
public readonly activationPromise: Promise<void>;
private sessionSeed: number;
private readonly _onRecommendationChange = this._register(new Emitter<RecommendationChangeNotification>());
@@ -62,7 +64,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@ILifecycleService lifecycleService: ILifecycleService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IStorageService private readonly storageService: IStorageService,
@@ -76,19 +78,20 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
storageKeysSyncRegistryService.registerStorageKey({ key: ignoredRecommendationsStorageKey, version: 1 });
const isExtensionAllowedToBeRecommended = (extensionId: string) => this.isExtensionAllowedToBeRecommended(extensionId);
this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations, isExtensionAllowedToBeRecommended);
this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations, isExtensionAllowedToBeRecommended);
this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations, isExtensionAllowedToBeRecommended);
this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations, isExtensionAllowedToBeRecommended);
this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations, isExtensionAllowedToBeRecommended);
this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations, isExtensionAllowedToBeRecommended);
this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations, isExtensionAllowedToBeRecommended);
this.staticRecommendations = instantiationService.createInstance(StaticRecommendations, isExtensionAllowedToBeRecommended); // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations, isExtensionAllowedToBeRecommended); // {{SQL CARBON EDIT}} add ours
this.promptedExtensionRecommendations = instantiationService.createInstance(PromptedExtensionRecommendations, isExtensionAllowedToBeRecommended);
this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations, this.promptedExtensionRecommendations);
this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations, this.promptedExtensionRecommendations);
this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations, this.promptedExtensionRecommendations);
this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations, this.promptedExtensionRecommendations);
this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations, this.promptedExtensionRecommendations);
this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations, this.promptedExtensionRecommendations);
this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations, this.promptedExtensionRecommendations);
this.staticRecommendations = instantiationService.createInstance(StaticRecommendations, this.promptedExtensionRecommendations); // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations, this.promptedExtensionRecommendations); // {{SQL CARBON EDIT}} add ours
if (!this.isEnabled()) {
this.sessionSeed = 0;
this.loadWorkspaceConfigPromise = Promise.resolve();
this.activationPromise = Promise.resolve();
return;
}
@@ -96,19 +99,35 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
this.globallyIgnoredRecommendations = this.getCachedIgnoredRecommendations();
// Activation
this.loadWorkspaceConfigPromise = this.workspaceRecommendations.activate().then(() => this.fileBasedRecommendations.activate());
this.experimentalRecommendations.activate();
this.keymapRecommendations.activate();
this.staticRecommendations.activate(); // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations.activate(); // {{SQL CARBON EDIT}} add ours
if (!this.configurationService.getValue<boolean>(ShowRecommendationsOnlyOnDemandKey)) {
lifecycleService.when(LifecyclePhase.Eventually).then(() => this.activateProactiveRecommendations());
}
this.activationPromise = this.activate();
this._register(this.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e)));
this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
}
private async activate(): Promise<void> {
await this.lifecycleService.when(LifecyclePhase.Restored);
// activate all recommendations
await Promise.all([
this.workspaceRecommendations.activate(),
this.fileBasedRecommendations.activate(),
this.experimentalRecommendations.activate(),
this.keymapRecommendations.activate(),
this.staticRecommendations.activate(), // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations.activate(), // {{SQL CARBON EDIT}} add ours
this.lifecycleService.when(LifecyclePhase.Eventually)
.then(() => {
if (!this.configurationService.getValue<boolean>(ShowRecommendationsOnlyOnDemandKey)) {
this.activateProactiveRecommendations();
}
})
]);
await this.promptWorkspaceRecommendations();
this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations)(() => this.promptWorkspaceRecommendations()));
}
private isEnabled(): boolean {
return this.galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI && this.configurationService.getValue<string>(ExtensionsPolicyKey) !== ExtensionsPolicy.allowNone;
}
@@ -143,9 +162,12 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return output;
}
async getConfigBasedRecommendations(): Promise<IExtensionRecommendation[]> {
async getConfigBasedRecommendations(): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> {
await this.configBasedRecommendations.activate();
return this.toExtensionRecommendations(this.configBasedRecommendations.recommendations);
return {
important: this.toExtensionRecommendations(this.configBasedRecommendations.importantRecommendations),
others: this.toExtensionRecommendations(this.configBasedRecommendations.otherRecommendations)
};
}
async getOtherRecommendations(): Promise<IExtensionRecommendation[]> {
@@ -202,6 +224,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);
}
@@ -265,6 +294,16 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1;
}
private async promptWorkspaceRecommendations(): Promise<void> {
const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations]
.map(({ extensionId }) => extensionId)
.filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId));
if (allowedRecommendations.length) {
await this.promptedExtensionRecommendations.promptWorkspaceRecommendations(allowedRecommendations);
}
}
private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void {
if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.GLOBAL
&& this.ignoredRecommendationsValue !== this.getStoredIgnoredRecommendationsValue() /* This checks if current window changed the value or not */) {

View File

@@ -15,11 +15,10 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo
import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import {
OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction,
ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction,
EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction
EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor';
@@ -55,7 +54,7 @@ import { MultiCommand } from 'vs/editor/browser/editorExtensions';
import { Webview } from 'vs/workbench/contrib/webview/browser/webview';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
// registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben uncomment when 'semver-umd' can be loaded
registerSingleton(IExtensionRecommendationsService, ExtensionRecommendationsService);
Registry.as<IOutputChannelRegistry>(OutputExtensions.OutputChannels)
@@ -113,9 +112,6 @@ actionRegistry.registerWorkbenchAction(keymapRecommendationsActionDescriptor, 'P
const languageExtensionsActionDescriptor = SyncActionDescriptor.from(ShowLanguageExtensionsAction);
actionRegistry.registerWorkbenchAction(languageExtensionsActionDescriptor, 'Preferences: Language Extensions', PreferencesLabel);
const azureExtensionsActionDescriptor = SyncActionDescriptor.from(ShowAzureExtensionsAction);
actionRegistry.registerWorkbenchAction(azureExtensionsActionDescriptor, 'Preferences: Azure Extensions', PreferencesLabel);
const popularActionDescriptor = SyncActionDescriptor.from(ShowPopularExtensionsAction);
actionRegistry.registerWorkbenchAction(popularActionDescriptor, 'Extensions: Show Popular Extensions', ExtensionsLabel);

View File

@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);

View File

@@ -35,7 +35,6 @@ import { Color } from 'vs/base/common/color';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { PagedModel } from 'vs/base/common/paging';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { MenuRegistry, MenuId, IMenuService } from 'vs/platform/actions/common/actions';
@@ -928,7 +927,7 @@ export class EnableForWorkspaceAction extends ExtensionAction {
if (this.extension && this.extension.local) {
this.enabled = this.extension.state === ExtensionState.Installed
&& !this.extensionEnablementService.isEnabled(this.extension.local)
&& this.extensionEnablementService.canChangeEnablement(this.extension.local);
&& this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local);
}
}
@@ -989,7 +988,7 @@ export class DisableForWorkspaceAction extends ExtensionAction {
if (this.extension && this.extension.local && this.runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) {
this.enabled = this.extension.state === ExtensionState.Installed
&& (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace)
&& this.extensionEnablementService.canChangeEnablement(this.extension.local);
&& this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local);
}
}
@@ -1850,86 +1849,6 @@ export class ShowRecommendedExtensionsAction extends Action {
}
}
export class InstallRecommendedExtensionsAction extends Action {
static readonly ID = 'workbench.extensions.action.installRecommendedExtensions';
static readonly LABEL = localize('installRecommendedExtensions', "Install Recommended Extensions");
private _recommendations: string[] = [];
get recommendations(): string[] { return this._recommendations; }
set recommendations(recommendations: string[]) { this._recommendations = recommendations; this.enabled = this._recommendations.length > 0; }
constructor(
id: string,
label: string,
recommendations: string[],
private readonly source: string,
@IViewletService private readonly viewletService: IViewletService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IProductService private readonly productService: IProductService,
) {
super(id, label, 'extension-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);
});
});
}
private async installExtension(extension: IExtension): Promise<void> {
try {
if (extension.local && extension.gallery) {
if (prefersExecuteOnUI(extension.local.manifest, this.productService, this.configurationService)) {
if (this.extensionManagementServerService.localExtensionManagementServer) {
await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(extension.gallery);
return;
}
} else if (this.extensionManagementServerService.remoteExtensionManagementServer) {
await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(extension.gallery);
return;
}
}
await this.extensionWorkbenchService.install(extension);
} catch (err) {
console.error(err);
return promptDownloadManually(extension.gallery, localize('failedToInstall', "Failed to install \'{0}\'.", extension.identifier.id), err, this.instantiationService);
}
}
}
export class InstallWorkspaceRecommendedExtensionsAction extends InstallRecommendedExtensionsAction {
constructor(
recommendations: string[],
@IViewletService viewletService: IViewletService,
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionsWorkbenchService extensionWorkbenchService: IExtensionsWorkbenchService,
@IConfigurationService configurationService: IConfigurationService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IProductService productService: IProductService,
) {
super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, 'install-all-workspace-recommendations',
viewletService, instantiationService, extensionWorkbenchService, configurationService, extensionManagementServerService, productService);
}
}
export class ShowRecommendedExtensionAction extends Action {
static readonly ID = 'workbench.extensions.action.showRecommendedExtension';
@@ -1942,7 +1861,7 @@ export class ShowRecommendedExtensionAction extends Action {
@IViewletService private readonly viewletService: IViewletService,
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
) {
super(InstallRecommendedExtensionAction.ID, InstallRecommendedExtensionAction.LABEL, undefined, false);
super(ShowRecommendedExtensionAction.ID, ShowRecommendedExtensionAction.LABEL, undefined, false);
this.extensionId = extensionId;
}
@@ -2096,29 +2015,6 @@ export class ShowLanguageExtensionsAction extends Action {
}
}
export class ShowAzureExtensionsAction extends Action {
static readonly ID = 'workbench.extensions.action.showAzureExtensions';
static readonly LABEL = localize('showAzureExtensionsShort', "Azure Extensions");
constructor(
id: string,
label: string,
@IViewletService private readonly viewletService: IViewletService
) {
super(id, label, undefined, true);
}
run(): Promise<void> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search('@sort:installs azure ');
viewlet.focus();
});
}
}
export class SearchCategoryAction extends Action {
constructor(
@@ -2131,12 +2027,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();
}
}

View File

@@ -60,6 +60,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr
import { DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { URI } from 'vs/base/common/uri';
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
const NonEmptyWorkspaceContext = new RawContextKey<boolean>('nonEmptyWorkspace', false);
const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true);
@@ -128,14 +129,18 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
if (this.extensionManagementServerService.localExtensionManagementServer) {
servers.push(this.extensionManagementServerService.localExtensionManagementServer);
}
if (this.extensionManagementServerService.webExtensionManagementServer) {
servers.push(this.extensionManagementServerService.webExtensionManagementServer);
}
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
servers.push(this.extensionManagementServerService.remoteExtensionManagementServer);
}
if (servers.length === 0 && this.extensionManagementServerService.webExtensionManagementServer) {
servers.push(this.extensionManagementServerService.webExtensionManagementServer);
}
const getViewName = (viewTitle: string, server: IExtensionManagementServer): string => {
return servers.length > 1 ? `${server.label} - ${viewTitle}` : viewTitle;
if (servers.length) {
const serverLabel = server === this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer ? localize('local', "Local") : server.label;
return servers.length > 1 ? `${serverLabel} - ${viewTitle}` : viewTitle;
}
return viewTitle;
};
for (const server of servers) {
const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server);
@@ -350,6 +355,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
@IInstantiationService instantiationService: IInstantiationService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@INotificationService private readonly notificationService: INotificationService,
@IViewletService private readonly viewletService: IViewletService,
@@ -520,14 +527,19 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
]);
if (this.extensionGalleryService.isEnabled()) {
filterActions.splice(0, 0, ...[
const galleryFilterActions = [
// this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.featured', localize('featured filter', "Featured"), '@featured'), // {{SQL CARBON EDIT}}
// this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.popular', localize('most popular filter', "Most Popular"), '@popular'), // {{SQL CARBON EDIT}}
this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.recommended', localize('most popular recommended', "Recommended"), '@recommended'),
// this.instantiationService.createInstance(RecentlyPublishedExtensionsAction, RecentlyPublishedExtensionsAction.ID, localize('recently published filter', "Recently Published")), // {{SQL CARBON EDIT}}
new Separator(),
new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category))),
new Separator(),
]);
];
if (this.extensionManagementServerService.webExtensionManagementServer || !this.environmentService.isBuilt) {
galleryFilterActions.splice(4, 0, this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.web', localize('web filter', "Web"), '@web'));
}
filterActions.splice(0, 0, ...galleryFilterActions);
filterActions.push(...[
new Separator(),
new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions),
@@ -582,6 +594,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
.replace(/@tag:/g, 'tag:')
.replace(/@ext:/g, 'ext:')
.replace(/@featured/g, 'featured')
.replace(/@web/g, 'tag:"__web_extension"')
.replace(/@popular/g, '@sort:installs')
: '';
}

View File

@@ -9,7 +9,7 @@ import { assign } from 'vs/base/common/objects';
import { Event, Emitter } from 'vs/base/common/event';
import { isPromiseCanceledError, getErrorMessage } from 'vs/base/common/errors';
import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging';
import { SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { SortBy, SortOrder, IQueryOptions, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionRecommendation, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@@ -17,7 +17,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { append, $, toggleClass, addClass } from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Delegate, Renderer, IExtensionsViewState } from 'vs/workbench/contrib/extensions/browser/extensionsList';
import { IExtension, IExtensionsWorkbenchService, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery';
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -25,13 +25,13 @@ import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { WorkbenchPagedList, ListResourceNavigator } from 'vs/platform/list/browser/listService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { distinct, coalesce, firstIndex } from 'vs/base/common/arrays';
import { coalesce, distinct, flatten, firstIndex } from 'vs/base/common/arrays'; // {{ SQL CARBON EDIT }}
import { IExperimentService, IExperiment, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
@@ -53,6 +53,10 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
// Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions
const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result'];
type WorkspaceRecommendationsClassification = {
count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', 'isMeasurement': true };
};
class ExtensionsViewState extends Disposable implements IExtensionsViewState {
private readonly _onFocus: Emitter<IExtension> = this._register(new Emitter<IExtension>());
@@ -98,12 +102,13 @@ export class ExtensionsListView extends ViewPane {
@IThemeService themeService: IThemeService,
@IExtensionService private readonly extensionService: IExtensionService,
@IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionRecommendationsService protected tipsService: IExtensionRecommendationsService,
@IExtensionRecommendationsService protected extensionRecommendationsService: IExtensionRecommendationsService,
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IExperimentService private readonly experimentService: IExperimentService,
@IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService,
@IProductService protected readonly productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@@ -471,14 +476,9 @@ export class ExtensionsListView extends ViewPane {
}
// {{SQL CARBON EDIT}} - End
if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {
return this.getWorkspaceRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
return this.getKeymapRecommendationsModel(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)) {
return this.getRecommendationsModel(query, options, token);
if (this.isRecommendationsQuery(query)) {
return this.queryRecommendations(query, options, token);
} else if (ExtensionsListView.isAllMarketplaceExtensionsQuery(query.value)) { // {{SQL CARBON EDIT}} add if
return this.getAllMarketplaceModel(query, options, token);
}
@@ -557,51 +557,6 @@ export class ExtensionsListView extends ViewPane {
return extensions;
}
// Get All types of recommendations, trimmed to show a max of 8 at any given time
private getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:all/g, '').replace(/@recommended/g, '').trim().toLowerCase();
return this.extensionsWorkbenchService.queryLocal(this.server)
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
const fileBasedRecommendations = this.tipsService.getFileBasedRecommendations();
const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations();
const othersPromise = this.tipsService.getOtherRecommendations();
const workspacePromise = this.tipsService.getWorkspaceRecommendations();
const importantRecommendationsPromise = this.tipsService.getImportantRecommendations();
return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise])
.then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => {
const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, workspaceRecommendations);
const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason();
/* __GDPR__
"extensionAllRecommendations:open" : {
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('extensionAllRecommendations:open', {
count: names.length,
recommendations: names.map(id => {
return {
id,
recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId
};
})
});
if (!names.length) {
return Promise.resolve(new PagedModel([]));
}
options.source = 'recommendations-all';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(pager => {
this.sortFirstPage(pager, names);
return this.getPagedModel(pager || []);
});
});
});
}
private async getCuratedModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/curated:/g, '').trim();
const names = await this.experimentService.getCuratedExtensionsList(value);
@@ -614,55 +569,13 @@ export class ExtensionsListView extends ViewPane {
return new PagedModel([]);
}
// Get All types of recommendations other than Workspace recommendations, trimmed to show a max of 8 at any given time
private getRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
return this.extensionsWorkbenchService.queryLocal(this.server)
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations();
const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations();
const othersPromise = this.tipsService.getOtherRecommendations();
const workspacePromise = this.tipsService.getWorkspaceRecommendations();
const importantRecommendationsPromise = this.tipsService.getImportantRecommendations();
return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise])
.then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => {
configBasedRecommendations = configBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
others = others.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, []);
const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason();
/* __GDPR__
"extensionRecommendations:open" : {
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('extensionRecommendations:open', {
count: names.length,
recommendations: names.map(id => {
return {
id,
recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId
};
})
});
if (!names.length) {
return Promise.resolve(new PagedModel([]));
}
options.source = 'recommendations';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(pager => {
this.sortFirstPage(pager, names);
return this.getPagedModel(pager || []);
});
});
});
private isRecommendationsQuery(query: Query): boolean {
return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)
|| ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)
|| ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)
|| /@recommended:all/i.test(query.value)
|| ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)
|| ExtensionsListView.isRecommendedExtensionsQuery(query.value);
}
// {{SQL CARBON EDIT}}
@@ -671,7 +584,7 @@ export class ExtensionsListView extends ViewPane {
return this.extensionsWorkbenchService.queryLocal()
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
return this.tipsService.getOtherRecommendations().then((recommmended) => {
return this.extensionRecommendationsService.getOtherRecommendations().then((recommmended) => {
const installedExtensions = local.map(x => `${x.publisher}.${x.name}`);
options = assign(options, { text: value, source: 'searchText' });
return this.extensionsWorkbenchService.queryGallery(options, token).then((pager) => {
@@ -709,7 +622,7 @@ export class ExtensionsListView extends ViewPane {
return this.extensionsWorkbenchService.queryLocal()
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
return this.tipsService.getRecommendedExtensionsByScenario(scenarioType).then((recommmended) => {
return this.extensionRecommendationsService.getRecommendedExtensionsByScenario(scenarioType).then((recommmended) => {
const installedExtensions = local.map(x => `${x.publisher}.${x.name}`);
return this.extensionsWorkbenchService.queryGallery(token).then((pager) => {
// filter out installed extensions and the extensions not in the recommended list
@@ -726,88 +639,129 @@ export class ExtensionsListView extends ViewPane {
}
// {{SQL CARBON EDIT}} - End
// Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions
private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, importantRecommendations: IExtensionRecommendation[], fileBasedRecommendations: IExtensionRecommendation[], configBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] {
const totalCount = 10;
workspaceRecommendations = workspaceRecommendations
.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
importantRecommendations = importantRecommendations
.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
configBasedRecommendations = configBasedRecommendations
.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
fileBasedRecommendations = fileBasedRecommendations.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId)
&& configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
otherRecommendations = otherRecommendations.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& fileBasedRecommendations.every(fileBasedRecommendation => fileBasedRecommendation.extensionId !== recommendation.extensionId)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId)
&& configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
const otherCount = Math.min(2, otherRecommendations.length);
const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - importantRecommendations.length - configBasedRecommendations.length - otherCount);
const recommendations = [...workspaceRecommendations, ...importantRecommendations, ...configBasedRecommendations];
recommendations.push(...fileBasedRecommendations.splice(0, fileBasedCount));
recommendations.push(...otherRecommendations.splice(0, otherCount));
return distinct(recommendations.map(({ extensionId }) => extensionId));
}
private isRecommendationInstalled(recommendation: IExtensionRecommendation, installed: IExtension[]): boolean {
return installed.some(i => areSameExtensions(i.identifier, { id: recommendation.extensionId }));
}
private getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase();
return this.tipsService.getWorkspaceRecommendations()
.then(recommendations => {
const names = recommendations.map(({ extensionId }) => extensionId).filter(name => name.toLowerCase().indexOf(value) > -1);
/* __GDPR__
"extensionWorkspaceRecommendations:open" : {
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this.telemetryService.publicLog('extensionWorkspaceRecommendations:open', { count: names.length });
if (!names.length) {
return Promise.resolve(new PagedModel([]));
}
options.source = 'recommendations-workspace';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(pager => this.getPagedModel(pager || []));
});
}
private getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase();
const names: string[] = this.tipsService.getKeymapRecommendations().map(({ extensionId }) => extensionId)
.filter(extensionId => extensionId.toLowerCase().indexOf(value) > -1);
if (!names.length) {
return Promise.resolve(new PagedModel([]));
private async queryRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
// Workspace recommendations
if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {
return this.getWorkspaceRecommendationsModel(query, options, token);
}
options.source = 'recommendations-keymaps';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(result => this.getPagedModel(result));
// Keymap recommendations
if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
return this.getKeymapRecommendationsModel(query, options, token);
}
// Exe recommendations
if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) {
return this.getExeRecommendationsModel(query, options, token);
}
// All recommendations
if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) {
return this.getAllRecommendationsModel(query, options, token);
}
// Other recommendations
if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
return this.getOtherRecommendationsModel(query, options, token);
}
return new PagedModel([]);
}
protected async getInstallableRecommendations(recommendations: IExtensionRecommendation[], options: IQueryOptions, token: CancellationToken): Promise<IExtension[]> {
const extensions: IExtension[] = [];
if (recommendations.length) {
const names = recommendations.map(({ extensionId }) => extensionId);
const pager = await this.extensionsWorkbenchService.queryGallery({ ...options, names, pageSize: names.length }, token);
for (const extension of pager.firstPage) {
if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) {
extensions.push(extension);
}
}
}
return extensions;
}
protected async getWorkspaceRecommendations(): Promise<IExtensionRecommendation[]> {
const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations();
const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations();
for (const configBasedRecommendation of important) {
if (recommendations.some(r => r.extensionId !== configBasedRecommendation.extensionId)) {
recommendations.push(configBasedRecommendation);
}
}
return recommendations;
}
private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase();
const recommendations = await this.getWorkspaceRecommendations();
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token))
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length });
const result: IExtension[] = coalesce(recommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result);
}
private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase();
const recommendations = this.extensionRecommendationsService.getKeymapRecommendations();
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-keymaps' }, token))
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
return new PagedModel(installableRecommendations);
}
private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase();
const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe);
const installableRecommendations = await this.getInstallableRecommendations([...important, ...others], { ...options, source: 'recommendations-exe' }, token);
return new PagedModel(installableRecommendations);
}
private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
const local = (await this.extensionsWorkbenchService.queryLocal(this.server))
.filter(e => e.type === ExtensionType.User)
.map(e => e.identifier.id.toLowerCase());
const workspaceRecommendations = (await this.getWorkspaceRecommendations())
.map(r => r.extensionId.toLowerCase());
const otherRecommendations = distinct(
flatten(await Promise.all([
// Order is important
this.extensionRecommendationsService.getImportantRecommendations(),
this.extensionRecommendationsService.getFileBasedRecommendations(),
this.extensionRecommendationsService.getOtherRecommendations()
])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase())
), r => r.extensionId.toLowerCase());
const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token))
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
const result: IExtension[] = coalesce(otherRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result);
}
// Get All types of recommendations, trimmed to show a max of 8 at any given time
private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const local = (await this.extensionsWorkbenchService.queryLocal(this.server))
.filter(e => e.type === ExtensionType.User)
.map(e => e.identifier.id.toLowerCase());
const allRecommendations = distinct(
flatten(await Promise.all([
// Order is important
this.getWorkspaceRecommendations(),
this.extensionRecommendationsService.getImportantRecommendations(),
this.extensionRecommendationsService.getFileBasedRecommendations(),
this.extensionRecommendationsService.getOtherRecommendations()
])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase())
), r => r.extensionId.toLowerCase());
const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token);
const result: IExtension[] = coalesce(allRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result.slice(0, 8));
}
// Sorts the firstPage of the pager in the same order as given array of extension ids
@@ -942,6 +896,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);
}
@@ -983,6 +941,7 @@ export class ServerExtensionsView extends ExtensionsListView {
@IExperimentService experimentService: IExperimentService,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
@IProductService productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IMenuService menuService: IMenuService,
@@ -991,7 +950,9 @@ export class ServerExtensionsView extends ExtensionsListView {
@IPreferencesService preferencesService: IPreferencesService,
) {
options.server = server;
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService);
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService,
telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, extensionManagementService, productService,
contextKeyService, viewDescriptorService, menuService, openerService, preferencesService);
this._register(onDidChangeTitle(title => this.updateTitle(title)));
}
@@ -1076,7 +1037,7 @@ export class DefaultRecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.tipsService.onRecommendationChange(() => {
this._register(this.extensionRecommendationsService.onRecommendationChange(() => {
this.show('');
}));
}
@@ -1101,7 +1062,7 @@ export class RecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.tipsService.onRecommendationChange(() => {
this._register(this.extensionRecommendationsService.onRecommendationChange(() => {
this.show('');
}));
}
@@ -1114,20 +1075,18 @@ export class RecommendedExtensionsView extends ExtensionsListView {
export class WorkspaceRecommendedExtensionsView extends ExtensionsListView {
private readonly recommendedExtensionsQuery = '@recommended:workspace';
private installAllAction: InstallWorkspaceRecommendedExtensionsAction | undefined;
private installAllAction: Action | undefined;
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.tipsService.onRecommendationChange(() => this.update()));
this._register(this.extensionsWorkbenchService.onChange(() => this.setRecommendationsToInstall()));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.update()));
this._register(this.extensionRecommendationsService.onRecommendationChange(() => this.show(this.recommendedExtensionsQuery)));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery)));
}
getActions(): IAction[] {
if (!this.installAllAction) {
this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, []));
this.installAllAction.class = 'codicon codicon-cloud-download';
this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), 'codicon codicon-cloud-download', false, () => this.installWorkspaceRecommendations()));
}
const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL));
@@ -1139,33 +1098,28 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView {
let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace';
let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery));
this.setExpanded(model.length > 0);
await this.setRecommendationsToInstall();
return model;
}
private update(): void {
this.show(this.recommendedExtensionsQuery);
this.setRecommendationsToInstall();
}
private async setRecommendationsToInstall(): Promise<void> {
const recommendations = await this.getRecommendationsToInstall();
const installableRecommendations = await this.getInstallableWorkspaceRecommendations();
if (this.installAllAction) {
this.installAllAction.recommendations = recommendations.map(({ extensionId }) => extensionId);
this.installAllAction.enabled = installableRecommendations.length > 0;
}
}
private getRecommendationsToInstall(): Promise<IExtensionRecommendation[]> {
return this.tipsService.getWorkspaceRecommendations()
.then(recommendations => recommendations.filter(({ extensionId }) => {
const extension = this.extensionsWorkbenchService.local.filter(i => areSameExtensions({ id: extensionId }, i.identifier))[0];
if (!extension
|| !extension.local
|| extension.state !== ExtensionState.Installed
|| extension.enablementState === EnablementState.DisabledByExtensionKind
) {
return true;
}
return false;
}));
private async getInstallableWorkspaceRecommendations() {
const installed = (await this.extensionsWorkbenchService.queryLocal())
.filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
const recommendations = (await this.getWorkspaceRecommendations())
.filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None);
}
private async installWorkspaceRecommendations(): Promise<void> {
const installableRecommendations = await this.getInstallableWorkspaceRecommendations();
await Promise.all(installableRecommendations.map(extension => this.extensionManagementService.installFromGallery(extension.gallery!)));
}
}

View File

@@ -235,7 +235,11 @@ class Extension implements IExtension {
return Promise.resolve(null);
}
return Promise.resolve(this.local!.manifest);
if (this.local) {
return Promise.resolve(this.local.manifest);
}
return Promise.resolve(null);
}
hasReadme(): boolean {
@@ -694,9 +698,18 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
}
const extensionsToChoose = enabledExtensions.length ? enabledExtensions : extensions;
const manifest = extensionsToChoose.find(e => e.local && e.local.manifest)?.local?.manifest;
// Manifest is not found which should not happen.
// In which case return the first extension.
if (!manifest) {
return extensionsToChoose[0];
}
const extensionKinds = getExtensionKind(manifest, this.productService, this.configurationService);
let extension = extensionsToChoose.find(extension => {
for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) {
for (const extensionKind of extensionKinds) {
switch (extensionKind) {
case 'ui':
/* UI extension is chosen only if it is installed locally */
@@ -723,7 +736,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
if (!extension && this.extensionManagementServerService.localExtensionManagementServer) {
extension = extensionsToChoose.find(extension => {
for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) {
for (const extensionKind of extensionKinds) {
switch (extensionKind) {
case 'workspace':
/* Choose local workspace extension if exists */
@@ -745,7 +758,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
if (!extension && this.extensionManagementServerService.remoteExtensionManagementServer) {
extension = extensionsToChoose.find(extension => {
for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) {
for (const extensionKind of extensionKinds) {
switch (extensionKind) {
case 'web':
/* Choose remote web extension if exists */

View File

@@ -4,28 +4,26 @@
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendationSource, ExtensionRecommendationReason, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProductService } from 'vs/platform/product/common/productService';
import { ImportantExtensionTip, IProductService } from 'vs/platform/product/common/productService';
import { forEach, IStringDictionary } from 'vs/base/common/collections';
import { ITextModel } from 'vs/editor/common/model';
import { Schemas } from 'vs/base/common/network';
import { extname } from 'vs/base/common/resources';
import { basename, extname } from 'vs/base/common/resources';
import { match } from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { MIME_UNKNOWN, guessMimeTypes } from 'vs/base/common/mime';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { setImmediate } from 'vs/base/common/platform';
import { IModeService } from 'vs/editor/common/services/modeService';
type FileExtensionSuggestionClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
@@ -35,32 +33,34 @@ type FileExtensionSuggestionClassification = {
const recommendationsStorageKey = 'extensionsAssistant/recommendations';
const searchMarketplace = localize('searchMarketplace', "Search Marketplace");
const milliSecondsInADay = 1000 * 60 * 60 * 24;
const processedFileExtensions: string[] = [];
export class FileBasedRecommendations extends ExtensionRecommendations {
private readonly extensionTips: IStringDictionary<string> = Object.create(null);
private readonly importantExtensionTips: IStringDictionary<{ name: string; pattern: string; isExtensionPack?: boolean }> = Object.create(null);
private readonly extensionTips = new Map<string, string>();
private readonly importantExtensionTips = new Map<string, ImportantExtensionTip>();
private fileBasedRecommendationsByPattern: IStringDictionary<string[]> = Object.create(null);
private fileBasedRecommendations: IStringDictionary<{ recommendedTime: number, sources: ExtensionRecommendationSource[] }> = Object.create(null);
private readonly fileBasedRecommendationsByPattern = new Map<string, string[]>();
private readonly fileBasedRecommendationsByLanguage = new Map<string, string[]>();
private readonly fileBasedRecommendations = new Map<string, { recommendedTime: number, sources: ExtensionRecommendationSource[] }>();
private readonly processedFileExtensions: string[] = [];
private readonly processedLanguages: string[] = [];
get recommendations(): ReadonlyArray<ExtensionRecommendation> {
const recommendations: ExtensionRecommendation[] = [];
Object.keys(this.fileBasedRecommendations)
[...this.fileBasedRecommendations.keys()]
.sort((a, b) => {
if (this.fileBasedRecommendations[a].recommendedTime === this.fileBasedRecommendations[b].recommendedTime) {
if (this.importantExtensionTips[a]) {
if (this.fileBasedRecommendations.get(a)!.recommendedTime === this.fileBasedRecommendations.get(b)!.recommendedTime) {
if (this.importantExtensionTips.has(a)) {
return -1;
}
if (this.importantExtensionTips[b]) {
if (this.importantExtensionTips.has(b)) {
return 1;
}
}
return this.fileBasedRecommendations[a].recommendedTime > this.fileBasedRecommendations[b].recommendedTime ? -1 : 1;
return this.fileBasedRecommendations.get(a)!.recommendedTime > this.fileBasedRecommendations.get(b)!.recommendedTime ? -1 : 1;
})
.forEach(extensionId => {
for (const source of this.fileBasedRecommendations[extensionId].sources) {
for (const source of this.fileBasedRecommendations.get(extensionId)!.sources) {
recommendations.push({
extensionId,
source,
@@ -75,53 +75,62 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
}
get importantRecommendations(): ReadonlyArray<ExtensionRecommendation> {
return this.recommendations.filter(e => this.importantExtensionTips[e.extensionId]);
return this.recommendations.filter(e => this.importantExtensionTips.has(e.extensionId));
}
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> {
return this.recommendations.filter(e => !this.importantExtensionTips[e.extensionId]);
return this.recommendations.filter(e => !this.importantExtensionTips.has(e.extensionId));
}
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService private readonly extensionService: IExtensionService,
@IViewletService private readonly viewletService: IViewletService,
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@IProductService productService: IProductService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@INotificationService private readonly notificationService: INotificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
if (productService.extensionTips) {
forEach(productService.extensionTips, ({ key, value }) => this.extensionTips[key.toLowerCase()] = value);
forEach(productService.extensionTips, ({ key, value }) => this.extensionTips.set(key.toLowerCase(), value));
}
if (productService.extensionImportantTips) {
forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips[key.toLowerCase()] = value);
forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips.set(key.toLowerCase(), value));
}
}
protected async doActivate(): Promise<void> {
await this.extensionService.whenInstalledExtensionsRegistered();
const allRecommendations: string[] = [];
// group extension recommendations by pattern, like {**/*.md} -> [ext.foo1, ext.bar2]
forEach(this.extensionTips, ({ key: extensionId, value: pattern }) => {
const ids = this.fileBasedRecommendationsByPattern[pattern] || [];
for (const [extensionId, pattern] of this.extensionTips) {
const ids = this.fileBasedRecommendationsByPattern.get(pattern) || [];
ids.push(extensionId);
this.fileBasedRecommendationsByPattern[pattern] = ids;
this.fileBasedRecommendationsByPattern.set(pattern, ids);
allRecommendations.push(extensionId);
});
forEach(this.importantExtensionTips, ({ key: extensionId, value }) => {
const ids = this.fileBasedRecommendationsByPattern[value.pattern] || [];
ids.push(extensionId);
this.fileBasedRecommendationsByPattern[value.pattern] = ids;
}
for (const [extensionId, value] of this.importantExtensionTips) {
if (value.pattern) {
const ids = this.fileBasedRecommendationsByPattern.get(value.pattern) || [];
ids.push(extensionId);
this.fileBasedRecommendationsByPattern.set(value.pattern, ids);
}
if (value.languages) {
for (const language of value.languages) {
const ids = this.fileBasedRecommendationsByLanguage.get(language) || [];
ids.push(extensionId);
this.fileBasedRecommendationsByLanguage.set(language, ids);
}
}
allRecommendations.push(extensionId);
});
}
const cachedRecommendations = this.getCachedRecommendations();
const now = Date.now();
@@ -129,12 +138,17 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
forEach(cachedRecommendations, ({ key, value }) => {
const diff = (now - value) / milliSecondsInADay;
if (diff <= 7 && allRecommendations.indexOf(key) > -1) {
this.fileBasedRecommendations[key] = { recommendedTime: value, sources: ['cached'] };
this.fileBasedRecommendations.set(key.toLowerCase(), { recommendedTime: value, sources: ['cached'] });
}
});
this._register(this.modelService.onModelAdded(this.promptRecommendationsForModel, this));
this.modelService.getModels().forEach(model => this.promptRecommendationsForModel(model));
this._register(this.modelService.onModelAdded(model => this.onModelAdded(model)));
this.modelService.getModels().forEach(model => this.onModelAdded(model));
}
private onModelAdded(model: ITextModel): void {
this.promptRecommendationsForModel(model);
this._register(model.onDidChangeLanguage(() => this.promptRecommendationsForModel(model)));
}
/**
@@ -144,63 +158,72 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
private promptRecommendationsForModel(model: ITextModel): void {
const uri = model.uri;
const supportedSchemes = [Schemas.untitled, Schemas.file, Schemas.vscodeRemote];
if (!uri || supportedSchemes.indexOf(uri.scheme) === -1) {
if (!uri || !supportedSchemes.includes(uri.scheme)) {
return;
}
let fileExtension = extname(uri);
if (fileExtension) {
if (processedFileExtensions.indexOf(fileExtension) > -1) {
return;
}
processedFileExtensions.push(fileExtension);
const language = model.getLanguageIdentifier().language;
const fileExtension = extname(uri);
if (this.processedLanguages.includes(language) && this.processedFileExtensions.includes(fileExtension)) {
return;
}
this.processedLanguages.push(language);
this.processedFileExtensions.push(fileExtension);
// re-schedule this bit of the operation to be off the critical path - in case glob-match is slow
setImmediate(() => this.promptRecommendations(uri, fileExtension));
setImmediate(() => this.promptRecommendations(uri, language, fileExtension));
}
private async promptRecommendations(uri: URI, fileExtension: string): Promise<void> {
const recommendationsToPrompt: string[] = [];
forEach(this.fileBasedRecommendationsByPattern, ({ key: pattern, value: extensionIds }) => {
if (match(pattern, uri.toString())) {
for (const extensionId of extensionIds) {
// Add to recommendation to prompt if it is an important tip
// Only prompt if the pattern matches the extensionImportantTips pattern
// Otherwise, assume pattern is from extensionTips, which means it should be a file based "passive" recommendation
if (this.importantExtensionTips[extensionId]?.pattern === pattern) {
recommendationsToPrompt.push(extensionId);
}
// Update file based recommendations
const filedBasedRecommendation = this.fileBasedRecommendations[extensionId] || { recommendedTime: Date.now(), sources: [] };
filedBasedRecommendation.recommendedTime = Date.now();
if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) {
filedBasedRecommendation.sources.push(uri);
}
this.fileBasedRecommendations[extensionId.toLowerCase()] = filedBasedRecommendation;
private async promptRecommendations(uri: URI, language: string, fileExtension: string): Promise<void> {
const importantRecommendations: string[] = (this.fileBasedRecommendationsByLanguage.get(language) || []).filter(extensionId => this.importantExtensionTips.has(extensionId));
let languageName: string | null = importantRecommendations.length ? this.modeService.getLanguageName(language) : null;
const fileBasedRecommendations: string[] = [...importantRecommendations];
for (let [pattern, extensionIds] of this.fileBasedRecommendationsByPattern) {
extensionIds = extensionIds.filter(extensionId => !importantRecommendations.includes(extensionId));
if (!extensionIds.length) {
continue;
}
if (!match(pattern, uri.toString())) {
continue;
}
for (const extensionId of extensionIds) {
fileBasedRecommendations.push(extensionId);
const importantExtensionTip = this.importantExtensionTips.get(extensionId);
if (importantExtensionTip && importantExtensionTip.pattern === pattern) {
importantRecommendations.push(extensionId);
}
}
});
}
// Update file based recommendations
for (const recommendation of fileBasedRecommendations) {
const filedBasedRecommendation = this.fileBasedRecommendations.get(recommendation) || { recommendedTime: Date.now(), sources: [] };
filedBasedRecommendation.recommendedTime = Date.now();
if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) {
filedBasedRecommendation.sources.push(uri);
}
this.fileBasedRecommendations.set(recommendation, filedBasedRecommendation);
}
this.storeCachedRecommendations();
if (this.hasToIgnoreRecommendationNotifications()) {
if (this.promptedExtensionRecommendations.hasToIgnoreRecommendationNotifications()) {
return;
}
const installed = await this.extensionsWorkbenchService.queryLocal();
if (await this.promptRecommendedExtensionForFileType(recommendationsToPrompt, installed)) {
if (importantRecommendations.length &&
await this.promptRecommendedExtensionForFileType(languageName || basename(uri), importantRecommendations, installed)) {
return;
}
if (fileExtension) {
fileExtension = fileExtension.substr(1); // Strip the dot
}
fileExtension = fileExtension.substr(1); // Strip the dot
if (!fileExtension) {
return;
}
await this.extensionService.whenInstalledExtensionsRegistered();
const mimeTypes = guessMimeTypes(uri);
if (mimeTypes.length !== 1 || mimeTypes[0] !== MIME_UNKNOWN) {
return;
@@ -209,9 +232,9 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
this.promptRecommendedExtensionForFileExtension(fileExtension, installed);
}
private async promptRecommendedExtensionForFileType(recommendations: string[], installed: IExtension[]): Promise<boolean> {
private async promptRecommendedExtensionForFileType(name: string, recommendations: string[], installed: IExtension[]): Promise<boolean> {
recommendations = this.filterIgnoredOrNotAllowed(recommendations);
recommendations = this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(recommendations);
if (recommendations.length === 0) {
return false;
}
@@ -222,17 +245,12 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
}
const extensionId = recommendations[0];
const entry = this.importantExtensionTips[extensionId];
const entry = this.importantExtensionTips.get(extensionId);
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.promptedExtensionRecommendations.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`);
return true;
}
@@ -310,7 +328,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
private getCachedRecommendations(): IStringDictionary<number> {
let storedRecommendations = JSON.parse(this.storageService.get(recommendationsStorageKey, StorageScope.GLOBAL, '[]'));
if (Array.isArray<string>(storedRecommendations)) {
if (Array.isArray(storedRecommendations)) {
storedRecommendations = storedRecommendations.reduce((result, id) => { result[id] = Date.now(); return result; }, <IStringDictionary<number>>{});
}
const result: IStringDictionary<number> = {};
@@ -324,7 +342,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
private storeCachedRecommendations(): void {
const storedRecommendations: IStringDictionary<number> = {};
forEach(this.fileBasedRecommendations, ({ key, value }) => storedRecommendations[key] = value.recommendedTime);
this.fileBasedRecommendations.forEach((value, key) => storedRecommendations[key] = value.recommendedTime);
this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL);
}
}

View File

@@ -3,15 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IProductService } from 'vs/platform/product/common/productService';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
export class KeymapRecommendations extends ExtensionRecommendations {
@@ -19,16 +13,10 @@ export class KeymapRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IProductService private readonly productService: IProductService,
@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);
super(promptedExtensionRecommendations);
}
protected async doActivate(): Promise<void> {

View File

@@ -3,63 +3,45 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { IFileService } from 'vs/platform/files/common/files';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { distinct, flatten, coalesce } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason, IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { parse } from 'vs/base/common/json';
import { EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
type ExtensionWorkspaceRecommendationsNotificationClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
const choiceNever = localize('neverShowAgain', "Don't Show Again");
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
import { Emitter } from 'vs/base/common/event';
export class WorkspaceRecommendations extends ExtensionRecommendations {
private _recommendations: ExtensionRecommendation[] = [];
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
private _onDidChangeRecommendations = this._register(new Emitter<void>());
readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;
private _ignoredRecommendations: string[] = [];
get ignoredRecommendations(): ReadonlyArray<string> { return this._ignoredRecommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@ILogService private readonly logService: ILogService,
@IFileService private readonly fileService: IFileService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@INotificationService private readonly notificationService: INotificationService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
}
protected async doActivate(): Promise<void> {
await this.fetch();
this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
this.promptWorkspaceRecommendations();
}
/**
@@ -71,7 +53,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigBySource.map(({ contents }) => contents));
if (invalidRecommendations.length) {
this.notificationService.warn(`The below ${invalidRecommendations.length} extension(s) in workspace recommendations have issues:\n${message}`);
this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`);
}
this._ignoredRecommendations = [];
@@ -97,63 +79,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
}
}
private async promptWorkspaceRecommendations(): Promise<void> {
const allowedRecommendations = this.recommendations.filter(rec => this.isExtensionAllowedToBeRecommended(rec.extensionId));
if (allowedRecommendations.length === 0 || this.hasToIgnoreWorkspaceRecommendationNotifications()) {
return;
}
let installed = await this.extensionManagementService.getInstalled();
installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
const recommendations = allowedRecommendations.filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
if (!recommendations.length) {
return;
}
return new Promise<void>(c => {
this.notificationService.prompt(
Severity.Info,
localize('workspaceRecommended', "This workspace has extension recommendations."),
[{
label: localize('installAll', "Install All"),
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' });
const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, recommendations.map(({ extensionId }) => extensionId));
installAllAction.run();
installAllAction.dispose();
c(undefined);
}
}, {
label: localize('showRecommendations', "Show Recommendations"),
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' });
const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations"));
showAction.run();
showAction.dispose();
c(undefined);
}
}, {
label: choiceNever,
isSecondary: true,
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' });
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
c(undefined);
}
}],
{
sticky: true,
onCancel: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' });
c(undefined);
}
}
);
});
}
private async fetchExtensionsConfigBySource(): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }[]> {
const workspace = this.contextService.getWorkspace();
const result = await Promise.all([
@@ -235,7 +160,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
await this.fetch();
// Suggest only if at least one of the newly added recommendations was not suggested before
if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) {
this.promptWorkspaceRecommendations();
this._onDidChangeRecommendations.fire();
}
}
}
@@ -250,8 +175,5 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
return null;
}
private hasToIgnoreWorkspaceRecommendationNotifications(): boolean {
return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false);
}
}

View File

@@ -24,8 +24,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions';
import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded
registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true);
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);

View File

@@ -8,7 +8,7 @@ import * as nls from 'vs/nls';
import * as os from 'os';
import { IProductService } from 'vs/platform/product/common/productService';
import { Action, IAction, Separator } from 'vs/base/common/actions';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
@@ -17,7 +17,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { IExtensionService, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { append, $, addClass, toggleClass, Dimension, clearNode } from 'vs/base/browser/dom';
import { append, $, reset, addClass, toggleClass, Dimension, clearNode } from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
@@ -38,8 +38,7 @@ import { randomPort } from 'vs/base/node/ports';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ILabelService } from 'vs/platform/label/common/label';
import { renderCodicons } from 'vs/base/common/codicons';
import { escape } from 'vs/base/common/strings';
import { renderCodiconsAsElement } from 'vs/base/browser/codicons';
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
@@ -100,7 +99,7 @@ interface IRuntimeExtension {
unresponsiveProfile?: IExtensionHostProfile;
}
export class RuntimeExtensionsEditor extends BaseEditor {
export class RuntimeExtensionsEditor extends EditorPane {
public static readonly ID: string = 'workbench.editor.runtimeExtensions';
@@ -233,11 +232,24 @@ export class RuntimeExtensionsEditor extends BaseEditor {
result = result.filter(element => element.status.activationTimes);
// bubble up extensions that have caused slowness
const isUnresponsive = (extension: IRuntimeExtension): boolean =>
extension.unresponsiveProfile === this._profileInfo;
const profileTime = (extension: IRuntimeExtension): number =>
extension.profileInfo?.totalTime ?? 0;
const activationTime = (extension: IRuntimeExtension): number =>
(extension.status.activationTimes?.codeLoadingTime ?? 0) +
(extension.status.activationTimes?.activateCallTime ?? 0);
result = result.sort((a, b) => {
if (a.unresponsiveProfile === this._profileInfo && !b.unresponsiveProfile) {
return -1;
} else if (!a.unresponsiveProfile && b.unresponsiveProfile === this._profileInfo) {
return 1;
if (isUnresponsive(a) || isUnresponsive(b)) {
return +isUnresponsive(b) - +isUnresponsive(a);
} else if (profileTime(a) || profileTime(b)) {
return profileTime(b) - profileTime(a);
} else if (activationTime(a) || activationTime(b)) {
return activationTime(b) - activationTime(a);
}
return a.originalIndex - b.originalIndex;
});
@@ -397,32 +409,28 @@ export class RuntimeExtensionsEditor extends BaseEditor {
clearNode(data.msgContainer);
if (this._extensionHostProfileService.getUnresponsiveProfile(element.description.identifier)) {
const el = $('span');
el.innerHTML = renderCodicons(escape(` $(alert) Unresponsive`));
const el = $('span', undefined, ...renderCodiconsAsElement(` $(alert) Unresponsive`));
el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze.");
data.msgContainer.appendChild(el);
}
if (isNonEmptyArray(element.status.runtimeErrors)) {
const el = $('span');
el.innerHTML = renderCodicons(escape(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));
const el = $('span', undefined, ...renderCodiconsAsElement(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));
data.msgContainer.appendChild(el);
}
if (element.status.messages && element.status.messages.length > 0) {
const el = $('span');
el.innerHTML = renderCodicons(escape(`$(alert) ${element.status.messages[0].message}`));
const el = $('span', undefined, ...renderCodiconsAsElement(`$(alert) ${element.status.messages[0].message}`));
data.msgContainer.appendChild(el);
}
if (element.description.extensionLocation.scheme !== 'file') {
const el = $('span');
el.innerHTML = renderCodicons(escape(`$(remote) ${element.description.extensionLocation.authority}`));
const el = $('span', undefined, ...renderCodiconsAsElement(`$(remote) ${element.description.extensionLocation.authority}`));
data.msgContainer.appendChild(el);
const hostLabel = this._labelService.getHostLabel(REMOTE_HOST_SCHEME, this._environmentService.configuration.remoteAuthority);
if (hostLabel) {
el.innerHTML = renderCodicons(escape(`$(remote) ${hostLabel}`));
reset(el, ...renderCodiconsAsElement(`$(remote) ${hostLabel}`));
}
}

View File

@@ -33,8 +33,7 @@ import { IPager } from 'vs/base/common/paging';
import { assign } from 'vs/base/common/objects';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { ConfigurationKey, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test';
import { IURLService } from 'vs/platform/url/common/url';
import { ITextModel } from 'vs/editor/common/model';
@@ -58,6 +57,8 @@ import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions
import { NoOpWorkspaceTagsService } from 'vs/workbench/contrib/tags/browser/workspaceTagsService';
import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags';
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
const mockExtensionGallery: IGalleryExtension[] = [
aGalleryExtension('MockExtension1', {
@@ -199,11 +200,18 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
testConfigurationService = new TestConfigurationService();
instantiationService.stub(IConfigurationService, testConfigurationService);
instantiationService.stub(INotificationService, new TestNotificationService());
instantiationService.stub(IExtensionManagementService, ExtensionManagementService);
instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event);
instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event);
instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event);
instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event);
instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
onInstallExtension: installEvent.event,
onDidInstallExtension: didInstallEvent.event,
onUninstallExtension: uninstallEvent.event,
onDidUninstallExtension: didUninstallEvent.event,
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
});
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
async whenInstalledExtensionsRegistered() { return true; }
});
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IURLService, NativeURLService);
@@ -231,6 +239,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
experimentService = instantiationService.createInstance(TestExperimentService);
instantiationService.stub(IExperimentService, experimentService);
instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService));
instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService));
onModelAddedEvent = new Emitter<ITextModel>();
@@ -302,7 +311,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
function testNoPromptForValidRecommendations(recommendations: string[]) {
return setUpFolderWorkspace('myFolder', recommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length);
assert.ok(!prompted);
});
@@ -338,20 +347,18 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return testNoPromptForValidRecommendations([]);
});
test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', () => {
return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
const recommendations = Object.keys(testObject.getAllRecommendationsWithReason());
test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => {
await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions);
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
await testObject.activationPromise;
assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length);
mockTestData.validRecommendedExtensions.forEach(x => {
assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true);
});
assert.ok(prompted);
});
const recommendations = Object.keys(testObject.getAllRecommendationsWithReason());
assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length);
mockTestData.validRecommendedExtensions.forEach(x => {
assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true);
});
assert.ok(prompted);
});
test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if they are already installed', () => {
@@ -373,7 +380,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true });
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
assert.ok(!prompted);
});
});
@@ -391,7 +398,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored
assert.ok(recommendations['ms-python.python']); // stored recommendation
@@ -409,7 +416,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored
assert.ok(recommendations['ms-python.python']); // stored recommendation
@@ -430,7 +437,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
@@ -449,7 +456,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
assert.ok(recommendations['mockpublisher1.mockextension1']);
@@ -486,7 +493,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
testObject.onRecommendationChange(changeHandlerTarget);
testObject.toggleIgnoredRecommendation(ignoredExtensionId, true);
await testObject.loadWorkspaceConfigPromise;
await testObject.activationPromise;
assert.ok(changeHandlerTarget.calledOnce);
assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: ignoredExtensionId.toLowerCase(), isRecommended: false }));
@@ -498,7 +505,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips
@@ -517,7 +524,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips

View File

@@ -101,7 +101,7 @@ async function setupTest() {
instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService {
#localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' };
constructor() {
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService));
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService));
}
get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; }
set localExtensionManagementServer(server: IExtensionManagementServer) { }

View File

@@ -16,7 +16,6 @@ import {
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IURLService } from 'vs/platform/url/common/url';
@@ -40,13 +39,13 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browse
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService';
import { IProductService } from 'vs/platform/product/common/productService';
import { ILabelService } from 'vs/platform/label/common/label';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { IMenuService } from 'vs/platform/actions/common/actions';
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
import { IProductService } from 'vs/platform/product/common/productService';
suite('ExtensionsListView Tests', () => {
@@ -68,6 +67,7 @@ suite('ExtensionsListView Tests', () => {
const workspaceRecommendationA = aGalleryExtension('workspace-recommendation-A');
const workspaceRecommendationB = aGalleryExtension('workspace-recommendation-B');
const configBasedRecommendationA = aGalleryExtension('configbased-recommendation-A');
const configBasedRecommendationB = aGalleryExtension('configbased-recommendation-B');
const fileBasedRecommendationA = aGalleryExtension('filebased-recommendation-A');
const fileBasedRecommendationB = aGalleryExtension('filebased-recommendation-B');
const otherRecommendationA = aGalleryExtension('other-recommendation-A');
@@ -89,11 +89,15 @@ suite('ExtensionsListView Tests', () => {
instantiationService.stub(ISharedProcessService, TestSharedProcessService);
instantiationService.stub(IExperimentService, ExperimentService);
instantiationService.stub(IExtensionManagementService, ExtensionManagementService);
instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event);
instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event);
instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event);
instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event);
instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
onInstallExtension: installEvent.event,
onDidInstallExtension: didInstallEvent.event,
onUninstallExtension: uninstallEvent.event,
onDidUninstallExtension: didUninstallEvent.event,
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
});
instantiationService.stub(IRemoteAgentService, RemoteAgentService);
instantiationService.stub(IContextKeyService, new MockContextKeyService());
instantiationService.stub(IMenuService, new TestMenuService());
@@ -101,7 +105,7 @@ suite('ExtensionsListView Tests', () => {
instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService {
#localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' };
constructor() {
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService));
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService));
}
get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; }
set localExtensionManagementServer(server: IExtensionManagementServer) { }
@@ -123,9 +127,10 @@ suite('ExtensionsListView Tests', () => {
{ extensionId: workspaceRecommendationB.identifier.id }]);
},
getConfigBasedRecommendations() {
return Promise.resolve([
{ extensionId: configBasedRecommendationA.identifier.id }
]);
return Promise.resolve({
important: [{ extensionId: configBasedRecommendationA.identifier.id }],
others: [{ extensionId: configBasedRecommendationB.identifier.id }],
});
},
getImportantRecommendations(): Promise<IExtensionRecommendation[]> {
return Promise.resolve([]);
@@ -138,6 +143,7 @@ suite('ExtensionsListView Tests', () => {
},
getOtherRecommendations() {
return Promise.resolve([
{ extensionId: configBasedRecommendationB.identifier.id },
{ extensionId: otherRecommendationA.identifier.id }
]);
},
@@ -333,7 +339,8 @@ suite('ExtensionsListView Tests', () => {
test('Test @recommended:workspace query', () => {
const workspaceRecommendedExtensions = [
workspaceRecommendationA,
workspaceRecommendationB
workspaceRecommendationB,
configBasedRecommendationA,
];
const target = <SinonStub>instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...workspaceRecommendedExtensions));
@@ -351,9 +358,9 @@ suite('ExtensionsListView Tests', () => {
test('Test @recommended query', () => {
const allRecommendedExtensions = [
configBasedRecommendationA,
fileBasedRecommendationA,
fileBasedRecommendationB,
configBasedRecommendationB,
otherRecommendationA
];
const target = <SinonStub>instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...allRecommendedExtensions));
@@ -379,7 +386,8 @@ suite('ExtensionsListView Tests', () => {
configBasedRecommendationA,
fileBasedRecommendationA,
fileBasedRecommendationB,
otherRecommendationA
configBasedRecommendationB,
otherRecommendationA,
];
const target = <SinonStub>instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...allRecommendedExtensions));