@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
437 lines (436 loc) • 16.6 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 '@sussudio/base/common/arrays.mjs';
import { disposableTimeout, timeout } from '@sussudio/base/common/async.mjs';
import { Event } from '@sussudio/base/common/event.mjs';
import { join } from '@sussudio/base/common/path.mjs';
import { isWindows } from '@sussudio/base/common/platform.mjs';
import { env } from '@sussudio/base/common/process.mjs';
import { URI } from '@sussudio/base/common/uri.mjs';
import { localize } from 'vscode-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 };