mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-29 16:20:29 -04:00
Merge from vscode 2e5312cd61ff99c570299ecc122c52584265eda2
This commit is contained in:
committed by
Anthony Dresser
parent
3603f55d97
commit
7f1d8fc32f
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { localize } from 'vs/nls';
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
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 { values } from 'vs/base/common/map';
|
||||
|
||||
export class ConfigBasedRecommendations extends ExtensionRecommendations {
|
||||
|
||||
private importantTips: IConfigBasedExtensionTip[] = [];
|
||||
private otherTips: IConfigBasedExtensionTip[] = [];
|
||||
|
||||
private _recommendations: ExtensionRecommendation[] = [];
|
||||
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
|
||||
|
||||
constructor(
|
||||
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
|
||||
@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);
|
||||
}
|
||||
|
||||
protected async doActivate(): Promise<void> {
|
||||
await this.fetch();
|
||||
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
|
||||
this.promptWorkspaceRecommendations();
|
||||
}
|
||||
|
||||
private async fetch(): Promise<void> {
|
||||
const workspace = this.workspaceContextService.getWorkspace();
|
||||
const importantTips: Map<string, IConfigBasedExtensionTip> = new Map<string, IConfigBasedExtensionTip>();
|
||||
const otherTips: Map<string, IConfigBasedExtensionTip> = new Map<string, IConfigBasedExtensionTip>();
|
||||
for (const folder of workspace.folders) {
|
||||
const configBasedTips = await this.extensionTipsService.getConfigBasedTips(folder.uri);
|
||||
for (const tip of configBasedTips) {
|
||||
if (tip.important) {
|
||||
importantTips.set(tip.extensionId, tip);
|
||||
} else {
|
||||
otherTips.set(tip.extensionId, tip);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.importantTips = values(importantTips);
|
||||
this.otherTips = values(otherTips).filter(tip => !importantTips.has(tip.extensionId));
|
||||
this._recommendations = [...this.importantTips, ...this.otherTips].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(ExtensionType.User);
|
||||
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.promptImportantExtensionInstallNotification(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private toExtensionRecommendation(tip: IConfigBasedExtensionTip): ExtensionRecommendation {
|
||||
return {
|
||||
extensionId: tip.extensionId,
|
||||
source: 'config',
|
||||
reason: {
|
||||
reasonId: ExtensionRecommendationReason.WorkspaceConfig,
|
||||
reasonText: localize('exeBasedRecommendation', "This extension is recommended because of the current workspace configuration")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
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 { 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 };
|
||||
cache: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
type IStoredDynamicWorkspaceRecommendations = { recommendations: string[], timestamp: number };
|
||||
const dynamicWorkspaceRecommendationsStorageKey = 'extensionsAssistant/dynamicWorkspaceRecommendations';
|
||||
const milliSecondsInADay = 1000 * 60 * 60 * 24;
|
||||
|
||||
export class DynamicWorkspaceRecommendations extends ExtensionRecommendations {
|
||||
|
||||
private _recommendations: ExtensionRecommendation[] = [];
|
||||
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
|
||||
|
||||
constructor(
|
||||
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
|
||||
@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,
|
||||
) {
|
||||
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
|
||||
}
|
||||
|
||||
protected async doActivate(): Promise<void> {
|
||||
await this.fetch();
|
||||
this._register(this.contextService.onDidChangeWorkbenchState(() => this._recommendations = []));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch extensions used by others on the same workspace as recommendations
|
||||
*/
|
||||
private async fetch(): Promise<void> {
|
||||
this._register(this.contextService.onDidChangeWorkbenchState(() => this._recommendations = []));
|
||||
|
||||
if (this._recommendations.length
|
||||
|| this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER
|
||||
|| !this.fileService.canHandleResource(this.contextService.getWorkspace().folders[0].uri)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = this.contextService.getWorkspace().folders[0];
|
||||
const cachedDynamicWorkspaceRecommendations = this.getCachedDynamicWorkspaceRecommendations();
|
||||
if (cachedDynamicWorkspaceRecommendations) {
|
||||
this._recommendations = cachedDynamicWorkspaceRecommendations.map(id => this.toExtensionRecommendation(id, folder));
|
||||
this.telemetryService.publicLog2<{ count: number, cache: number }, DynamicWorkspaceRecommendationsClassification>('dynamicWorkspaceRecommendations', { count: this._recommendations.length, cache: 1 });
|
||||
return;
|
||||
}
|
||||
|
||||
const [hashedRemotes1, hashedRemotes2] = await Promise.all([this.workspaceTagsService.getHashedRemotesFromUri(folder.uri, false), this.workspaceTagsService.getHashedRemotesFromUri(folder.uri, true)]);
|
||||
const hashedRemotes = (hashedRemotes1 || []).concat(hashedRemotes2 || []);
|
||||
if (!hashedRemotes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspacesTips = await this.extensionTipsService.getAllWorkspacesTips();
|
||||
if (!workspacesTips.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const hashedRemote of hashedRemotes) {
|
||||
const workspaceTip = workspacesTips.filter(workspaceTip => isNonEmptyArray(workspaceTip.remoteSet) && workspaceTip.remoteSet.indexOf(hashedRemote) > -1)[0];
|
||||
if (workspaceTip) {
|
||||
this._recommendations = workspaceTip.recommendations.map(id => this.toExtensionRecommendation(id, folder));
|
||||
this.storageService.store(dynamicWorkspaceRecommendationsStorageKey, JSON.stringify(<IStoredDynamicWorkspaceRecommendations>{ recommendations: workspaceTip.recommendations, timestamp: Date.now() }), StorageScope.WORKSPACE);
|
||||
this.telemetryService.publicLog2<{ count: number, cache: number }, DynamicWorkspaceRecommendationsClassification>('dynamicWorkspaceRecommendations', { count: this._recommendations.length, cache: 0 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getCachedDynamicWorkspaceRecommendations(): string[] | undefined {
|
||||
try {
|
||||
const storedDynamicWorkspaceRecommendations: IStoredDynamicWorkspaceRecommendations = JSON.parse(this.storageService.get(dynamicWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, '{}'));
|
||||
if (isNonEmptyArray(storedDynamicWorkspaceRecommendations.recommendations)
|
||||
&& isNumber(storedDynamicWorkspaceRecommendations.timestamp)
|
||||
&& storedDynamicWorkspaceRecommendations.timestamp > 0
|
||||
&& (Date.now() - storedDynamicWorkspaceRecommendations.timestamp) / milliSecondsInADay < 14) {
|
||||
return storedDynamicWorkspaceRecommendations.recommendations;
|
||||
}
|
||||
} catch (e) {
|
||||
this.storageService.remove(dynamicWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private toExtensionRecommendation(extensionId: string, folder: IWorkspaceFolder): ExtensionRecommendation {
|
||||
return {
|
||||
extensionId: extensionId.toLowerCase(),
|
||||
source: 'dynamic',
|
||||
reason: {
|
||||
reasonId: ExtensionRecommendationReason.DynamicWorkspace,
|
||||
reasonText: localize('dynamicWorkspaceRecommendation', "This extension may interest you because it's popular among users of the {0} repository.", folder.name)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { timeout } from 'vs/base/common/async';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
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';
|
||||
|
||||
type ExeExtensionRecommendationsClassification = {
|
||||
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
||||
exeName: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
||||
};
|
||||
|
||||
export class ExeBasedRecommendations extends ExtensionRecommendations {
|
||||
|
||||
readonly _recommendations: ExtensionRecommendation[] = [];
|
||||
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
|
||||
|
||||
constructor(
|
||||
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
|
||||
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@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);
|
||||
|
||||
/*
|
||||
3s has come out to be the good number to fetch and prompt important exe based recommendations
|
||||
Also fetch important exe based recommendations for reporting telemetry
|
||||
*/
|
||||
timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations());
|
||||
}
|
||||
|
||||
protected async doActivate(): Promise<void> {
|
||||
const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips();
|
||||
otherExectuableBasedTips.forEach(tip => this._recommendations.push(this.toExtensionRecommendation(tip)));
|
||||
}
|
||||
|
||||
private async fetchAndPromptImportantExeBasedRecommendations(): Promise<void> {
|
||||
const importantExeBasedRecommendations: IStringDictionary<IExecutableBasedExtensionTip> = {};
|
||||
const importantExectuableBasedTips = await this.extensionTipsService.getImportantExecutableBasedTips();
|
||||
importantExectuableBasedTips.forEach(tip => {
|
||||
this._recommendations.push(this.toExtensionRecommendation(tip));
|
||||
importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip;
|
||||
});
|
||||
|
||||
const local = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), 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!) });
|
||||
}
|
||||
for (const extensionId of uninstalled) {
|
||||
const tip = importantExeBasedRecommendations[extensionId];
|
||||
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
|
||||
}
|
||||
|
||||
this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations);
|
||||
}
|
||||
|
||||
private promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary<IExecutableBasedExtensionTip>): void {
|
||||
if (this.hasToIgnoreRecommendationNotifications()) {
|
||||
return;
|
||||
}
|
||||
recommendations = this.filterIgnoredOrNotAllowed(recommendations);
|
||||
if (recommendations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionId = recommendations[0];
|
||||
const tip = importantExeBasedRecommendations[extensionId];
|
||||
const message = localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.friendlyName!, tip.exeFriendlyName || basename(tip.windowsPath!));
|
||||
this.promptImportantExtensionInstallNotification(extensionId, 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 toExtensionRecommendation(tip: IExecutableBasedExtensionTip): ExtensionRecommendation {
|
||||
return {
|
||||
extensionId: tip.extensionId.toLowerCase(),
|
||||
source: 'executable',
|
||||
reason: {
|
||||
reasonId: ExtensionRecommendationReason.Executable,
|
||||
reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", tip.friendlyName)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { 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 {
|
||||
|
||||
private _recommendations: ExtensionRecommendation[] = [];
|
||||
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
|
||||
|
||||
constructor(
|
||||
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch extensions used by others on the same workspace as recommendations
|
||||
*/
|
||||
protected async doActivate(): Promise<void> {
|
||||
const experiments = await this.experimentService.getExperimentsByType(ExperimentActionType.AddToRecommendations);
|
||||
for (const { action, state } of experiments) {
|
||||
if (state === ExperimentState.Run && isNonEmptyArray(action?.properties?.recommendations) && action?.properties?.recommendationReason) {
|
||||
action.properties.recommendations.forEach((extensionId: string) => this._recommendations.push({
|
||||
extensionId: extensionId.toLowerCase(),
|
||||
source: 'experimental',
|
||||
reason: {
|
||||
reasonId: ExtensionRecommendationReason.Experimental,
|
||||
reasonText: action.properties.recommendationReason
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -316,10 +316,10 @@ export class ExtensionEditor extends BaseEditor {
|
||||
}
|
||||
|
||||
async setInput(input: ExtensionsInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
|
||||
await super.setInput(input, options, token);
|
||||
if (this.template) {
|
||||
await this.updateTemplate(input, this.template, !!options?.preserveFocus);
|
||||
}
|
||||
return super.setInput(input, options, token);
|
||||
}
|
||||
|
||||
private async updateTemplate(input: ExtensionsInput, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {
|
||||
@@ -499,7 +499,7 @@ export class ExtensionEditor extends BaseEditor {
|
||||
ignoreAction.enabled = true;
|
||||
template.subtext.textContent = extRecommendations[extension.identifier.id.toLowerCase()].reasonText;
|
||||
show(template.subtextContainer);
|
||||
} else if (this.extensionRecommendationsService.getAllIgnoredRecommendations().global.indexOf(extension.identifier.id.toLowerCase()) !== -1) {
|
||||
} else if (this.extensionRecommendationsService.getIgnoredRecommendations().indexOf(extension.identifier.id.toLowerCase()) !== -1) {
|
||||
undoIgnoreAction.enabled = true;
|
||||
template.subtext.textContent = localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.");
|
||||
show(template.subtextContainer);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { localize } from 'vs/nls';
|
||||
import { InstallRecommendedExtensionAction, ShowRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
|
||||
import { ExtensionRecommendationSource, IExtensionRecommendationReson } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
|
||||
type ExtensionRecommendationsNotificationClassification = {
|
||||
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
||||
};
|
||||
|
||||
const ignoreImportantExtensionRecommendation = 'extensionsAssistant/importantRecommendationsIgnore';
|
||||
const choiceNever = localize('neverShowAgain', "Don't Show Again");
|
||||
|
||||
export type ExtensionRecommendation = {
|
||||
readonly extensionId: string,
|
||||
readonly source: ExtensionRecommendationSource;
|
||||
readonly reason: IExtensionRecommendationReson;
|
||||
};
|
||||
|
||||
export abstract class ExtensionRecommendations extends Disposable {
|
||||
|
||||
readonly abstract recommendations: ReadonlyArray<ExtensionRecommendation>;
|
||||
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,
|
||||
) {
|
||||
super();
|
||||
storageKeysSyncRegistryService.registerStorageKey({ key: ignoreImportantExtensionRecommendation, version: 1 });
|
||||
}
|
||||
|
||||
private _activationPromise: Promise<void> | null = null;
|
||||
get activated(): boolean { return this._activationPromise !== null; }
|
||||
activate(): Promise<void> {
|
||||
if (!this._activationPromise) {
|
||||
this._activationPromise = this.doActivate();
|
||||
}
|
||||
return this._activationPromise;
|
||||
}
|
||||
|
||||
protected promptImportantExtensionInstallNotification(extensionId: string, message: string): void {
|
||||
this.notificationService.prompt(Severity.Info, message,
|
||||
[{
|
||||
label: localize('install', 'Install'),
|
||||
run: () => {
|
||||
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId });
|
||||
this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionId).run();
|
||||
}
|
||||
}, {
|
||||
label: localize('showRecommendations', "Show Recommendations"),
|
||||
run: () => {
|
||||
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId });
|
||||
|
||||
const recommendationsAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations"));
|
||||
recommendationsAction.run();
|
||||
recommendationsAction.dispose();
|
||||
}
|
||||
}, {
|
||||
label: choiceNever,
|
||||
isSecondary: true,
|
||||
run: () => {
|
||||
this.addToImportantRecommendationsIgnore(extensionId);
|
||||
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId });
|
||||
this.notificationService.prompt(
|
||||
Severity.Info,
|
||||
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
|
||||
[{
|
||||
label: localize('ignoreAll', "Yes, Ignore All"),
|
||||
run: () => this.setIgnoreRecommendationsConfig(true)
|
||||
}, {
|
||||
label: localize('no', "No"),
|
||||
run: () => this.setIgnoreRecommendationsConfig(false)
|
||||
}]
|
||||
);
|
||||
}
|
||||
}],
|
||||
{
|
||||
sticky: true,
|
||||
onCancel: () => {
|
||||
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected hasToIgnoreRecommendationNotifications(): boolean {
|
||||
const config = this.configurationService.getValue<IExtensionsConfiguration>(ConfigurationKey);
|
||||
return config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand;
|
||||
}
|
||||
|
||||
protected 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) {
|
||||
return false;
|
||||
}
|
||||
if (!this.isExtensionAllowedToBeRecommended(id)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private addToImportantRecommendationsIgnore(id: string) {
|
||||
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendation, StorageScope.GLOBAL, '[]'));
|
||||
importantRecommendationsIgnoreList.push(id.toLowerCase());
|
||||
this.storageService.store(ignoreImportantExtensionRecommendation, JSON.stringify(importantRecommendationsIgnoreList), StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
private setIgnoreRecommendationsConfig(configVal: boolean) {
|
||||
this.configurationService.updateValue('extensions.ignoreRecommendations', configVal, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,75 +85,76 @@ Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegis
|
||||
icon: 'codicon-extensions',
|
||||
order: 14, // {{SQL CARBON EDIT}}
|
||||
rejectAddedViews: true,
|
||||
alwaysUseContainerInfo: true
|
||||
}, ViewContainerLocation.Sidebar);
|
||||
|
||||
|
||||
// Global actions
|
||||
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions);
|
||||
|
||||
const openViewletActionDescriptor = SyncActionDescriptor.create(OpenExtensionsViewletAction, OpenExtensionsViewletAction.ID, OpenExtensionsViewletAction.LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_X });
|
||||
const openViewletActionDescriptor = SyncActionDescriptor.from(OpenExtensionsViewletAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_X });
|
||||
actionRegistry.registerWorkbenchAction(openViewletActionDescriptor, 'View: Show Extensions', localize('view', "View"));
|
||||
|
||||
const installActionDescriptor = SyncActionDescriptor.create(InstallExtensionsAction, InstallExtensionsAction.ID, InstallExtensionsAction.LABEL);
|
||||
const installActionDescriptor = SyncActionDescriptor.from(InstallExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(installActionDescriptor, 'Extensions: Install Extensions', ExtensionsLabel);
|
||||
|
||||
const listOutdatedActionDescriptor = SyncActionDescriptor.create(ShowOutdatedExtensionsAction, ShowOutdatedExtensionsAction.ID, ShowOutdatedExtensionsAction.LABEL);
|
||||
const listOutdatedActionDescriptor = SyncActionDescriptor.from(ShowOutdatedExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(listOutdatedActionDescriptor, 'Extensions: Show Outdated Extensions', ExtensionsLabel);
|
||||
|
||||
const recommendationsActionDescriptor = SyncActionDescriptor.create(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL);
|
||||
const recommendationsActionDescriptor = SyncActionDescriptor.from(ShowRecommendedExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(recommendationsActionDescriptor, 'Extensions: Show Recommended Extensions', ExtensionsLabel);
|
||||
|
||||
const keymapRecommendationsActionDescriptor = SyncActionDescriptor.create(ShowRecommendedKeymapExtensionsAction, ShowRecommendedKeymapExtensionsAction.ID, ShowRecommendedKeymapExtensionsAction.SHORT_LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_M) });
|
||||
const keymapRecommendationsActionDescriptor = SyncActionDescriptor.from(ShowRecommendedKeymapExtensionsAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_M) });
|
||||
actionRegistry.registerWorkbenchAction(keymapRecommendationsActionDescriptor, 'Preferences: Keymaps', PreferencesLabel);
|
||||
|
||||
const languageExtensionsActionDescriptor = SyncActionDescriptor.create(ShowLanguageExtensionsAction, ShowLanguageExtensionsAction.ID, ShowLanguageExtensionsAction.SHORT_LABEL);
|
||||
const languageExtensionsActionDescriptor = SyncActionDescriptor.from(ShowLanguageExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(languageExtensionsActionDescriptor, 'Preferences: Language Extensions', PreferencesLabel);
|
||||
|
||||
const azureExtensionsActionDescriptor = SyncActionDescriptor.create(ShowAzureExtensionsAction, ShowAzureExtensionsAction.ID, ShowAzureExtensionsAction.SHORT_LABEL);
|
||||
const azureExtensionsActionDescriptor = SyncActionDescriptor.from(ShowAzureExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(azureExtensionsActionDescriptor, 'Preferences: Azure Extensions', PreferencesLabel);
|
||||
|
||||
const popularActionDescriptor = SyncActionDescriptor.create(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL);
|
||||
const popularActionDescriptor = SyncActionDescriptor.from(ShowPopularExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(popularActionDescriptor, 'Extensions: Show Popular Extensions', ExtensionsLabel);
|
||||
|
||||
const enabledActionDescriptor = SyncActionDescriptor.create(ShowEnabledExtensionsAction, ShowEnabledExtensionsAction.ID, ShowEnabledExtensionsAction.LABEL);
|
||||
const enabledActionDescriptor = SyncActionDescriptor.from(ShowEnabledExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(enabledActionDescriptor, 'Extensions: Show Enabled Extensions', ExtensionsLabel);
|
||||
|
||||
const installedActionDescriptor = SyncActionDescriptor.create(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL);
|
||||
const installedActionDescriptor = SyncActionDescriptor.from(ShowInstalledExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(installedActionDescriptor, 'Extensions: Show Installed Extensions', ExtensionsLabel);
|
||||
|
||||
const disabledActionDescriptor = SyncActionDescriptor.create(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, ShowDisabledExtensionsAction.LABEL);
|
||||
const disabledActionDescriptor = SyncActionDescriptor.from(ShowDisabledExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(disabledActionDescriptor, 'Extensions: Show Disabled Extensions', ExtensionsLabel);
|
||||
|
||||
const builtinActionDescriptor = SyncActionDescriptor.create(ShowBuiltInExtensionsAction, ShowBuiltInExtensionsAction.ID, ShowBuiltInExtensionsAction.LABEL);
|
||||
const builtinActionDescriptor = SyncActionDescriptor.from(ShowBuiltInExtensionsAction);
|
||||
actionRegistry.registerWorkbenchAction(builtinActionDescriptor, 'Extensions: Show Built-in Extensions', ExtensionsLabel);
|
||||
|
||||
const updateAllActionDescriptor = SyncActionDescriptor.create(UpdateAllAction, UpdateAllAction.ID, UpdateAllAction.LABEL);
|
||||
const updateAllActionDescriptor = SyncActionDescriptor.from(UpdateAllAction);
|
||||
actionRegistry.registerWorkbenchAction(updateAllActionDescriptor, 'Extensions: Update All Extensions', ExtensionsLabel);
|
||||
|
||||
if (InstallVSIXAction.AVAILABLE) { // {{SQL CARBON EDIT}} add disable logic
|
||||
const installVSIXActionDescriptor = SyncActionDescriptor.create(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL);
|
||||
const installVSIXActionDescriptor = SyncActionDescriptor.from(InstallVSIXAction);
|
||||
actionRegistry.registerWorkbenchAction(installVSIXActionDescriptor, 'Extensions: Install from VSIX...', ExtensionsLabel);
|
||||
}
|
||||
|
||||
const disableAllAction = SyncActionDescriptor.create(DisableAllAction, DisableAllAction.ID, DisableAllAction.LABEL);
|
||||
const disableAllAction = SyncActionDescriptor.from(DisableAllAction);
|
||||
actionRegistry.registerWorkbenchAction(disableAllAction, 'Extensions: Disable All Installed Extensions', ExtensionsLabel);
|
||||
|
||||
const disableAllWorkspaceAction = SyncActionDescriptor.create(DisableAllWorkspaceAction, DisableAllWorkspaceAction.ID, DisableAllWorkspaceAction.LABEL);
|
||||
const disableAllWorkspaceAction = SyncActionDescriptor.from(DisableAllWorkspaceAction);
|
||||
actionRegistry.registerWorkbenchAction(disableAllWorkspaceAction, 'Extensions: Disable All Installed Extensions for this Workspace', ExtensionsLabel);
|
||||
|
||||
const enableAllAction = SyncActionDescriptor.create(EnableAllAction, EnableAllAction.ID, EnableAllAction.LABEL);
|
||||
const enableAllAction = SyncActionDescriptor.from(EnableAllAction);
|
||||
actionRegistry.registerWorkbenchAction(enableAllAction, 'Extensions: Enable All Extensions', ExtensionsLabel);
|
||||
|
||||
const enableAllWorkspaceAction = SyncActionDescriptor.create(EnableAllWorkspaceAction, EnableAllWorkspaceAction.ID, EnableAllWorkspaceAction.LABEL);
|
||||
const enableAllWorkspaceAction = SyncActionDescriptor.from(EnableAllWorkspaceAction);
|
||||
actionRegistry.registerWorkbenchAction(enableAllWorkspaceAction, 'Extensions: Enable All Extensions for this Workspace', ExtensionsLabel);
|
||||
|
||||
const checkForUpdatesAction = SyncActionDescriptor.create(CheckForUpdatesAction, CheckForUpdatesAction.ID, CheckForUpdatesAction.LABEL);
|
||||
const checkForUpdatesAction = SyncActionDescriptor.from(CheckForUpdatesAction);
|
||||
actionRegistry.registerWorkbenchAction(checkForUpdatesAction, `Extensions: Check for Extension Updates`, ExtensionsLabel);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(EnableAutoUpdateAction, EnableAutoUpdateAction.ID, EnableAutoUpdateAction.LABEL), `Extensions: Enable Auto Updating Extensions`, ExtensionsLabel);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(DisableAutoUpdateAction, DisableAutoUpdateAction.ID, DisableAutoUpdateAction.LABEL), `Extensions: Disable Auto Updating Extensions`, ExtensionsLabel);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(InstallSpecificVersionOfExtensionAction, InstallSpecificVersionOfExtensionAction.ID, InstallSpecificVersionOfExtensionAction.LABEL), 'Install Specific Version of Extension...', ExtensionsLabel);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ReinstallAction, ReinstallAction.ID, ReinstallAction.LABEL), 'Reinstall Extension...', localize('developer', "Developer"));
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAutoUpdateAction), `Extensions: Enable Auto Updating Extensions`, ExtensionsLabel);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAutoUpdateAction), `Extensions: Disable Auto Updating Extensions`, ExtensionsLabel);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(InstallSpecificVersionOfExtensionAction), 'Install Specific Version of Extension...', ExtensionsLabel);
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ReinstallAction), 'Reinstall Extension...', localize('developer', "Developer"));
|
||||
|
||||
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
|
||||
.registerConfiguration({
|
||||
|
||||
@@ -16,7 +16,7 @@ import { dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, EXTENSIONS_CONFIG, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate';
|
||||
import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionRecommendation, IExtensionsConfigContent, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionsConfigContent, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension, ExtensionsPolicy, ExtensionsPolicyKey } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}}
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -59,6 +59,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; // {{SQL CARBON EDIT}}
|
||||
import product from 'vs/platform/product/common/product';
|
||||
@@ -1718,14 +1719,14 @@ export class InstallWorkspaceRecommendedExtensionsAction extends Action {
|
||||
static readonly ID = 'workbench.extensions.action.installWorkspaceRecommendedExtensions';
|
||||
static readonly LABEL = localize('installWorkspaceRecommendedExtensions', "Install All Workspace Recommended Extensions");
|
||||
|
||||
private _recommendations: IExtensionRecommendation[] = [];
|
||||
get recommendations(): IExtensionRecommendation[] { return this._recommendations; }
|
||||
set recommendations(recommendations: IExtensionRecommendation[]) { this._recommendations = recommendations; this.enabled = this._recommendations.length > 0; }
|
||||
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 = InstallWorkspaceRecommendedExtensionsAction.ID,
|
||||
label: string = InstallWorkspaceRecommendedExtensionsAction.LABEL,
|
||||
recommendations: IExtensionRecommendation[],
|
||||
recommendations: string[],
|
||||
@IViewletService private readonly viewletService: IViewletService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
|
||||
@@ -1743,7 +1744,7 @@ export class InstallWorkspaceRecommendedExtensionsAction extends Action {
|
||||
.then(viewlet => {
|
||||
viewlet.search('@recommended ');
|
||||
viewlet.focus();
|
||||
const names = this.recommendations.map(({ extensionId }) => extensionId);
|
||||
const names = this.recommendations;
|
||||
return this.extensionWorkbenchService.queryGallery({ names, source: 'install-all-workspace-recommendations' }, CancellationToken.None).then(pager => {
|
||||
let installPromises: Promise<any>[] = [];
|
||||
let model = new PagedModel(pager);
|
||||
@@ -1864,7 +1865,7 @@ export class UndoIgnoreExtensionRecommendationAction extends Action {
|
||||
export class ShowRecommendedKeymapExtensionsAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.extensions.action.showRecommendedKeymapExtensions';
|
||||
static readonly SHORT_LABEL = localize('showRecommendedKeymapExtensionsShort', "Keymaps");
|
||||
static readonly LABEL = localize('showRecommendedKeymapExtensionsShort', "Keymaps");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
@@ -1887,7 +1888,7 @@ export class ShowRecommendedKeymapExtensionsAction extends Action {
|
||||
export class ShowLanguageExtensionsAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.extensions.action.showLanguageExtensions';
|
||||
static readonly SHORT_LABEL = localize('showLanguageExtensionsShort', "Language Extensions");
|
||||
static readonly LABEL = localize('showLanguageExtensionsShort', "Language Extensions");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
@@ -1910,7 +1911,7 @@ export class ShowLanguageExtensionsAction extends Action {
|
||||
export class ShowAzureExtensionsAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.extensions.action.showAzureExtensions';
|
||||
static readonly SHORT_LABEL = localize('showAzureExtensionsShort', "Azure Extensions");
|
||||
static readonly LABEL = localize('showAzureExtensionsShort', "Azure Extensions");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
@@ -2712,8 +2713,8 @@ export class ExtensionToolTipAction extends ExtensionAction {
|
||||
export class SystemDisabledWarningAction extends ExtensionAction {
|
||||
|
||||
private static readonly CLASS = `${ExtensionAction.ICON_ACTION_CLASS} system-disable`;
|
||||
private static readonly WARNING_CLASS = `${SystemDisabledWarningAction.CLASS} codicon-warning`;
|
||||
private static readonly INFO_CLASS = `${SystemDisabledWarningAction.CLASS} codicon-info`;
|
||||
private static readonly WARNING_CLASS = `${SystemDisabledWarningAction.CLASS} ${Codicon.warning.classNames}`;
|
||||
private static readonly INFO_CLASS = `${SystemDisabledWarningAction.CLASS} ${Codicon.info.classNames}`;
|
||||
|
||||
updateWhenCounterExtensionChanges: boolean = true;
|
||||
private _runningExtensions: IExtensionDescription[] | null = null;
|
||||
@@ -2754,8 +2755,8 @@ export class SystemDisabledWarningAction extends ExtensionAction {
|
||||
if (!this.extensionsWorkbenchService.installed.some(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server !== this.extension!.server)) {
|
||||
this.class = `${SystemDisabledWarningAction.INFO_CLASS}`;
|
||||
this.tooltip = this.extension.server === this.extensionManagementServerService.localExtensionManagementServer
|
||||
? localize('Install language pack also in remote server', "Install the language pack extension on '{0}' to enable it also there.", this.extensionManagementServerService.remoteExtensionManagementServer.label)
|
||||
: localize('Install language pack also locally', "Install the language pack extension locally to enable it also there.");
|
||||
? localize('Install language pack also in remote server', "Install the language pack extension on '{0}' to enable it there also.", this.extensionManagementServerService.remoteExtensionManagementServer.label)
|
||||
: localize('Install language pack also locally', "Install the language pack extension locally to enable it there also.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { listFocusForeground, listFocusBackground } from 'vs/platform/theme/comm
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
|
||||
export class ExtensionsGridView extends Disposable {
|
||||
|
||||
@@ -267,7 +268,15 @@ export class ExtensionsTree extends WorkbenchAsyncDataTree<IExtensionData, IExte
|
||||
indent: 40,
|
||||
identityProvider,
|
||||
multipleSelectionSupport: false,
|
||||
overrideStyles
|
||||
overrideStyles,
|
||||
accessibilityProvider: <IListAccessibilityProvider<IExtensionData>>{
|
||||
getAriaLabel(extensionData: IExtensionData): string {
|
||||
return localize('extension-arialabel', "{0}. Press enter for extension details.", extensionData.extension.displayName);
|
||||
},
|
||||
getWidgetAriaLabel(): string {
|
||||
return localize('extensions', "Extensions");
|
||||
}
|
||||
}
|
||||
},
|
||||
contextKeyService, listService, themeService, configurationService, keybindingService, accessibilityService
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ import Severity from 'vs/base/common/severity';
|
||||
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewContainersRegistry, IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewDescriptorService, IAddedViewDescriptorRef } from 'vs/workbench/common/views';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
@@ -44,7 +44,6 @@ import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views';
|
||||
import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
|
||||
import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery';
|
||||
import { SuggestEnabledInput, attachSuggestEnabledInputBoxStyler } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput';
|
||||
@@ -94,8 +93,9 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
|
||||
constructor(
|
||||
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IViewDescriptorService viewDescriptorService: IViewDescriptorService
|
||||
) {
|
||||
this.container = Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).get(VIEWLET_ID)!;
|
||||
this.container = viewDescriptorService.getViewContainerById(VIEWLET_ID)!;
|
||||
this.registerViews();
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
|
||||
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
|
||||
@IPreferencesService private readonly preferencesService: IPreferencesService
|
||||
) {
|
||||
super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService);
|
||||
super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService);
|
||||
|
||||
this.searchDelayer = new Delayer(500);
|
||||
this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService);
|
||||
|
||||
@@ -28,7 +28,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
|
||||
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
|
||||
import { WorkbenchPagedList, ResourceNavigator } from 'vs/platform/list/browser/listService';
|
||||
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';
|
||||
@@ -50,6 +50,7 @@ import { IMenuService } from 'vs/platform/actions/common/actions';
|
||||
import { IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
|
||||
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'];
|
||||
@@ -136,10 +137,17 @@ export class ExtensionsListView extends ViewPane {
|
||||
const extensionsViewState = new ExtensionsViewState();
|
||||
const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState);
|
||||
this.list = this.instantiationService.createInstance<typeof WorkbenchPagedList, WorkbenchPagedList<IExtension>>(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], {
|
||||
ariaLabel: localize('extensions', "Extensions"),
|
||||
multipleSelectionSupport: false,
|
||||
setRowLineHeight: false,
|
||||
horizontalScrolling: false,
|
||||
accessibilityProvider: <IListAccessibilityProvider<IExtension | null>>{
|
||||
getAriaLabel(extension: IExtension | null): string {
|
||||
return extension ? localize('extension-arialabel', "{0}. Press enter for extension details.", extension.displayName) : '';
|
||||
},
|
||||
getWidgetAriaLabel(): string {
|
||||
return localize('extensions', "Extensions");
|
||||
}
|
||||
},
|
||||
overrideStyles: {
|
||||
listBackground: SIDE_BAR_BACKGROUND
|
||||
}
|
||||
@@ -149,7 +157,7 @@ export class ExtensionsListView extends ViewPane {
|
||||
this._register(this.list);
|
||||
this._register(extensionsViewState);
|
||||
|
||||
const resourceNavigator = this._register(ResourceNavigator.createListResourceNavigator(this.list, { openOnFocus: false, openOnSelection: true, openOnSingleClick: true }));
|
||||
const resourceNavigator = this._register(new ListResourceNavigator(this.list, { openOnFocus: false, openOnSelection: true, openOnSingleClick: true }));
|
||||
this._register(Event.debounce(Event.filter(resourceNavigator.onDidOpenResource, e => e.element !== null), (last, event) => event, 75, true)(options => {
|
||||
this.openExtension(this.list!.model.get(options.element!), { sideByside: options.sideBySide, ...options.editorOptions });
|
||||
}));
|
||||
@@ -168,6 +176,7 @@ export class ExtensionsListView extends ViewPane {
|
||||
}
|
||||
|
||||
protected layoutBody(height: number, width: number): void {
|
||||
super.layoutBody(height, width);
|
||||
if (this.bodyTemplate) {
|
||||
this.bodyTemplate.extensionsList.style.height = height + 'px';
|
||||
}
|
||||
@@ -563,12 +572,13 @@ export class ExtensionsListView extends ViewPane {
|
||||
.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();
|
||||
|
||||
return Promise.all([othersPromise, workspacePromise])
|
||||
.then(([others, workspaceRecommendations]) => {
|
||||
const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, others, workspaceRecommendations);
|
||||
return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise])
|
||||
.then(([others, workspaceRecommendations, configBasedRecommendations]) => {
|
||||
const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, configBasedRecommendations, others, workspaceRecommendations);
|
||||
const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason();
|
||||
/* __GDPR__
|
||||
"extensionAllRecommendations:open" : {
|
||||
@@ -618,15 +628,17 @@ export class ExtensionsListView extends ViewPane {
|
||||
.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();
|
||||
|
||||
return Promise.all([othersPromise, workspacePromise])
|
||||
.then(([others, workspaceRecommendations]) => {
|
||||
return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise])
|
||||
.then(([others, workspaceRecommendations, configBasedRecommendations]) => {
|
||||
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, fileBasedRecommendations, others, []);
|
||||
const names = this.getTrimmedRecommendations(local, value, fileBasedRecommendations, configBasedRecommendations, others, []);
|
||||
const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason();
|
||||
|
||||
/* __GDPR__
|
||||
@@ -720,28 +732,36 @@ 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, fileBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] {
|
||||
const totalCount = 8;
|
||||
private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, 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;
|
||||
});
|
||||
configBasedRecommendations = configBasedRecommendations
|
||||
.filter(recommendation => {
|
||||
return !this.isRecommendationInstalled(recommendation, installedExtensions)
|
||||
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.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)
|
||||
&& 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)
|
||||
&& 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 - otherCount);
|
||||
const recommendations = workspaceRecommendations;
|
||||
const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - configBasedRecommendations.length - otherCount);
|
||||
const recommendations = [...workspaceRecommendations, ...configBasedRecommendations];
|
||||
recommendations.push(...fileBasedRecommendations.splice(0, fileBasedCount));
|
||||
recommendations.push(...otherRecommendations.splice(0, otherCount));
|
||||
|
||||
@@ -1108,7 +1128,7 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView {
|
||||
private async setRecommendationsToInstall(): Promise<void> {
|
||||
const recommendations = await this.getRecommendationsToInstall();
|
||||
if (this.installAllAction) {
|
||||
this.installAllAction.recommendations = recommendations;
|
||||
this.installAllAction.recommendations = recommendations.map(({ extensionId }) => extensionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,12 +159,7 @@ export class TooltipWidget extends ExtensionWidget {
|
||||
}
|
||||
|
||||
render(): void {
|
||||
this.parent.title = '';
|
||||
this.parent.removeAttribute('aria-label');
|
||||
this.parent.title = this.getTooltip();
|
||||
if (this.extension) {
|
||||
this.parent.setAttribute('aria-label', localize('extension-arialabel', "{0}. Press enter for extension details.", this.extension.displayName));
|
||||
}
|
||||
}
|
||||
|
||||
private getTooltip(): string {
|
||||
@@ -208,7 +203,6 @@ export class RecommendationWidget extends ExtensionWidget {
|
||||
|
||||
private clear(): void {
|
||||
this.tooltip = '';
|
||||
this.parent.setAttribute('aria-label', this.extension ? localize('viewExtensionDetailsAria', "{0}. Press enter for extension details.", this.extension.displayName) : '');
|
||||
if (this.element) {
|
||||
this.parent.removeChild(this.element);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
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 { 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 { 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';
|
||||
|
||||
type FileExtensionSuggestionClassification = {
|
||||
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
fileExtension: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
|
||||
};
|
||||
|
||||
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 fileBasedRecommendationsByPattern: IStringDictionary<string[]> = Object.create(null);
|
||||
private fileBasedRecommendations: IStringDictionary<{ recommendedTime: number, sources: ExtensionRecommendationSource[] }> = Object.create(null);
|
||||
|
||||
get recommendations(): ReadonlyArray<ExtensionRecommendation> {
|
||||
const recommendations: ExtensionRecommendation[] = [];
|
||||
Object.keys(this.fileBasedRecommendations)
|
||||
.sort((a, b) => {
|
||||
if (this.fileBasedRecommendations[a].recommendedTime === this.fileBasedRecommendations[b].recommendedTime) {
|
||||
if (this.importantExtensionTips[a]) {
|
||||
return -1;
|
||||
}
|
||||
if (this.importantExtensionTips[b]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return this.fileBasedRecommendations[a].recommendedTime > this.fileBasedRecommendations[b].recommendedTime ? -1 : 1;
|
||||
})
|
||||
.forEach(extensionId => {
|
||||
for (const source of this.fileBasedRecommendations[extensionId].sources) {
|
||||
recommendations.push({
|
||||
extensionId,
|
||||
source,
|
||||
reason: {
|
||||
reasonId: ExtensionRecommendationReason.File,
|
||||
reasonText: localize('fileBasedRecommendation', "This extension is recommended based on the files you recently opened.")
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
constructor(
|
||||
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@IViewletService private readonly viewletService: IViewletService,
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IProductService 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);
|
||||
|
||||
if (productService.extensionTips) {
|
||||
forEach(productService.extensionTips, ({ key, value }) => this.extensionTips[key.toLowerCase()] = value);
|
||||
}
|
||||
if (productService.extensionImportantTips) {
|
||||
forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips[key.toLowerCase()] = value);
|
||||
}
|
||||
}
|
||||
|
||||
protected async doActivate(): Promise<void> {
|
||||
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] || [];
|
||||
ids.push(extensionId);
|
||||
this.fileBasedRecommendationsByPattern[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;
|
||||
allRecommendations.push(extensionId);
|
||||
});
|
||||
|
||||
const cachedRecommendations = this.getCachedRecommendations();
|
||||
const now = Date.now();
|
||||
// Retire existing recommendations if they are older than a week or are not part of this.productService.extensionTips anymore
|
||||
forEach(cachedRecommendations, ({ key, value }) => {
|
||||
const diff = (now - value) / milliSecondsInADay;
|
||||
if (diff <= 7 && allRecommendations.indexOf(key) > -1) {
|
||||
this.fileBasedRecommendations[key] = { recommendedTime: value, sources: ['cached'] };
|
||||
}
|
||||
});
|
||||
|
||||
this._register(this.modelService.onModelAdded(this.promptRecommendationsForModel, this));
|
||||
this.modelService.getModels().forEach(model => this.promptRecommendationsForModel(model));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to either install the recommended extension for the file type in the current editor model
|
||||
* or prompt to search the marketplace if it has extensions that can support the file type
|
||||
*/
|
||||
private promptRecommendationsForModel(model: ITextModel): void {
|
||||
const uri = model.uri;
|
||||
const supportedSchemes = [Schemas.untitled, Schemas.file, Schemas.vscodeRemote];
|
||||
if (!uri || supportedSchemes.indexOf(uri.scheme) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fileExtension = extname(uri);
|
||||
if (fileExtension) {
|
||||
if (processedFileExtensions.indexOf(fileExtension) > -1) {
|
||||
return;
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
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
|
||||
if (this.importantExtensionTips[extensionId]) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.storeCachedRecommendations();
|
||||
|
||||
if (this.hasToIgnoreRecommendationNotifications()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installed = await this.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
if (await this.promptRecommendedExtensionForFileType(recommendationsToPrompt, installed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileExtension) {
|
||||
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;
|
||||
}
|
||||
|
||||
this.promptRecommendedExtensionForFileExtension(fileExtension, installed);
|
||||
}
|
||||
|
||||
private async promptRecommendedExtensionForFileType(recommendations: string[], installed: ILocalExtension[]): Promise<boolean> {
|
||||
|
||||
recommendations = this.filterIgnoredOrNotAllowed(recommendations);
|
||||
if (recommendations.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
recommendations = this.filterInstalled(recommendations, installed);
|
||||
if (recommendations.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionId = recommendations[0];
|
||||
const entry = this.importantExtensionTips[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.promptImportantExtensionInstallNotification(extensionId, message);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async promptRecommendedExtensionForFileExtension(fileExtension: string, installed: ILocalExtension[]): Promise<void> {
|
||||
const fileExtensionSuggestionIgnoreList = <string[]>JSON.parse(this.storageService.get('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]'));
|
||||
if (fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = `ext:${fileExtension}`;
|
||||
const pager = await this.extensionsWorkbenchService.queryGallery({ text, pageSize: 100 }, CancellationToken.None);
|
||||
if (pager.firstPage.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
|
||||
if (pager.firstPage.some(e => installedExtensionsIds.has(e.identifier.id.toLowerCase()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notificationService.prompt(
|
||||
Severity.Info,
|
||||
localize('showLanguageExtensions', "The Marketplace has extensions that can help with '.{0}' files", fileExtension),
|
||||
[{
|
||||
label: searchMarketplace,
|
||||
run: () => {
|
||||
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'ok', fileExtension });
|
||||
this.viewletService.openViewlet('workbench.view.extensions', true)
|
||||
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
|
||||
.then(viewlet => {
|
||||
viewlet.search(`ext:${fileExtension}`);
|
||||
viewlet.focus();
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: localize('dontShowAgainExtension', "Don't Show Again for '.{0}' files", fileExtension),
|
||||
run: () => {
|
||||
fileExtensionSuggestionIgnoreList.push(fileExtension);
|
||||
this.storageService.store(
|
||||
'extensionsAssistant/fileExtensionsSuggestionIgnore',
|
||||
JSON.stringify(fileExtensionSuggestionIgnoreList),
|
||||
StorageScope.GLOBAL
|
||||
);
|
||||
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'neverShowAgain', fileExtension });
|
||||
}
|
||||
}],
|
||||
{
|
||||
sticky: true,
|
||||
onCancel: () => {
|
||||
this.telemetryService.publicLog2<{ userReaction: string, fileExtension: string }, FileExtensionSuggestionClassification>('fileExtensionSuggestion:popup', { userReaction: 'cancelled', fileExtension });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private filterInstalled(recommendationsToSuggest: string[], installed: ILocalExtension[]): string[] {
|
||||
const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
|
||||
return recommendationsToSuggest.filter(id => !installedExtensionsIds.has(id.toLowerCase()));
|
||||
}
|
||||
|
||||
private getCachedRecommendations(): IStringDictionary<number> {
|
||||
let storedRecommendations = JSON.parse(this.storageService.get(recommendationsStorageKey, StorageScope.GLOBAL, '[]'));
|
||||
if (Array.isArray<string>(storedRecommendations)) {
|
||||
storedRecommendations = storedRecommendations.reduce((result, id) => { result[id] = Date.now(); return result; }, <IStringDictionary<number>>{});
|
||||
}
|
||||
const result: IStringDictionary<number> = {};
|
||||
forEach(storedRecommendations, ({ key, value }) => {
|
||||
if (typeof value === 'number') {
|
||||
result[key.toLowerCase()] = value;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private storeCachedRecommendations(): void {
|
||||
const storedRecommendations: IStringDictionary<number> = {};
|
||||
forEach(this.fileBasedRecommendations, ({ key, value }) => storedRecommendations[key] = value.recommendedTime);
|
||||
this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { 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 {
|
||||
|
||||
private _recommendations: ExtensionRecommendation[] = [];
|
||||
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
|
||||
|
||||
constructor(
|
||||
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
|
||||
@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);
|
||||
}
|
||||
|
||||
protected async doActivate(): Promise<void> {
|
||||
if (this.productService.keymapExtensionTips) {
|
||||
this._recommendations = this.productService.keymapExtensionTips.map(extensionId => (<ExtensionRecommendation>{
|
||||
extensionId: extensionId.toLowerCase(),
|
||||
source: 'application',
|
||||
reason: {
|
||||
reasonId: ExtensionRecommendationReason.Application,
|
||||
reasonText: ''
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-action-bar .action-item .action-label.extension-action {
|
||||
padding: 0 5px;
|
||||
outline-offset: 2px;
|
||||
line-height: initial;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .action-label.extension-action.label {
|
||||
padding: 0 5px;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .action-label.extension-action.text,
|
||||
.monaco-action-bar .action-item .action-label.extension-action.label {
|
||||
line-height: 14px;
|
||||
@@ -19,8 +22,9 @@
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .action-label.extension-action.icon {
|
||||
padding: 0 2px;
|
||||
height: 18px;
|
||||
width: 10px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .action-label.extension-action.multiserver.install:after,
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* 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 { 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 { 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 { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { InstallWorkspaceRecommendedExtensionsAction, ShowRecommendedExtensionsAction } 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';
|
||||
|
||||
export class WorkspaceRecommendations extends ExtensionRecommendations {
|
||||
|
||||
private _recommendations: ExtensionRecommendation[] = [];
|
||||
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
|
||||
|
||||
private _ignoredRecommendations: string[] = [];
|
||||
get ignoredRecommendations(): ReadonlyArray<string> { return this._ignoredRecommendations; }
|
||||
|
||||
constructor(
|
||||
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
|
||||
@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,
|
||||
) {
|
||||
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
|
||||
}
|
||||
|
||||
protected async doActivate(): Promise<void> {
|
||||
await this.fetch();
|
||||
this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
|
||||
this.promptWorkspaceRecommendations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all extensions.json files, fetch workspace recommendations, filter out invalid and unwanted ones
|
||||
*/
|
||||
private async fetch(): Promise<void> {
|
||||
|
||||
const extensionsConfigBySource = await this.fetchExtensionsConfigBySource();
|
||||
|
||||
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._ignoredRecommendations = [];
|
||||
|
||||
for (const extensionsConfig of extensionsConfigBySource) {
|
||||
for (const unwantedRecommendation of extensionsConfig.contents.unwantedRecommendations) {
|
||||
if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) {
|
||||
this._ignoredRecommendations.push(unwantedRecommendation);
|
||||
}
|
||||
}
|
||||
for (const extensionId of extensionsConfig.contents.recommendations) {
|
||||
if (invalidRecommendations.indexOf(extensionId) === -1) {
|
||||
this._recommendations.push({
|
||||
extensionId,
|
||||
source: extensionsConfig.source,
|
||||
reason: {
|
||||
reasonId: ExtensionRecommendationReason.Workspace,
|
||||
reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(ExtensionType.User);
|
||||
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, InstallWorkspaceRecommendedExtensionsAction.ID, localize('installAll', "Install All"), 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([
|
||||
this.resolveWorkspaceExtensionConfig(workspace),
|
||||
...workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder))
|
||||
]);
|
||||
return coalesce(result);
|
||||
}
|
||||
|
||||
private async resolveWorkspaceExtensionConfig(workspace: IWorkspace): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource } | null> {
|
||||
try {
|
||||
if (workspace.configuration) {
|
||||
const content = await this.fileService.readFile(workspace.configuration);
|
||||
const extensionsConfigContent = <IExtensionsConfigContent | undefined>parse(content.value.toString())['extensions'];
|
||||
const contents = this.parseExtensionConfig(extensionsConfigContent);
|
||||
if (contents) {
|
||||
return { contents, source: workspace };
|
||||
}
|
||||
}
|
||||
} catch (e) { /* Ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource } | null> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(workspaceFolder.toResource(EXTENSIONS_CONFIG));
|
||||
const extensionsConfigContent = <IExtensionsConfigContent>parse(content.value.toString());
|
||||
const contents = this.parseExtensionConfig(extensionsConfigContent);
|
||||
if (contents) {
|
||||
return { contents, source: workspaceFolder };
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
private async validateExtensions(contents: IExtensionsConfigContent[]): Promise<{ validRecommendations: string[], invalidRecommendations: string[], message: string }> {
|
||||
|
||||
const validExtensions: string[] = [];
|
||||
const invalidExtensions: string[] = [];
|
||||
const extensionsToQuery: string[] = [];
|
||||
let message = '';
|
||||
|
||||
const allRecommendations = distinct(flatten(contents.map(({ recommendations }) => recommendations || [])));
|
||||
const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN);
|
||||
for (const extensionId of allRecommendations) {
|
||||
if (regEx.test(extensionId)) {
|
||||
extensionsToQuery.push(extensionId);
|
||||
} else {
|
||||
invalidExtensions.push(extensionId);
|
||||
message += `${extensionId} (bad format) Expected: <provider>.<name>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionsToQuery.length) {
|
||||
try {
|
||||
const queryResult = await this.galleryService.query({ names: extensionsToQuery, pageSize: extensionsToQuery.length }, CancellationToken.None);
|
||||
const extensions = queryResult.firstPage.map(extension => extension.identifier.id.toLowerCase());
|
||||
|
||||
for (const extensionId of extensionsToQuery) {
|
||||
if (extensions.indexOf(extensionId) === -1) {
|
||||
invalidExtensions.push(extensionId);
|
||||
message += `${extensionId} (not found in marketplace)\n`;
|
||||
} else {
|
||||
validExtensions.push(extensionId);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
this.logService.warn('Error querying extensions gallery', e);
|
||||
}
|
||||
}
|
||||
|
||||
return { validRecommendations: validExtensions, invalidRecommendations: invalidExtensions, message };
|
||||
}
|
||||
|
||||
private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise<void> {
|
||||
if (event.added.length) {
|
||||
const oldWorkspaceRecommended = this._recommendations;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent | undefined): IExtensionsConfigContent | null {
|
||||
if (extensionsConfigContent) {
|
||||
return {
|
||||
recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),
|
||||
unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private hasToIgnoreWorkspaceRecommendationNotifications(): boolean {
|
||||
return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio
|
||||
if (visible) {
|
||||
const indicator: IStatusbarEntry = {
|
||||
text: '$(sync~spin) ' + nls.localize('profilingExtensionHost', "Profiling Extension Host"),
|
||||
ariaLabel: nls.localize('profilingExtensionHost', "Profiling Extension Host"),
|
||||
tooltip: nls.localize('selectAndStartDebug', "Click to stop profiling."),
|
||||
command: 'workbench.action.extensionHostProfilder.stop'
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactor
|
||||
// Global actions
|
||||
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions);
|
||||
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ShowRuntimeExtensionsAction, ShowRuntimeExtensionsAction.ID, ShowRuntimeExtensionsAction.LABEL), 'Show Running Extensions', localize('developer', "Developer"));
|
||||
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ShowRuntimeExtensionsAction), 'Show Running Extensions', localize('developer', "Developer"));
|
||||
|
||||
class ExtensionsContributions implements IWorkbenchContribution {
|
||||
|
||||
@@ -68,7 +68,7 @@ class ExtensionsContributions implements IWorkbenchContribution {
|
||||
@IWorkbenchEnvironmentService workbenchEnvironmentService: INativeWorkbenchEnvironmentService
|
||||
) {
|
||||
if (workbenchEnvironmentService.extensionsPath) {
|
||||
const openExtensionsFolderActionDescriptor = SyncActionDescriptor.create(OpenExtensionsFolderAction, OpenExtensionsFolderAction.ID, OpenExtensionsFolderAction.LABEL);
|
||||
const openExtensionsFolderActionDescriptor = SyncActionDescriptor.from(OpenExtensionsFolderAction);
|
||||
actionRegistry.registerWorkbenchAction(openExtensionsFolderActionDescriptor, 'Extensions: Open Extensions Folder', ExtensionsLabel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { append, $, addClass, toggleClass, Dimension, clearNode } from 'vs/base/
|
||||
import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { clipboard } from 'electron';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IElectronService } from 'vs/platform/electron/node/electron';
|
||||
@@ -48,7 +48,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
|
||||
export const IExtensionHostProfileService = createDecorator<IExtensionHostProfileService>('extensionHostProfileService');
|
||||
export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey<string>('profileSessionState', 'none');
|
||||
@@ -126,7 +126,8 @@ export class RuntimeExtensionsEditor extends BaseEditor {
|
||||
@IStorageService storageService: IStorageService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@IClipboardService private readonly _clipboardService: IClipboardService
|
||||
) {
|
||||
super(RuntimeExtensionsEditor.ID, telemetryService, themeService, storageService);
|
||||
|
||||
@@ -338,7 +339,7 @@ export class RuntimeExtensionsEditor extends BaseEditor {
|
||||
data.actionbar.push(this._instantiationService.createInstance(SlowExtensionAction, element.description, element.unresponsiveProfile), { icon: true, label: true });
|
||||
}
|
||||
if (isNonEmptyArray(element.status.runtimeErrors)) {
|
||||
data.actionbar.push(new ReportExtensionIssueAction(element, this._openerService), { icon: true, label: true });
|
||||
data.actionbar.push(new ReportExtensionIssueAction(element, this._openerService, this._clipboardService), { icon: true, label: true });
|
||||
}
|
||||
|
||||
let title: string;
|
||||
@@ -450,7 +451,7 @@ export class RuntimeExtensionsEditor extends BaseEditor {
|
||||
|
||||
const actions: IAction[] = [];
|
||||
|
||||
actions.push(new ReportExtensionIssueAction(e.element, this._openerService));
|
||||
actions.push(new ReportExtensionIssueAction(e.element, this._openerService, this._clipboardService));
|
||||
actions.push(new Separator());
|
||||
|
||||
if (e.element.marketplaceInfo) {
|
||||
@@ -509,25 +510,29 @@ export class ReportExtensionIssueAction extends Action {
|
||||
|
||||
private readonly _url: string;
|
||||
|
||||
constructor(extension: {
|
||||
description: IExtensionDescription;
|
||||
marketplaceInfo: IExtension;
|
||||
status?: IExtensionsStatus;
|
||||
unresponsiveProfile?: IExtensionHostProfile
|
||||
}, @IOpenerService private readonly openerService: IOpenerService) {
|
||||
constructor(
|
||||
extension: {
|
||||
description: IExtensionDescription;
|
||||
marketplaceInfo: IExtension;
|
||||
status?: IExtensionsStatus;
|
||||
unresponsiveProfile?: IExtensionHostProfile
|
||||
},
|
||||
@IOpenerService private readonly openerService: IOpenerService,
|
||||
@IClipboardService private readonly clipboardService: IClipboardService
|
||||
) {
|
||||
super(ReportExtensionIssueAction._id, ReportExtensionIssueAction._label, 'extension-action report-issue');
|
||||
this.enabled = extension.marketplaceInfo
|
||||
&& extension.marketplaceInfo.type === ExtensionType.User
|
||||
&& !!extension.description.repository && !!extension.description.repository.url;
|
||||
|
||||
this._url = ReportExtensionIssueAction._generateNewIssueUrl(extension);
|
||||
this._url = this._generateNewIssueUrl(extension);
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
this.openerService.open(URI.parse(this._url));
|
||||
}
|
||||
|
||||
private static _generateNewIssueUrl(extension: {
|
||||
private _generateNewIssueUrl(extension: {
|
||||
description: IExtensionDescription;
|
||||
marketplaceInfo: IExtension;
|
||||
status?: IExtensionsStatus;
|
||||
@@ -543,7 +548,7 @@ export class ReportExtensionIssueAction extends Action {
|
||||
let reason = 'Bug';
|
||||
let title = 'Extension issue';
|
||||
let message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:';
|
||||
clipboard.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```');
|
||||
this.clipboardService.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```');
|
||||
|
||||
const osVersion = `${os.type()} ${os.arch()} ${os.release()}`;
|
||||
const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&';
|
||||
@@ -692,7 +697,11 @@ export class SaveExtensionHostProfileAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
class RuntimeExtensionsEditorAccessibilityProvider implements IAccessibilityProvider<IRuntimeExtension> {
|
||||
class RuntimeExtensionsEditorAccessibilityProvider implements IListAccessibilityProvider<IRuntimeExtension> {
|
||||
getWidgetAriaLabel(): string {
|
||||
return nls.localize('runtimeExtensions', "Runtime Extensions");
|
||||
}
|
||||
|
||||
getAriaLabel(element: IRuntimeExtension): string | null {
|
||||
return element.description.name;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import { Emitter } from 'vs/base/common/event';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { TestLifecycleService, productService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices';
|
||||
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -48,7 +48,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
@@ -57,6 +57,7 @@ import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/exten
|
||||
import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationsService';
|
||||
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';
|
||||
|
||||
const mockExtensionGallery: IGalleryExtension[] = [
|
||||
aGalleryExtension('MockExtension1', {
|
||||
@@ -207,23 +208,23 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
instantiationService.stub(IURLService, URLService);
|
||||
instantiationService.stub(IWorkspaceTagsService, new NoOpWorkspaceTagsService());
|
||||
instantiationService.set(IProductService, {
|
||||
...productService,
|
||||
...{
|
||||
extensionTips: {
|
||||
'ms-dotnettools.csharp': '{**/*.cs,**/project.json,**/global.json,**/*.csproj,**/*.sln,**/appsettings.json}',
|
||||
'msjsdiag.debugger-for-chrome': '{**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.es6,**/*.mjs,**/*.cjs,**/.babelrc}',
|
||||
'lukehoban.Go': '**/*.go'
|
||||
instantiationService.stub(IStorageService, new TestStorageService());
|
||||
instantiationService.stub(ILogService, new NullLogService());
|
||||
instantiationService.stub(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryService());
|
||||
instantiationService.stub(IProductService, <Partial<IProductService>>{
|
||||
extensionTips: {
|
||||
'ms-dotnettools.csharp': '{**/*.cs,**/project.json,**/global.json,**/*.csproj,**/*.sln,**/appsettings.json}',
|
||||
'msjsdiag.debugger-for-chrome': '{**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.es6,**/*.mjs,**/*.cjs,**/.babelrc}',
|
||||
'lukehoban.Go': '**/*.go'
|
||||
},
|
||||
extensionImportantTips: {
|
||||
'ms-python.python': {
|
||||
'name': 'Python',
|
||||
'pattern': '{**/*.py}'
|
||||
},
|
||||
extensionImportantTips: {
|
||||
'ms-python.python': {
|
||||
'name': 'Python',
|
||||
'pattern': '{**/*.py}'
|
||||
},
|
||||
'ms-vscode.PowerShell': {
|
||||
'name': 'PowerShell',
|
||||
'pattern': '{**/*.ps,**/*.ps1}'
|
||||
}
|
||||
'ms-vscode.PowerShell': {
|
||||
'name': 'PowerShell',
|
||||
'pattern': '{**/*.ps,**/*.ps1}'
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -259,7 +260,6 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
instantiationService.stub(INotificationService, new TestNotificationService2());
|
||||
|
||||
testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: false, showRecommendationsOnlyOnDemand: false });
|
||||
instantiationService.stub(IStorageService, <Partial<IStorageService>>{ get: (a: string, b: StorageScope, c?: string) => c, getBoolean: (a: string, b: StorageScope, c: boolean) => c, store: () => { } });
|
||||
instantiationService.stub(IModelService, <IModelService>{
|
||||
getModels(): any { return []; },
|
||||
onModelAdded: onModelAddedEvent.event
|
||||
@@ -380,23 +380,14 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
});
|
||||
|
||||
test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if ignoreRecommendations is set for current workspace', () => {
|
||||
instantiationService.stub(IStorageService, <Partial<IStorageService>>{ get: (a: string, b: StorageScope, c?: string) => c, getBoolean: (a: string, b: StorageScope, c?: boolean) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c });
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
|
||||
return testNoPromptForValidRecommendations(mockTestData.validRecommendedExtensions);
|
||||
});
|
||||
|
||||
test('ExtensionRecommendationsService: No Recommendations of globally ignored recommendations', () => {
|
||||
const storageGetterStub = (a: string, _: StorageScope, c?: string) => {
|
||||
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]';
|
||||
const ignoredRecommendations = '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]'; // ignore a stored recommendation and a workspace recommendation.
|
||||
if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; }
|
||||
if (a === 'extensionsAssistant/ignored_recommendations') { return ignoredRecommendations; }
|
||||
return c;
|
||||
};
|
||||
|
||||
instantiationService.stub(IStorageService, <Partial<IStorageService>>{
|
||||
get: storageGetterStub,
|
||||
getBoolean: (a: string, _: StorageScope, c?: boolean) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c
|
||||
});
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]', StorageScope.GLOBAL);
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-dotnettools.csharp", "mockpublisher2.mockextension2"]', StorageScope.GLOBAL);
|
||||
|
||||
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
|
||||
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
|
||||
@@ -413,10 +404,8 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
test('ExtensionRecommendationsService: No Recommendations of workspace ignored recommendations', () => {
|
||||
const ignoredRecommendations = ['ms-dotnettools.csharp', 'mockpublisher2.mockextension2']; // ignore a stored recommendation and a workspace recommendation.
|
||||
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]';
|
||||
instantiationService.stub(IStorageService, <Partial<IStorageService>>{
|
||||
get: (a: string, b: StorageScope, c?: string) => a === 'extensionsAssistant/recommendations' ? storedRecommendations : c,
|
||||
getBoolean: (a: string, _: StorageScope, c?: boolean) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c
|
||||
});
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
|
||||
|
||||
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => {
|
||||
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
|
||||
@@ -432,19 +421,12 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
|
||||
test('ExtensionRecommendationsService: Able to retrieve collection of all ignored recommendations', () => {
|
||||
|
||||
const storageGetterStub = (a: string, _: StorageScope, c?: string) => {
|
||||
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]';
|
||||
const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation.
|
||||
if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; }
|
||||
if (a === 'extensionsAssistant/ignored_recommendations') { return globallyIgnoredRecommendations; }
|
||||
return c;
|
||||
};
|
||||
|
||||
const workspaceIgnoredRecommendations = ['ms-dotnettools.csharp']; // ignore a stored recommendation and a workspace recommendation.
|
||||
instantiationService.stub(IStorageService, <Partial<IStorageService>>{
|
||||
get: storageGetterStub,
|
||||
getBoolean: (a: string, _: StorageScope, c?: boolean) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c
|
||||
});
|
||||
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]';
|
||||
const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation.
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL);
|
||||
|
||||
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => {
|
||||
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
|
||||
@@ -459,19 +441,11 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
});
|
||||
|
||||
test('ExtensionRecommendationsService: Able to dynamically ignore/unignore global recommendations', () => {
|
||||
const storageGetterStub = (a: string, _: StorageScope, c?: string) => {
|
||||
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]';
|
||||
const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation.
|
||||
if (a === 'extensionsAssistant/recommendations') { return storedRecommendations; }
|
||||
if (a === 'extensionsAssistant/ignored_recommendations') { return globallyIgnoredRecommendations; }
|
||||
return c;
|
||||
};
|
||||
|
||||
instantiationService.stub(IStorageService, <Partial<IStorageService>>{
|
||||
get: storageGetterStub,
|
||||
store: () => { },
|
||||
getBoolean: (a: string, _: StorageScope, c?: boolean) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c
|
||||
});
|
||||
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python"]';
|
||||
const globallyIgnoredRecommendations = '["mockpublisher2.mockextension2"]'; // ignore a workspace recommendation.
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', globallyIgnoredRecommendations, StorageScope.GLOBAL);
|
||||
|
||||
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
|
||||
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
|
||||
@@ -502,16 +476,11 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
});
|
||||
|
||||
test('test global extensions are modified and recommendation change event is fired when an extension is ignored', async () => {
|
||||
const storageSetterTarget = sinon.spy();
|
||||
const changeHandlerTarget = sinon.spy();
|
||||
const ignoredExtensionId = 'Some.Extension';
|
||||
instantiationService.stub(IStorageService, <any>{ // {{SQL CARBON EDIT}} strict-null-checks?
|
||||
get: (a: string, b: StorageScope, c?: boolean) => a === 'extensionsAssistant/ignored_recommendations' ? '["ms-vscode.vscode"]' : c,
|
||||
getBoolean: (a: string, b: StorageScope, c: boolean) => c,
|
||||
store: (...args: any[]) => {
|
||||
storageSetterTarget(...args);
|
||||
}
|
||||
});
|
||||
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/workspaceRecommendationsIgnore', true, StorageScope.WORKSPACE);
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/ignored_recommendations', '["ms-vscode.vscode"]', StorageScope.GLOBAL);
|
||||
|
||||
await setUpFolderWorkspace('myFolder', []);
|
||||
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
|
||||
@@ -520,13 +489,12 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
await testObject.loadWorkspaceConfigPromise;
|
||||
|
||||
assert.ok(changeHandlerTarget.calledOnce);
|
||||
assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: 'Some.Extension', isRecommended: false }));
|
||||
assert.ok(storageSetterTarget.calledWithExactly('extensionsAssistant/ignored_recommendations', `["ms-vscode.vscode","${ignoredExtensionId.toLowerCase()}"]`, StorageScope.GLOBAL));
|
||||
assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: ignoredExtensionId.toLowerCase(), isRecommended: false }));
|
||||
});
|
||||
|
||||
test('ExtensionRecommendationsService: Get file based recommendations from storage (old format)', () => {
|
||||
const storedRecommendations = '["ms-dotnettools.csharp", "ms-python.python", "ms-vscode.vscode-typescript-tslint-plugin"]';
|
||||
instantiationService.stub(IStorageService, <Partial<IStorageService>>{ get: (a: string, b: StorageScope, c?: string) => a === 'extensionsAssistant/recommendations' ? storedRecommendations : c, getBoolean: (a: string, b: StorageScope, c: boolean) => c });
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
|
||||
|
||||
return setUpFolderWorkspace('myFolder', []).then(() => {
|
||||
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
|
||||
@@ -545,7 +513,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
|
||||
const now = Date.now();
|
||||
const tenDaysOld = 10 * milliSecondsInADay;
|
||||
const storedRecommendations = `{"ms-dotnettools.csharp": ${now}, "ms-python.python": ${now}, "ms-vscode.vscode-typescript-tslint-plugin": ${now}, "lukehoban.Go": ${tenDaysOld}}`;
|
||||
instantiationService.stub(IStorageService, <Partial<IStorageService>>{ get: (a: string, b: StorageScope, c?: string) => a === 'extensionsAssistant/recommendations' ? storedRecommendations : c, getBoolean: (a: string, b: StorageScope, c: boolean) => c });
|
||||
instantiationService.get(IStorageService).store('extensionsAssistant/recommendations', storedRecommendations, StorageScope.GLOBAL);
|
||||
|
||||
return setUpFolderWorkspace('myFolder', []).then(() => {
|
||||
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
|
||||
|
||||
@@ -11,12 +11,11 @@ import * as ExtensionsActions from 'vs/workbench/contrib/extensions/browser/exte
|
||||
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
|
||||
import {
|
||||
IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension,
|
||||
DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, InstallOperation
|
||||
DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, InstallOperation, IExtensionTipsService
|
||||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
|
||||
import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationsService';
|
||||
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';
|
||||
@@ -47,6 +46,12 @@ import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { ProgressService } from 'vs/workbench/services/progress/browser/progressService';
|
||||
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test';
|
||||
import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
|
||||
import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
|
||||
suite('ExtensionsActions Test', () => {
|
||||
|
||||
@@ -71,6 +76,8 @@ suite('ExtensionsActions Test', () => {
|
||||
instantiationService.stub(IWorkspaceContextService, new TestContextService());
|
||||
instantiationService.stub(IConfigurationService, new TestConfigurationService());
|
||||
instantiationService.stub(IProgressService, ProgressService);
|
||||
instantiationService.stub(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryService());
|
||||
instantiationService.stub(IProductService, {});
|
||||
|
||||
instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService);
|
||||
instantiationService.stub(ISharedProcessService, TestSharedProcessService);
|
||||
@@ -94,7 +101,10 @@ suite('ExtensionsActions Test', () => {
|
||||
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
|
||||
instantiationService.stub(ILabelService, { onDidChangeFormatters: new Emitter<IFormatterChangeEvent>().event });
|
||||
|
||||
instantiationService.set(IExtensionRecommendationsService, instantiationService.createInstance(ExtensionRecommendationsService));
|
||||
instantiationService.stub(ILifecycleService, new TestLifecycleService());
|
||||
instantiationService.stub(IExperimentService, instantiationService.createInstance(TestExperimentService));
|
||||
instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService));
|
||||
instantiationService.stub(IExtensionRecommendationsService, {});
|
||||
instantiationService.stub(IURLService, URLService);
|
||||
|
||||
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []);
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
|
||||
import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationsService';
|
||||
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';
|
||||
@@ -49,7 +48,6 @@ import { IMenuService } from 'vs/platform/actions/common/actions';
|
||||
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
|
||||
|
||||
|
||||
suite('ExtensionsListView Tests', () => {
|
||||
|
||||
let instantiationService: TestInstantiationService;
|
||||
@@ -69,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 fileBasedRecommendationA = aGalleryExtension('filebased-recommendation-A');
|
||||
const fileBasedRecommendationB = aGalleryExtension('filebased-recommendation-B');
|
||||
const otherRecommendationA = aGalleryExtension('other-recommendation-A');
|
||||
@@ -110,27 +109,40 @@ suite('ExtensionsListView Tests', () => {
|
||||
|
||||
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
|
||||
|
||||
instantiationService.stub(IExtensionRecommendationsService, ExtensionRecommendationsService);
|
||||
instantiationService.stub(IURLService, URLService);
|
||||
|
||||
instantiationService.stubPromise(IExtensionRecommendationsService, 'getWorkspaceRecommendations', [
|
||||
{ extensionId: workspaceRecommendationA.identifier.id },
|
||||
{ extensionId: workspaceRecommendationB.identifier.id }]);
|
||||
instantiationService.stub(IExtensionRecommendationsService, 'getFileBasedRecommendations', [
|
||||
{ extensionId: fileBasedRecommendationA.identifier.id },
|
||||
{ extensionId: fileBasedRecommendationB.identifier.id }]);
|
||||
instantiationService.stubPromise(IExtensionRecommendationsService, 'getOtherRecommendations', [
|
||||
{ extensionId: otherRecommendationA.identifier.id }
|
||||
]);
|
||||
const reasons: { [key: string]: any } = {};
|
||||
reasons[workspaceRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.Workspace };
|
||||
reasons[workspaceRecommendationB.identifier.id] = { reasonId: ExtensionRecommendationReason.Workspace };
|
||||
reasons[fileBasedRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.File };
|
||||
reasons[fileBasedRecommendationB.identifier.id] = { reasonId: ExtensionRecommendationReason.File };
|
||||
reasons[otherRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.Executable };
|
||||
|
||||
instantiationService.stub(IExtensionRecommendationsService, 'getAllRecommendationsWithReason', reasons);
|
||||
|
||||
reasons[configBasedRecommendationA.identifier.id] = { reasonId: ExtensionRecommendationReason.WorkspaceConfig };
|
||||
instantiationService.stub(IExtensionRecommendationsService, <Partial<IExtensionRecommendationsService>>{
|
||||
getWorkspaceRecommendations() {
|
||||
return Promise.resolve([
|
||||
{ extensionId: workspaceRecommendationA.identifier.id },
|
||||
{ extensionId: workspaceRecommendationB.identifier.id }]);
|
||||
},
|
||||
getConfigBasedRecommendations() {
|
||||
return Promise.resolve([
|
||||
{ extensionId: configBasedRecommendationA.identifier.id }
|
||||
]);
|
||||
},
|
||||
getFileBasedRecommendations() {
|
||||
return [
|
||||
{ extensionId: fileBasedRecommendationA.identifier.id },
|
||||
{ extensionId: fileBasedRecommendationB.identifier.id }
|
||||
];
|
||||
},
|
||||
getOtherRecommendations() {
|
||||
return Promise.resolve([
|
||||
{ extensionId: otherRecommendationA.identifier.id }
|
||||
]);
|
||||
},
|
||||
getAllRecommendationsWithReason() {
|
||||
return reasons;
|
||||
}
|
||||
});
|
||||
instantiationService.stub(IURLService, URLService);
|
||||
});
|
||||
|
||||
setup(async () => {
|
||||
@@ -140,7 +152,7 @@ suite('ExtensionsListView Tests', () => {
|
||||
instantiationService.stubPromise(IExperimentService, 'getExperimentsByType', []);
|
||||
|
||||
instantiationService.stub(IViewDescriptorService, {
|
||||
getViewLocation(): ViewContainerLocation {
|
||||
getViewLocationById(): ViewContainerLocation {
|
||||
return ViewContainerLocation.Sidebar;
|
||||
}
|
||||
});
|
||||
@@ -336,6 +348,7 @@ suite('ExtensionsListView Tests', () => {
|
||||
|
||||
test('Test @recommended query', () => {
|
||||
const allRecommendedExtensions = [
|
||||
configBasedRecommendationA,
|
||||
fileBasedRecommendationA,
|
||||
fileBasedRecommendationB,
|
||||
otherRecommendationA
|
||||
@@ -360,6 +373,7 @@ suite('ExtensionsListView Tests', () => {
|
||||
const allRecommendedExtensions = [
|
||||
workspaceRecommendationA,
|
||||
workspaceRecommendationB,
|
||||
configBasedRecommendationA,
|
||||
fileBasedRecommendationA,
|
||||
fileBasedRecommendationB,
|
||||
otherRecommendationA
|
||||
|
||||
@@ -12,12 +12,11 @@ import { IExtensionsWorkbenchService, ExtensionState, AutoCheckUpdatesConfigurat
|
||||
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
|
||||
import {
|
||||
IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension,
|
||||
DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, InstallOperation
|
||||
DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, InstallOperation, IExtensionTipsService
|
||||
} from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
|
||||
import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationsService';
|
||||
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';
|
||||
@@ -41,6 +40,13 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA
|
||||
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
|
||||
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
|
||||
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService';
|
||||
import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test';
|
||||
import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService';
|
||||
|
||||
suite('ExtensionsWorkbenchServiceTest', () => {
|
||||
|
||||
@@ -62,6 +68,8 @@ suite('ExtensionsWorkbenchServiceTest', () => {
|
||||
instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
instantiationService.stub(ILogService, NullLogService);
|
||||
instantiationService.stub(IProgressService, ProgressService);
|
||||
instantiationService.stub(IStorageKeysSyncRegistryService, new StorageKeysSyncRegistryService());
|
||||
instantiationService.stub(IProductService, {});
|
||||
|
||||
instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService);
|
||||
instantiationService.stub(IURLService, URLService);
|
||||
@@ -91,7 +99,10 @@ suite('ExtensionsWorkbenchServiceTest', () => {
|
||||
|
||||
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
|
||||
|
||||
instantiationService.set(IExtensionRecommendationsService, instantiationService.createInstance(ExtensionRecommendationsService));
|
||||
instantiationService.stub(ILifecycleService, new TestLifecycleService());
|
||||
instantiationService.stub(IExperimentService, instantiationService.createInstance(TestExperimentService));
|
||||
instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService));
|
||||
instantiationService.stub(IExtensionRecommendationsService, {});
|
||||
|
||||
instantiationService.stub(INotificationService, { prompt: () => null! });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user