sussudio
Version:
An unofficial VS Code Internal API
316 lines (315 loc) • 18.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import { isNonEmptyArray } from "../../../base/common/arrays.mjs";
import { disposableTimeout, timeout } from "../../../base/common/async.mjs";
import { Event } from "../../../base/common/event.mjs";
import { join } from "../../../base/common/path.mjs";
import { isWindows } from "../../../base/common/platform.mjs";
import { env } from "../../../base/common/process.mjs";
import { URI } from "../../../base/common/uri.mjs";
import { localize } from "../../../nls.mjs";
import { INativeEnvironmentService } from "../../environment/common/environment.mjs";
import { IExtensionManagementService } from "../common/extensionManagement.mjs";
import { areSameExtensions } from "../common/extensionManagementUtil.mjs";
import { ExtensionTipsService as BaseExtensionTipsService } from "../common/extensionTipsService.mjs";
import { IExtensionRecommendationNotificationService } from "../../extensionRecommendations/common/extensionRecommendations.mjs";
import { IFileService } from "../../files/common/files.mjs";
import { INativeHostService } from "../../native/electron-sandbox/native.mjs";
import { IProductService } from "../../product/common/productService.mjs";
import { IStorageService } from "../../storage/common/storage.mjs";
import { ITelemetryService } from "../../telemetry/common/telemetry.mjs";
const promptedExecutableTipsStorageKey = 'extensionTips/promptedExecutableTips';
const lastPromptedMediumImpExeTimeStorageKey = 'extensionTips/lastPromptedMediumImpExeTime';
let ExtensionTipsService = class ExtensionTipsService extends BaseExtensionTipsService {
environmentService;
telemetryService;
extensionManagementService;
storageService;
nativeHostService;
extensionRecommendationNotificationService;
highImportanceExecutableTips = new Map();
mediumImportanceExecutableTips = new Map();
allOtherExecutableTips = new Map();
highImportanceTipsByExe = new Map();
mediumImportanceTipsByExe = new Map();
constructor(environmentService, telemetryService, extensionManagementService, storageService, nativeHostService, extensionRecommendationNotificationService, fileService, productService) {
super(fileService, productService);
this.environmentService = environmentService;
this.telemetryService = telemetryService;
this.extensionManagementService = extensionManagementService;
this.storageService = storageService;
this.nativeHostService = nativeHostService;
this.extensionRecommendationNotificationService = extensionRecommendationNotificationService;
if (productService.exeBasedExtensionTips) {
Object.entries(productService.exeBasedExtensionTips).forEach(([key, exeBasedExtensionTip]) => {
const highImportanceRecommendations = [];
const mediumImportanceRecommendations = [];
const otherRecommendations = [];
Object.entries(exeBasedExtensionTip.recommendations).forEach(([extensionId, value]) => {
if (value.important) {
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 (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: exeBasedExtensionTip.friendlyName, windowsPath: exeBasedExtensionTip.windowsPath, recommendations: otherRecommendations });
}
});
}
/*
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(async () => {
await this.collectTips();
this.promptHighImportanceExeBasedTip();
this.promptMediumImportanceExeBasedTip();
});
}
async getImportantExecutableBasedTips() {
const highImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.highImportanceExecutableTips);
const mediumImportanceExeTips = await this.getValidExecutableBasedExtensionTips(this.mediumImportanceExecutableTips);
return [...highImportanceExeTips, ...mediumImportanceExeTips];
}
getOtherExecutableBasedTips() {
return this.getValidExecutableBasedExtensionTips(this.allOtherExecutableTips);
}
async collectTips() {
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);
}
groupImportantTipsByExe(importantExeBasedTips, local) {
const importantExeBasedRecommendations = new Map();
importantExeBasedTips.forEach(tip => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip));
const { installed, uninstalled: recommendations } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], local);
/* Log installed and uninstalled exe based recommendations */
for (const extensionId of installed) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: tip.exeName });
}
}
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2('exeExtensionRecommendations:notInstalled', { extensionId, exeName: tip.exeName });
}
}
const promptedExecutableTips = this.getPromptedExecutableTips();
const tipsByExe = new Map();
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip && (!promptedExecutableTips[tip.exeName] || !promptedExecutableTips[tip.exeName].includes(tip.extensionId))) {
let tips = tipsByExe.get(tip.exeName);
if (!tips) {
tips = [];
tipsByExe.set(tip.exeName, tips);
}
tips.push(tip);
}
}
return tipsByExe;
}
/**
* High importance tips are prompted once per restart session
*/
promptHighImportanceExeBasedTip() {
if (this.highImportanceTipsByExe.size === 0) {
return;
}
const [exeName, tips] = [...this.highImportanceTipsByExe.entries()][0];
this.promptExeRecommendations(tips)
.then(result => {
switch (result) {
case "reacted" /* RecommendationsNotificationResult.Accepted */:
this.addToRecommendedExecutables(tips[0].exeName, tips);
break;
case "ignored" /* RecommendationsNotificationResult.Ignored */:
this.highImportanceTipsByExe.delete(exeName);
break;
case "incompatibleWindow" /* RecommendationsNotificationResult.IncompatibleWindow */: {
// Recommended in incompatible window. Schedule the prompt after active window change
const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)));
this._register(onActiveWindowChange(() => this.promptHighImportanceExeBasedTip()));
break;
}
case "toomany" /* RecommendationsNotificationResult.TooMany */: {
// Too many notifications. Schedule the prompt after one hour
const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptHighImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */));
break;
}
}
});
}
/**
* Medium importance tips are prompted once per 7 days
*/
promptMediumImportanceExeBasedTip() {
if (this.mediumImportanceTipsByExe.size === 0) {
return;
}
const lastPromptedMediumExeTime = this.getLastPromptedMediumExeTime();
const timeSinceLastPrompt = Date.now() - lastPromptedMediumExeTime;
const promptInterval = 7 * 24 * 60 * 60 * 1000; // 7 Days
if (timeSinceLastPrompt < promptInterval) {
// Wait until interval and prompt
const disposable = this._register(disposableTimeout(() => { disposable.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval - timeSinceLastPrompt));
return;
}
const [exeName, tips] = [...this.mediumImportanceTipsByExe.entries()][0];
this.promptExeRecommendations(tips)
.then(result => {
switch (result) {
case "reacted" /* RecommendationsNotificationResult.Accepted */: {
// Accepted: Update the last prompted time and caches.
this.updateLastPromptedMediumExeTime(Date.now());
this.mediumImportanceTipsByExe.delete(exeName);
this.addToRecommendedExecutables(tips[0].exeName, tips);
// Schedule the next recommendation for next internval
const disposable1 = this._register(disposableTimeout(() => { disposable1.dispose(); this.promptMediumImportanceExeBasedTip(); }, promptInterval));
break;
}
case "ignored" /* RecommendationsNotificationResult.Ignored */:
// Ignored: Remove from the cache and prompt next recommendation
this.mediumImportanceTipsByExe.delete(exeName);
this.promptMediumImportanceExeBasedTip();
break;
case "incompatibleWindow" /* RecommendationsNotificationResult.IncompatibleWindow */: {
// Recommended in incompatible window. Schedule the prompt after active window change
const onActiveWindowChange = Event.once(Event.latch(Event.any(this.nativeHostService.onDidOpenWindow, this.nativeHostService.onDidFocusWindow)));
this._register(onActiveWindowChange(() => this.promptMediumImportanceExeBasedTip()));
break;
}
case "toomany" /* RecommendationsNotificationResult.TooMany */: {
// Too many notifications. Schedule the prompt after one hour
const disposable2 = this._register(disposableTimeout(() => { disposable2.dispose(); this.promptMediumImportanceExeBasedTip(); }, 60 * 60 * 1000 /* 1 hour */));
break;
}
}
});
}
async promptExeRecommendations(tips) {
const installed = await this.extensionManagementService.getInstalled(1 /* ExtensionType.User */);
const extensionIds = tips
.filter(tip => !tip.whenNotInstalled || tip.whenNotInstalled.every(id => installed.every(local => !areSameExtensions(local.identifier, { id }))))
.map(({ extensionId }) => extensionId.toLowerCase());
const message = localize({ key: 'exeRecommended', comment: ['Placeholder string is the name of the software that is installed.'] }, "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, `:"${tips[0].exeName}"`, 3 /* RecommendationSource.EXE */);
}
getLastPromptedMediumExeTime() {
let value = this.storageService.getNumber(lastPromptedMediumImpExeTimeStorageKey, -1 /* StorageScope.APPLICATION */);
if (!value) {
value = Date.now();
this.updateLastPromptedMediumExeTime(value);
}
return value;
}
updateLastPromptedMediumExeTime(value) {
this.storageService.store(lastPromptedMediumImpExeTimeStorageKey, value, -1 /* StorageScope.APPLICATION */, 1 /* StorageTarget.MACHINE */);
}
getPromptedExecutableTips() {
return JSON.parse(this.storageService.get(promptedExecutableTipsStorageKey, -1 /* StorageScope.APPLICATION */, '{}'));
}
addToRecommendedExecutables(exeName, tips) {
const promptedExecutableTips = this.getPromptedExecutableTips();
promptedExecutableTips[exeName] = tips.map(({ extensionId }) => extensionId.toLowerCase());
this.storageService.store(promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), -1 /* StorageScope.APPLICATION */, 0 /* StorageTarget.USER */);
}
groupByInstalled(recommendationsToSuggest, local) {
const installed = [], uninstalled = [];
const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set());
recommendationsToSuggest.forEach(id => {
if (installedExtensionsIds.has(id.toLowerCase())) {
installed.push(id);
}
else {
uninstalled.push(id);
}
});
return { installed, uninstalled };
}
async getValidExecutableBasedExtensionTips(executableTips) {
const result = [];
const checkedExecutables = new Map();
for (const exeName of executableTips.keys()) {
const extensionTip = executableTips.get(exeName);
if (!extensionTip || !isNonEmptyArray(extensionTip.recommendations)) {
continue;
}
const exePaths = [];
if (isWindows) {
if (extensionTip.windowsPath) {
exePaths.push(extensionTip.windowsPath.replace('%USERPROFILE%', () => env['USERPROFILE'])
.replace('%ProgramFiles(x86)%', () => env['ProgramFiles(x86)'])
.replace('%ProgramFiles%', () => env['ProgramFiles'])
.replace('%APPDATA%', () => env['APPDATA'])
.replace('%WINDIR%', () => env['WINDIR']));
}
}
else {
exePaths.push(join('/usr/local/bin', exeName));
exePaths.push(join('/usr/bin', exeName));
exePaths.push(join(this.environmentService.userHome.fsPath, exeName));
}
for (const exePath of exePaths) {
let exists = checkedExecutables.get(exePath);
if (exists === undefined) {
exists = await this.fileService.exists(URI.file(exePath));
checkedExecutables.set(exePath, exists);
}
if (exists) {
for (const { extensionId, extensionName, isExtensionPack, whenNotInstalled } of extensionTip.recommendations) {
result.push({
extensionId,
extensionName,
isExtensionPack,
exeName,
exeFriendlyName: extensionTip.exeFriendlyName,
windowsPath: extensionTip.windowsPath,
whenNotInstalled: whenNotInstalled
});
}
}
}
}
return result;
}
};
ExtensionTipsService = __decorate([
__param(0, INativeEnvironmentService),
__param(1, ITelemetryService),
__param(2, IExtensionManagementService),
__param(3, IStorageService),
__param(4, INativeHostService),
__param(5, IExtensionRecommendationNotificationService),
__param(6, IFileService),
__param(7, IProductService)
], ExtensionTipsService);
export { ExtensionTipsService };