mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-30 00:30:29 -04:00
Merge from vscode 7eaf220cafb9d9e901370ffce02229171cbf3ea6
This commit is contained in:
committed by
Anthony Dresser
parent
39d9eed585
commit
a63578e6f7
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 */) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
: '';
|
||||
}
|
||||
|
||||
@@ -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!)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user