Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';6import { IExtensionManagementService, IExtensionGalleryService, InstallOperation, InstallExtensionResult } from '../../../../platform/extensionManagement/common/extensionManagement.js';7import { IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';8import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';9import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';10import { shuffle } from '../../../../base/common/arrays.js';11import { Emitter, Event } from '../../../../base/common/event.js';12import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';13import { LifecyclePhase, ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';14import { ExeBasedRecommendations } from './exeBasedRecommendations.js';15import { WorkspaceRecommendations } from './workspaceRecommendations.js';16import { FileBasedRecommendations } from './fileBasedRecommendations.js';17import { KeymapRecommendations } from './keymapRecommendations.js';18import { LanguageRecommendations } from './languageRecommendations.js';19import { ExtensionRecommendation } from './extensionRecommendations.js';20import { ConfigBasedRecommendations } from './configBasedRecommendations.js';21import { IExtensionRecommendationNotificationService } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js';22import { CancelablePromise, timeout } from '../../../../base/common/async.js';23import { URI } from '../../../../base/common/uri.js';24import { WebRecommendations } from './webRecommendations.js';25import { IExtensionsWorkbenchService } from '../common/extensions.js';26import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';27import { RemoteRecommendations } from './remoteRecommendations.js';28import { IRemoteExtensionsScannerService } from '../../../../platform/remote/common/remoteExtensionsScanner.js';29import { IUserDataInitializationService } from '../../../services/userData/browser/userDataInit.js';30import { isString } from '../../../../base/common/types.js';3132export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService {3334declare readonly _serviceBrand: undefined;3536// Recommendations37private readonly fileBasedRecommendations: FileBasedRecommendations;38private readonly workspaceRecommendations: WorkspaceRecommendations;39private readonly configBasedRecommendations: ConfigBasedRecommendations;40private readonly exeBasedRecommendations: ExeBasedRecommendations;41private readonly keymapRecommendations: KeymapRecommendations;42private readonly webRecommendations: WebRecommendations;43private readonly languageRecommendations: LanguageRecommendations;44private readonly remoteRecommendations: RemoteRecommendations;4546public readonly activationPromise: Promise<void>;47private sessionSeed: number;4849private _onDidChangeRecommendations = this._register(new Emitter<void>());50readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;5152constructor(53@IInstantiationService instantiationService: IInstantiationService,54@ILifecycleService private readonly lifecycleService: ILifecycleService,55@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,56@ITelemetryService private readonly telemetryService: ITelemetryService,57@IEnvironmentService private readonly environmentService: IEnvironmentService,58@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,59@IExtensionIgnoredRecommendationsService private readonly extensionRecommendationsManagementService: IExtensionIgnoredRecommendationsService,60@IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,61@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,62@IRemoteExtensionsScannerService private readonly remoteExtensionsScannerService: IRemoteExtensionsScannerService,63@IUserDataInitializationService private readonly userDataInitializationService: IUserDataInitializationService,64) {65super();6667this.workspaceRecommendations = this._register(instantiationService.createInstance(WorkspaceRecommendations));68this.fileBasedRecommendations = this._register(instantiationService.createInstance(FileBasedRecommendations));69this.configBasedRecommendations = this._register(instantiationService.createInstance(ConfigBasedRecommendations));70this.exeBasedRecommendations = this._register(instantiationService.createInstance(ExeBasedRecommendations));71this.keymapRecommendations = this._register(instantiationService.createInstance(KeymapRecommendations));72this.webRecommendations = this._register(instantiationService.createInstance(WebRecommendations));73this.languageRecommendations = this._register(instantiationService.createInstance(LanguageRecommendations));74this.remoteRecommendations = this._register(instantiationService.createInstance(RemoteRecommendations));7576if (!this.isEnabled()) {77this.sessionSeed = 0;78this.activationPromise = Promise.resolve();79return;80}8182this.sessionSeed = +new Date();8384// Activation85this.activationPromise = this.activate();8687this._register(this.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e)));88}8990private async activate(): Promise<void> {91try {92await Promise.allSettled([93this.remoteExtensionsScannerService.whenExtensionsReady(),94this.userDataInitializationService.whenInitializationFinished(),95this.lifecycleService.when(LifecyclePhase.Restored)]);96} catch (error) { /* ignore */ }9798// activate all recommendations99await Promise.all([100this.workspaceRecommendations.activate(),101this.configBasedRecommendations.activate(),102this.fileBasedRecommendations.activate(),103this.keymapRecommendations.activate(),104this.languageRecommendations.activate(),105this.webRecommendations.activate(),106this.remoteRecommendations.activate()107]);108109this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire()));110111this.promptWorkspaceRecommendations();112}113114private isEnabled(): boolean {115return this.galleryService.isEnabled() && !this.environmentService.isExtensionDevelopment;116}117118private async activateProactiveRecommendations(): Promise<void> {119await Promise.all([this.exeBasedRecommendations.activate(), this.configBasedRecommendations.activate()]);120}121122getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string } } {123/* Activate proactive recommendations */124this.activateProactiveRecommendations();125126const output: { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string } } = Object.create(null);127128const allRecommendations = [129...this.configBasedRecommendations.recommendations,130...this.exeBasedRecommendations.recommendations,131...this.fileBasedRecommendations.recommendations,132...this.workspaceRecommendations.recommendations,133...this.keymapRecommendations.recommendations,134...this.languageRecommendations.recommendations,135...this.webRecommendations.recommendations,136];137138for (const { extension, reason } of allRecommendations) {139if (isString(extension) && this.isExtensionAllowedToBeRecommended(extension)) {140output[extension.toLowerCase()] = reason;141}142}143144return output;145}146147async getConfigBasedRecommendations(): Promise<{ important: string[]; others: string[] }> {148await this.configBasedRecommendations.activate();149return {150important: this.toExtensionIds(this.configBasedRecommendations.importantRecommendations),151others: this.toExtensionIds(this.configBasedRecommendations.otherRecommendations)152};153}154155async getOtherRecommendations(): Promise<string[]> {156await this.activationPromise;157await this.activateProactiveRecommendations();158159const recommendations = [160...this.configBasedRecommendations.otherRecommendations,161...this.exeBasedRecommendations.otherRecommendations,162...this.webRecommendations.recommendations163];164165const extensionIds = this.toExtensionIds(recommendations);166shuffle(extensionIds, this.sessionSeed);167return extensionIds;168}169170async getImportantRecommendations(): Promise<string[]> {171await this.activateProactiveRecommendations();172173const recommendations = [174...this.fileBasedRecommendations.importantRecommendations,175...this.configBasedRecommendations.importantRecommendations,176...this.exeBasedRecommendations.importantRecommendations,177];178179const extensionIds = this.toExtensionIds(recommendations);180shuffle(extensionIds, this.sessionSeed);181return extensionIds;182}183184getKeymapRecommendations(): string[] {185return this.toExtensionIds(this.keymapRecommendations.recommendations);186}187188getLanguageRecommendations(): string[] {189return this.toExtensionIds(this.languageRecommendations.recommendations);190}191192getRemoteRecommendations(): string[] {193return this.toExtensionIds(this.remoteRecommendations.recommendations);194}195196async getWorkspaceRecommendations(): Promise<Array<string | URI>> {197if (!this.isEnabled()) {198return [];199}200await this.workspaceRecommendations.activate();201const result: Array<string | URI> = [];202for (const { extension } of this.workspaceRecommendations.recommendations) {203if (isString(extension)) {204if (!result.includes(extension.toLowerCase()) && this.isExtensionAllowedToBeRecommended(extension)) {205result.push(extension.toLowerCase());206}207} else {208result.push(extension);209}210}211return result;212}213214async getExeBasedRecommendations(exe?: string): Promise<{ important: string[]; others: string[] }> {215await this.exeBasedRecommendations.activate();216const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe)217: { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations };218return { important: this.toExtensionIds(important), others: this.toExtensionIds(others) };219}220221getFileBasedRecommendations(): string[] {222return this.toExtensionIds(this.fileBasedRecommendations.recommendations);223}224225private onDidInstallExtensions(results: readonly InstallExtensionResult[]): void {226for (const e of results) {227if (e.source && !URI.isUri(e.source) && e.operation === InstallOperation.Install) {228const extRecommendations = this.getAllRecommendationsWithReason() || {};229const recommendationReason = extRecommendations[e.source.identifier.id.toLowerCase()];230if (recommendationReason) {231/* __GDPR__232"extensionGallery:install:recommendations" : {233"owner": "sandy081",234"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },235"${include}": [236"${GalleryExtensionTelemetryData}"237]238}239*/240this.telemetryService.publicLog('extensionGallery:install:recommendations', { ...e.source.telemetryData, recommendationReason: recommendationReason.reasonId });241}242}243}244}245246private toExtensionIds(recommendations: ReadonlyArray<ExtensionRecommendation>): string[] {247const extensionIds: string[] = [];248for (const { extension } of recommendations) {249if (isString(extension) && this.isExtensionAllowedToBeRecommended(extension) && !extensionIds.includes(extension.toLowerCase())) {250extensionIds.push(extension.toLowerCase());251}252}253return extensionIds;254}255256private isExtensionAllowedToBeRecommended(extensionId: string): boolean {257return !this.extensionRecommendationsManagementService.ignoredRecommendations.includes(extensionId.toLowerCase());258}259260private async promptWorkspaceRecommendations(): Promise<void> {261const installed = await this.extensionsWorkbenchService.queryLocal();262const allowedRecommendations = [263...this.workspaceRecommendations.recommendations,264...this.configBasedRecommendations.importantRecommendations.filter(265recommendation => !recommendation.whenNotInstalled || recommendation.whenNotInstalled.every(id => installed.every(local => !areSameExtensions(local.identifier, { id }))))266]267.map(({ extension }) => extension)268.filter(extension => !isString(extension) || this.isExtensionAllowedToBeRecommended(extension));269270if (allowedRecommendations.length) {271await this._registerP(timeout(5000));272await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations);273}274}275276private _registerP<T>(o: CancelablePromise<T>): CancelablePromise<T> {277this._register(toDisposable(() => o.cancel()));278return o;279}280}281282283