提交 44fc7336 编写于 作者: S Sandeep Somavarapu

- introduce medium importance exe tips

- recommend them each in every 7 days
- do not recommend exe tip if a window has more than 2 recommendations
上级 7882fc9e
......@@ -14,8 +14,9 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { ILogService } from 'vs/platform/log/common/log';
import { joinPath } from 'vs/base/common/resources';
import { getDomainsOfRemotes } from 'vs/platform/extensionManagement/common/configRemotes';
import { Disposable } from 'vs/base/common/lifecycle';
export class ExtensionTipsService implements IExtensionTipsService {
export class ExtensionTipsService extends Disposable implements IExtensionTipsService {
_serviceBrand: any;
......@@ -27,6 +28,7 @@ export class ExtensionTipsService implements IExtensionTipsService {
@IRequestService private readonly requestService: IRequestService,
@ILogService private readonly logService: ILogService,
) {
super();
if (this.productService.configBasedExtensionTips) {
forEach(this.productService.configBasedExtensionTips, ({ value }) => this.allConfigBasedTips.set(value.configPath, value));
}
......
......@@ -18,9 +18,11 @@ import { ILogService } from 'vs/platform/log/common/log';
import { ExtensionTipsService as BaseExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionTipsService';
import { timeout } from 'vs/base/common/async';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionRecommendationNotificationService, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { localize } from 'vs/nls';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { Event } from 'vs/base/common/event';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
type ExeExtensionRecommendationsClassification = {
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
......@@ -34,20 +36,26 @@ type IExeBasedExtensionTips = {
};
const promptedExecutableTipsStorageKey = 'extensionTips/promptedExecutableTips';
const lastPromptedMediumImpExeTimeStorageKey = 'extensionTips/lastPromptedMediumImpExeTime';
export class ExtensionTipsService extends BaseExtensionTipsService {
_serviceBrand: any;
private readonly allImportantExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private readonly highImportanceExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private readonly mediumImportanceExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private readonly allOtherExecutableTips: Map<string, IExeBasedExtensionTips> = new Map<string, IExeBasedExtensionTips>();
private highImportanceTipsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
private mediumImportanceTipsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
constructor(
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,
@INativeHostService private readonly nativeHostMainService: INativeHostService,
@IFileService fileService: IFileService,
@IProductService productService: IProductService,
@IRequestService requestService: IRequestService,
......@@ -55,21 +63,29 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
) {
super(fileService, productService, requestService, logService);
if (productService.exeBasedExtensionTips) {
forEach(productService.exeBasedExtensionTips, ({ key, value }) => {
const importantRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
forEach(productService.exeBasedExtensionTips, ({ key, value: exeBasedExtensionTip }) => {
const highImportanceRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
const mediumImportanceRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
const otherRecommendations: { extensionId: string, extensionName: string, isExtensionPack: boolean }[] = [];
forEach(value.recommendations, ({ key: extensionId, value }) => {
forEach(exeBasedExtensionTip.recommendations, ({ key: extensionId, value }) => {
if (value.important) {
importantRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
if (exeBasedExtensionTip.important) {
highImportanceRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
} else {
mediumImportanceRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
}
} else {
otherRecommendations.push({ extensionId, extensionName: value.name, isExtensionPack: !!value.isExtensionPack });
}
});
if (importantRecommendations.length) {
this.allImportantExecutableTips.set(key, { exeFriendlyName: value.friendlyName, windowsPath: value.windowsPath, recommendations: importantRecommendations });
if (highImportanceRecommendations.length) {
this.highImportanceExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: highImportanceRecommendations });
}
if (mediumImportanceRecommendations.length) {
this.mediumImportanceExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: mediumImportanceRecommendations });
}
if (otherRecommendations.length) {
this.allOtherExecutableTips.set(key, { exeFriendlyName: value.friendlyName, windowsPath: value.windowsPath, recommendations: otherRecommendations });
this.allOtherExecutableTips.set(key, { exeFriendlyName: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: otherRecommendations });
}
});
}
......@@ -78,23 +94,36 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
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.promptImportantExeBasedRecommendations());
timeout(3000).then(async () => {
await this.collectTips();
this.promptHighImportanceExeBasedTip();
this.promptMediumImportanceExeBasedTip();
});
}
getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allImportantExecutableTips);
async getImportantExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
const highImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.highImportanceExecutableTips);
const mediumImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.mediumImportanceExecutableTips);
return [...highImportanceExeTips, ...mediumImportanceExeTips];
}
getOtherExecutableBasedTips(): Promise<IExecutableBasedExtensionTip[]> {
return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips);
}
private async promptImportantExeBasedRecommendations(): Promise<void> {
private async collectTips(): Promise<void> {
const highImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.highImportanceExecutableTips);
const mediumImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.mediumImportanceExecutableTips);
const local = await this.extensionManagementService.getInstalled();
this.highImportanceTipsByExe = this.groupImportantTipsByExe(highImportanceExeTips, local);
this.mediumImportanceTipsByExe = this.groupImportantTipsByExe(mediumImportanceExeTips, local);
}
private groupImportantTipsByExe(importantExeBasedTips: IExecutableBasedExtensionTip[], local: ILocalExtension[]): Map<string, IExecutableBasedExtensionTip[]> {
const importantExeBasedRecommendations = new Map<string, IExecutableBasedExtensionTip>();
const importantExeBasedTips = await this.getImportantExecutableBasedTips();
importantExeBasedTips.forEach(tip => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip));
const local = await this.extensionManagementService.getInstalled();
const { installed, uninstalled: recommendations } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], local);
/* Log installed and uninstalled exe based recommendations */
......@@ -111,39 +140,108 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
}
}
const recommendationsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
const promptedExecutableTips = this.getPromptedExecutableTips();
const tipsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip && (!promptedExecutableTips[tip.exeName] || !promptedExecutableTips[tip.exeName].includes(tip.extensionId))) {
let tips = recommendationsByExe.get(tip.exeName);
let tips = tipsByExe.get(tip.exeName);
if (!tips) {
tips = [];
recommendationsByExe.set(tip.exeName, tips);
tipsByExe.set(tip.exeName, tips);
}
tips.push(tip);
}
}
for (const [, tips] of recommendationsByExe) {
const extensionIds = tips.map(({ extensionId }) => extensionId.toLowerCase());
const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName);
this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`, RecommendationSource.EXE)
.then(result => {
if (result) {
this.addToRecommendedExecutables(tips[0].exeName, extensionIds);
}
});
return tipsByExe;
}
/**
* High importance tips are prompted once per restart session
*/
private promptHighImportanceExeBasedTip(): void {
if (this.highImportanceTipsByExe.size === 0) {
return;
}
const [exeName, tips] = [...this.highImportanceTipsByExe.entries()][0];
this.promptExeRecommendations(tips)
.then(result => {
switch (result) {
case RecommendationsNotificationResult.Accepted:
this.addToRecommendedExecutables(tips[0].exeName, tips);
break;
case RecommendationsNotificationResult.Ignored:
this.highImportanceTipsByExe.delete(exeName);
break;
case RecommendationsNotificationResult.TooMany:
// It is skipped. So try to recommend when new window is opened
this._register(Event.once(this.nativeHostMainService.onWindowOpen)(() => this.promptHighImportanceExeBasedTip()));
break;
}
});
}
/**
* Medium importance tips are prompted once per 7 days
*/
private promptMediumImportanceExeBasedTip(): void {
if (this.mediumImportanceExecutableTips.size === 0) {
return;
}
const lastPromptedMediumExeTime = this.getLastPromptedMediumExeTime();
if ((Date.now() - lastPromptedMediumExeTime) < 7 * 24 * 60 * 60 * 1000) { // 7 Days
// Not 7 days yet. So try to recommend when new window is opened
this._register(Event.once(this.nativeHostMainService.onWindowOpen)(() => this.promptMediumImportanceExeBasedTip()));
return;
}
this.updateLastPromptedMediumExeTime();
const [exeName, tips] = [...this.mediumImportanceTipsByExe.entries()][0];
this.promptExeRecommendations(tips)
.then(result => {
switch (result) {
case RecommendationsNotificationResult.Accepted:
this.mediumImportanceTipsByExe.delete(exeName);
this.addToRecommendedExecutables(tips[0].exeName, tips);
break;
case RecommendationsNotificationResult.Ignored:
this.mediumImportanceExecutableTips.delete(exeName);
break;
}
// Schedule next prompt when new window is opened
this._register(Event.once(this.nativeHostMainService.onWindowOpen)(() => this.promptMediumImportanceExeBasedTip()));
});
}
private promptExeRecommendations(tips: IExecutableBasedExtensionTip[]): Promise<RecommendationsNotificationResult> {
const extensionIds = tips.map(({ extensionId }) => extensionId.toLowerCase());
const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName);
return this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`, RecommendationSource.EXE);
}
private getLastPromptedMediumExeTime(): number {
let value = this.storageService.getNumber(lastPromptedMediumImpExeTimeStorageKey, StorageScope.GLOBAL);
if (!value) {
value = Date.now();
this.storageService.store(lastPromptedMediumImpExeTimeStorageKey, value, StorageScope.GLOBAL);
}
return value;
}
private updateLastPromptedMediumExeTime(): void {
this.storageService.store(lastPromptedMediumImpExeTimeStorageKey, Date.now(), StorageScope.GLOBAL);
}
private getPromptedExecutableTips(): IStringDictionary<string[]> {
return JSON.parse(this.storageService.get(promptedExecutableTipsStorageKey, StorageScope.GLOBAL, '{}'));
}
private addToRecommendedExecutables(exeName: string, extensions: string[]) {
private addToRecommendedExecutables(exeName: string, tips: IExecutableBasedExtensionTip[]) {
const promptedExecutableTips = this.getPromptedExecutableTips();
promptedExecutableTips[exeName] = extensions;
promptedExecutableTips[exeName] = tips.map(({ extensionId }) => extensionId.toLowerCase());
this.storageService.store(promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), StorageScope.GLOBAL);
}
......
......@@ -11,6 +11,13 @@ export const enum RecommendationSource {
EXE = 3
}
export const enum RecommendationsNotificationResult {
Ignored = 'ignored',
Cancelled = 'cancelled',
TooMany = 'toomany',
Accepted = 'reacted',
}
export const IExtensionRecommendationNotificationService = createDecorator<IExtensionRecommendationNotificationService>('IExtensionRecommendationNotificationService');
export interface IExtensionRecommendationNotificationService {
......@@ -19,7 +26,7 @@ export interface IExtensionRecommendationNotificationService {
readonly ignoredRecommendations: string[];
hasToIgnoreRecommendationNotifications(): boolean;
promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, source: RecommendationSource): Promise<boolean>;
promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, source: RecommendationSource): Promise<RecommendationsNotificationResult>;
promptWorkspaceRecommendations(recommendations: string[]): Promise<void>;
}
......@@ -5,7 +5,7 @@
import { Event } from 'vs/base/common/event';
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionRecommendationNotificationService, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
export class ExtensionRecommendationNotificationServiceChannelClient implements IExtensionRecommendationNotificationService {
......@@ -15,7 +15,7 @@ export class ExtensionRecommendationNotificationServiceChannelClient implements
get ignoredRecommendations(): string[] { throw new Error('not supported'); }
promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, priority: RecommendationSource): Promise<boolean> {
promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, priority: RecommendationSource): Promise<RecommendationsNotificationResult> {
return this.channel.call('promptImportantExtensionsInstallNotification', [extensionIds, message, searchValue, priority]);
}
......
......@@ -147,6 +147,7 @@ export interface IConfigBasedExtensionTip {
export interface IExeBasedExtensionTip {
friendlyName: string;
windowsPath?: string;
important?: boolean;
recommendations: IStringDictionary<{ name: string, important?: boolean, isExtensionPack?: boolean }>;
}
......
......@@ -12,7 +12,7 @@ import { localize } from 'vs/nls';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionRecommendationNotificationService, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation';
import { INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, Severity } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
......@@ -66,6 +66,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec
return distinct([...(<string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendationStorageKey, StorageScope.GLOBAL, '[]')))].map(i => i.toLowerCase()));
}
private notificationsCount: number = 0;
private hideVisibleNotificationPromise: CancelablePromise<void> | undefined;
private visibleNotification: VisibleRecommendationNotification | undefined;
private pendingNotificaitons: PendingRecommendationNotification[] = [];
......@@ -92,32 +93,37 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec
return config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand;
}
async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, priority: RecommendationSource): Promise<boolean> {
async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string, source: RecommendationSource): Promise<RecommendationsNotificationResult> {
if (this.hasToIgnoreRecommendationNotifications()) {
return false;
return RecommendationsNotificationResult.Ignored;
}
const ignoredRecommendations = [...this.extensionIgnoredRecommendationsService.ignoredRecommendations, ...this.ignoredRecommendations];
extensionIds = extensionIds.filter(id => !ignoredRecommendations.includes(id));
if (!extensionIds.length) {
return false;
return RecommendationsNotificationResult.Ignored;
}
const extensions = await this.getInstallableExtensions(extensionIds);
if (!extensions.length) {
return false;
return RecommendationsNotificationResult.Ignored;
}
// Do not show exe recommendation if the window has shown two recommendations already
if (this.notificationsCount >= 2 && source === RecommendationSource.EXE) {
return RecommendationsNotificationResult.TooMany;
}
if (this.tasExperimentService && extensionIds.indexOf('ms-vscode-remote.remote-wsl') !== -1) {
await this.tasExperimentService.getTreatment<boolean>('wslpopupaa');
}
return new Promise<boolean>(async (c, e) => {
let cancelled: boolean = false;
return new Promise<RecommendationsNotificationResult>(async (c, e) => {
let result = RecommendationsNotificationResult.Accepted;
const handle = await this.showNotification({
severity: Severity.Info,
message,
priority: priority,
priority: source,
choices: [{
label: localize('install', "Install"),
run: async () => {
......@@ -161,7 +167,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec
options: {
sticky: true,
onCancel: () => {
cancelled = true;
result = RecommendationsNotificationResult.Cancelled;
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id });
}
......@@ -171,7 +177,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec
);
const disposable = handle.onDidClose(() => {
disposable.dispose();
c(!cancelled);
c(result);
});
});
}
......@@ -250,6 +256,7 @@ export class ExtensionRecommendationNotificationService implements IExtensionRec
* => Otherwise wait until the current notification is hidden.
*/
private async showNotification(recommendationNotification: RecommendationNotification): Promise<INotificationHandle> {
this.notificationsCount++;
if (this.visibleNotification) {
return new Promise<INotificationHandle>((onDidShow, e) => {
this.pendingNotificaitons.push({ ...recommendationNotification, onDidShow });
......
......@@ -25,7 +25,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IModelService } from 'vs/editor/common/services/modelService';
import { setImmediate } from 'vs/base/common/platform';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IExtensionRecommendationNotificationService, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
type FileExtensionSuggestionClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
......@@ -255,7 +255,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
this.extensionRecommendationNotificationService.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`, RecommendationSource.FILE)
.then(result => {
if (result) {
if (result === RecommendationsNotificationResult.Accepted) {
this.addToPromptedRecommendations(language, [extensionId]);
}
});
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册