Path: blob/main/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.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 { EXTENSION_IDENTIFIER_PATTERN } from '../../../../platform/extensionManagement/common/extensionManagement.js';6import { distinct, equals } from '../../../../base/common/arrays.js';7import { ExtensionRecommendations, ExtensionRecommendation } from './extensionRecommendations.js';8import { INotificationService } from '../../../../platform/notification/common/notification.js';9import { ExtensionRecommendationReason } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';10import { localize } from '../../../../nls.js';11import { Emitter } from '../../../../base/common/event.js';12import { IExtensionsConfigContent, IWorkspaceExtensionsConfigService } from '../../../services/extensionRecommendations/common/workspaceExtensionsConfig.js';13import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';14import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';15import { FileChangeType, IFileService } from '../../../../platform/files/common/files.js';16import { URI } from '../../../../base/common/uri.js';17import { RunOnceScheduler } from '../../../../base/common/async.js';18import { IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js';1920const WORKSPACE_EXTENSIONS_FOLDER = '.vscode/extensions';2122export class WorkspaceRecommendations extends ExtensionRecommendations {2324private _recommendations: ExtensionRecommendation[] = [];25get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }2627private _onDidChangeRecommendations = this._register(new Emitter<void>());28readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;2930private _ignoredRecommendations: string[] = [];31get ignoredRecommendations(): ReadonlyArray<string> { return this._ignoredRecommendations; }3233private workspaceExtensions: URI[] = [];34private readonly onDidChangeWorkspaceExtensionsScheduler: RunOnceScheduler;3536constructor(37@IWorkspaceExtensionsConfigService private readonly workspaceExtensionsConfigService: IWorkspaceExtensionsConfigService,38@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,39@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,40@IFileService private readonly fileService: IFileService,41@IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService,42@INotificationService private readonly notificationService: INotificationService,43) {44super();45this.onDidChangeWorkspaceExtensionsScheduler = this._register(new RunOnceScheduler(() => this.onDidChangeWorkspaceExtensionsFolders(), 1000));46}4748protected async doActivate(): Promise<void> {49this.workspaceExtensions = await this.fetchWorkspaceExtensions();50await this.fetch();5152this._register(this.workspaceExtensionsConfigService.onDidChangeExtensionsConfigs(() => this.onDidChangeExtensionsConfigs()));53for (const folder of this.contextService.getWorkspace().folders) {54this._register(this.fileService.watch(this.uriIdentityService.extUri.joinPath(folder.uri, WORKSPACE_EXTENSIONS_FOLDER)));55}5657this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.onDidChangeWorkspaceExtensionsScheduler.schedule()));5859this._register(this.fileService.onDidFilesChange(e => {60if (this.contextService.getWorkspace().folders.some(folder =>61e.affects(this.uriIdentityService.extUri.joinPath(folder.uri, WORKSPACE_EXTENSIONS_FOLDER), FileChangeType.ADDED, FileChangeType.DELETED))62) {63this.onDidChangeWorkspaceExtensionsScheduler.schedule();64}65}));66}6768private async onDidChangeWorkspaceExtensionsFolders(): Promise<void> {69const existing = this.workspaceExtensions;70this.workspaceExtensions = await this.fetchWorkspaceExtensions();71if (!equals(existing, this.workspaceExtensions, (a, b) => this.uriIdentityService.extUri.isEqual(a, b))) {72this.onDidChangeExtensionsConfigs();73}74}7576private async fetchWorkspaceExtensions(): Promise<URI[]> {77const workspaceExtensions: URI[] = [];78for (const workspaceFolder of this.contextService.getWorkspace().folders) {79const extensionsLocaiton = this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, WORKSPACE_EXTENSIONS_FOLDER);80try {81const stat = await this.fileService.resolve(extensionsLocaiton);82for (const extension of stat.children ?? []) {83if (!extension.isDirectory) {84continue;85}86workspaceExtensions.push(extension.resource);87}88} catch (error) {89// ignore90}91}92if (workspaceExtensions.length) {93const resourceExtensions = await this.workbenchExtensionManagementService.getExtensions(workspaceExtensions);94return resourceExtensions.map(extension => extension.location);95}96return [];97}9899/**100* Parse all extensions.json files, fetch workspace recommendations, filter out invalid and unwanted ones101*/102private async fetch(): Promise<void> {103104const extensionsConfigs = await this.workspaceExtensionsConfigService.getExtensionsConfigs();105106const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigs);107if (invalidRecommendations.length) {108this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`);109}110111this._recommendations = [];112this._ignoredRecommendations = [];113114for (const extensionsConfig of extensionsConfigs) {115if (extensionsConfig.unwantedRecommendations) {116for (const unwantedRecommendation of extensionsConfig.unwantedRecommendations) {117if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) {118this._ignoredRecommendations.push(unwantedRecommendation);119}120}121}122if (extensionsConfig.recommendations) {123for (const extensionId of extensionsConfig.recommendations) {124if (invalidRecommendations.indexOf(extensionId) === -1) {125this._recommendations.push({126extension: extensionId,127reason: {128reasonId: ExtensionRecommendationReason.Workspace,129reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")130}131});132}133}134}135}136137for (const extension of this.workspaceExtensions) {138this._recommendations.push({139extension,140reason: {141reasonId: ExtensionRecommendationReason.Workspace,142reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")143}144});145}146}147148private async validateExtensions(contents: IExtensionsConfigContent[]): Promise<{ validRecommendations: string[]; invalidRecommendations: string[]; message: string }> {149150const validExtensions: string[] = [];151const invalidExtensions: string[] = [];152let message = '';153154const allRecommendations = distinct(contents.flatMap(({ recommendations }) => recommendations || []));155const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN);156for (const extensionId of allRecommendations) {157if (regEx.test(extensionId)) {158validExtensions.push(extensionId);159} else {160invalidExtensions.push(extensionId);161message += `${extensionId} (bad format) Expected: <provider>.<name>\n`;162}163}164165return { validRecommendations: validExtensions, invalidRecommendations: invalidExtensions, message };166}167168private async onDidChangeExtensionsConfigs(): Promise<void> {169await this.fetch();170this._onDidChangeRecommendations.fire();171}172173}174175176177