Path: blob/main/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.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 { distinct } from '../../../../base/common/arrays.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { JSONPath, parse } from '../../../../base/common/json.js';8import { Disposable } from '../../../../base/common/lifecycle.js';9import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';10import { FileKind, IFileService } from '../../../../platform/files/common/files.js';11import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';12import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';13import { isWorkspace, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';14import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';15import { IModelService } from '../../../../editor/common/services/model.js';16import { ILanguageService } from '../../../../editor/common/languages/language.js';17import { localize } from '../../../../nls.js';18import { URI } from '../../../../base/common/uri.js';19import { IJSONEditingService, IJSONValue } from '../../configuration/common/jsonEditing.js';20import { ResourceMap } from '../../../../base/common/map.js';2122export const EXTENSIONS_CONFIG = '.vscode/extensions.json';2324export interface IExtensionsConfigContent {25recommendations?: string[];26unwantedRecommendations?: string[];27}2829export const IWorkspaceExtensionsConfigService = createDecorator<IWorkspaceExtensionsConfigService>('IWorkspaceExtensionsConfigService');3031export interface IWorkspaceExtensionsConfigService {32readonly _serviceBrand: undefined;3334onDidChangeExtensionsConfigs: Event<void>;35getExtensionsConfigs(): Promise<IExtensionsConfigContent[]>;36getRecommendations(): Promise<string[]>;37getUnwantedRecommendations(): Promise<string[]>;3839toggleRecommendation(extensionId: string): Promise<void>;40toggleUnwantedRecommendation(extensionId: string): Promise<void>;41}4243export class WorkspaceExtensionsConfigService extends Disposable implements IWorkspaceExtensionsConfigService {4445declare readonly _serviceBrand: undefined;4647private readonly _onDidChangeExtensionsConfigs = this._register(new Emitter<void>());48readonly onDidChangeExtensionsConfigs = this._onDidChangeExtensionsConfigs.event;4950constructor(51@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,52@IFileService private readonly fileService: IFileService,53@IQuickInputService private readonly quickInputService: IQuickInputService,54@IModelService private readonly modelService: IModelService,55@ILanguageService private readonly languageService: ILanguageService,56@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,57) {58super();59this._register(workspaceContextService.onDidChangeWorkspaceFolders(e => this._onDidChangeExtensionsConfigs.fire()));60this._register(fileService.onDidFilesChange(e => {61const workspace = workspaceContextService.getWorkspace();62if ((workspace.configuration && e.affects(workspace.configuration))63|| workspace.folders.some(folder => e.affects(folder.toResource(EXTENSIONS_CONFIG)))64) {65this._onDidChangeExtensionsConfigs.fire();66}67}));68}6970async getExtensionsConfigs(): Promise<IExtensionsConfigContent[]> {71const workspace = this.workspaceContextService.getWorkspace();72const result: IExtensionsConfigContent[] = [];73const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;74if (workspaceExtensionsConfigContent) {75result.push(workspaceExtensionsConfigContent);76}77result.push(...await Promise.all(workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder))));78return result;79}8081async getRecommendations(): Promise<string[]> {82const configs = await this.getExtensionsConfigs();83return distinct(configs.flatMap(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : []));84}8586async getUnwantedRecommendations(): Promise<string[]> {87const configs = await this.getExtensionsConfigs();88return distinct(configs.flatMap(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : []));89}9091async toggleRecommendation(extensionId: string): Promise<void> {92extensionId = extensionId.toLowerCase();93const workspace = this.workspaceContextService.getWorkspace();94const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;95const workspaceFolderExtensionsConfigContents = new ResourceMap<IExtensionsConfigContent>();96await Promise.all(workspace.folders.map(async workspaceFolder => {97const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder);98workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent);99}));100101const isWorkspaceRecommended = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.recommendations?.some(r => r.toLowerCase() === extensionId);102const recommendedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.recommendations?.some(r => r.toLowerCase() === extensionId));103const isRecommended = isWorkspaceRecommended || recommendedWorksapceFolders.length > 0;104105const workspaceOrFolders = isRecommended106? await this.pickWorkspaceOrFolders(recommendedWorksapceFolders, isWorkspaceRecommended ? workspace : undefined, localize('select for remove', "Remove extension recommendation from"))107: await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to"));108109for (const workspaceOrWorkspaceFolder of workspaceOrFolders) {110if (isWorkspace(workspaceOrWorkspaceFolder)) {111await this.addOrRemoveWorkspaceRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isRecommended);112} else {113await this.addOrRemoveWorkspaceFolderRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isRecommended);114}115}116}117118async toggleUnwantedRecommendation(extensionId: string): Promise<void> {119const workspace = this.workspaceContextService.getWorkspace();120const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;121const workspaceFolderExtensionsConfigContents = new ResourceMap<IExtensionsConfigContent>();122await Promise.all(workspace.folders.map(async workspaceFolder => {123const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder);124workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent);125}));126127const isWorkspaceUnwanted = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.unwantedRecommendations?.some(r => r === extensionId);128const unWantedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.unwantedRecommendations?.some(r => r === extensionId));129const isUnwanted = isWorkspaceUnwanted || unWantedWorksapceFolders.length > 0;130131const workspaceOrFolders = isUnwanted132? await this.pickWorkspaceOrFolders(unWantedWorksapceFolders, isWorkspaceUnwanted ? workspace : undefined, localize('select for remove', "Remove extension recommendation from"))133: await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to"));134135for (const workspaceOrWorkspaceFolder of workspaceOrFolders) {136if (isWorkspace(workspaceOrWorkspaceFolder)) {137await this.addOrRemoveWorkspaceUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isUnwanted);138} else {139await this.addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isUnwanted);140}141}142}143144private async addOrRemoveWorkspaceFolderRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise<void> {145const values: IJSONValue[] = [];146if (add) {147if (Array.isArray(extensionsConfigContent.recommendations)) {148values.push({ path: ['recommendations', -1], value: extensionId });149} else {150values.push({ path: ['recommendations'], value: [extensionId] });151}152const unwantedRecommendationEdit = this.getEditToRemoveValueFromArray(['unwantedRecommendations'], extensionsConfigContent.unwantedRecommendations, extensionId);153if (unwantedRecommendationEdit) {154values.push(unwantedRecommendationEdit);155}156} else if (extensionsConfigContent.recommendations) {157const recommendationEdit = this.getEditToRemoveValueFromArray(['recommendations'], extensionsConfigContent.recommendations, extensionId);158if (recommendationEdit) {159values.push(recommendationEdit);160}161}162163if (values.length) {164return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true);165}166}167168private async addOrRemoveWorkspaceRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise<void> {169const values: IJSONValue[] = [];170if (extensionsConfigContent) {171if (add) {172const path: JSONPath = ['extensions', 'recommendations'];173if (Array.isArray(extensionsConfigContent.recommendations)) {174values.push({ path: [...path, -1], value: extensionId });175} else {176values.push({ path, value: [extensionId] });177}178const unwantedRecommendationEdit = this.getEditToRemoveValueFromArray(['extensions', 'unwantedRecommendations'], extensionsConfigContent.unwantedRecommendations, extensionId);179if (unwantedRecommendationEdit) {180values.push(unwantedRecommendationEdit);181}182} else if (extensionsConfigContent.recommendations) {183const recommendationEdit = this.getEditToRemoveValueFromArray(['extensions', 'recommendations'], extensionsConfigContent.recommendations, extensionId);184if (recommendationEdit) {185values.push(recommendationEdit);186}187}188} else if (add) {189values.push({ path: ['extensions'], value: { recommendations: [extensionId] } });190}191192if (values.length) {193return this.jsonEditingService.write(workspace.configuration!, values, true);194}195}196197private async addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise<void> {198const values: IJSONValue[] = [];199if (add) {200const path: JSONPath = ['unwantedRecommendations'];201if (Array.isArray(extensionsConfigContent.unwantedRecommendations)) {202values.push({ path: [...path, -1], value: extensionId });203} else {204values.push({ path, value: [extensionId] });205}206const recommendationEdit = this.getEditToRemoveValueFromArray(['recommendations'], extensionsConfigContent.recommendations, extensionId);207if (recommendationEdit) {208values.push(recommendationEdit);209}210} else if (extensionsConfigContent.unwantedRecommendations) {211const unwantedRecommendationEdit = this.getEditToRemoveValueFromArray(['unwantedRecommendations'], extensionsConfigContent.unwantedRecommendations, extensionId);212if (unwantedRecommendationEdit) {213values.push(unwantedRecommendationEdit);214}215}216if (values.length) {217return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true);218}219}220221private async addOrRemoveWorkspaceUnwantedRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise<void> {222const values: IJSONValue[] = [];223if (extensionsConfigContent) {224if (add) {225const path: JSONPath = ['extensions', 'unwantedRecommendations'];226if (Array.isArray(extensionsConfigContent.recommendations)) {227values.push({ path: [...path, -1], value: extensionId });228} else {229values.push({ path, value: [extensionId] });230}231const recommendationEdit = this.getEditToRemoveValueFromArray(['extensions', 'recommendations'], extensionsConfigContent.recommendations, extensionId);232if (recommendationEdit) {233values.push(recommendationEdit);234}235} else if (extensionsConfigContent.unwantedRecommendations) {236const unwantedRecommendationEdit = this.getEditToRemoveValueFromArray(['extensions', 'unwantedRecommendations'], extensionsConfigContent.unwantedRecommendations, extensionId);237if (unwantedRecommendationEdit) {238values.push(unwantedRecommendationEdit);239}240}241} else if (add) {242values.push({ path: ['extensions'], value: { unwantedRecommendations: [extensionId] } });243}244245if (values.length) {246return this.jsonEditingService.write(workspace.configuration!, values, true);247}248}249250private async pickWorkspaceOrFolders(workspaceFolders: IWorkspaceFolder[], workspace: IWorkspace | undefined, placeHolder: string): Promise<(IWorkspace | IWorkspaceFolder)[]> {251const workspaceOrFolders = workspace ? [...workspaceFolders, workspace] : [...workspaceFolders];252if (workspaceOrFolders.length === 1) {253return workspaceOrFolders;254}255256const folderPicks: (IQuickPickItem & { workspaceOrFolder: IWorkspace | IWorkspaceFolder } | IQuickPickSeparator)[] = workspaceFolders.map(workspaceFolder => {257return {258label: workspaceFolder.name,259description: localize('workspace folder', "Workspace Folder"),260workspaceOrFolder: workspaceFolder,261iconClasses: getIconClasses(this.modelService, this.languageService, workspaceFolder.uri, FileKind.ROOT_FOLDER)262};263});264265if (workspace) {266folderPicks.push({ type: 'separator' });267folderPicks.push({268label: localize('workspace', "Workspace"),269workspaceOrFolder: workspace,270});271}272273const result = await this.quickInputService.pick(folderPicks, { placeHolder, canPickMany: true }) || [];274return result.map(r => r.workspaceOrFolder);275}276277private async resolveWorkspaceExtensionConfig(workspaceConfigurationResource: URI): Promise<IExtensionsConfigContent | undefined> {278try {279const content = await this.fileService.readFile(workspaceConfigurationResource);280const extensionsConfigContent = <IExtensionsConfigContent | undefined>parse(content.value.toString())['extensions'];281return extensionsConfigContent ? this.parseExtensionConfig(extensionsConfigContent) : undefined;282} catch (e) { /* Ignore */ }283return undefined;284}285286private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<IExtensionsConfigContent> {287try {288const content = await this.fileService.readFile(workspaceFolder.toResource(EXTENSIONS_CONFIG));289const extensionsConfigContent = <IExtensionsConfigContent>parse(content.value.toString());290return this.parseExtensionConfig(extensionsConfigContent);291} catch (e) { /* ignore */ }292return {};293}294295private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent {296return {297recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),298unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))299};300}301302private getEditToRemoveValueFromArray(path: JSONPath, array: string[] | undefined, value: string): IJSONValue | undefined {303const index = array?.indexOf(value);304if (index !== undefined && index !== -1) {305return { path: [...path, index], value: undefined };306}307return undefined;308}309310}311312registerSingleton(IWorkspaceExtensionsConfigService, WorkspaceExtensionsConfigService, InstantiationType.Delayed);313314315