Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { distinct } from '../../../../base/common/arrays.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { JSONPath, parse } from '../../../../base/common/json.js';
9
import { Disposable } from '../../../../base/common/lifecycle.js';
10
import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
11
import { FileKind, IFileService } from '../../../../platform/files/common/files.js';
12
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
13
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
14
import { isWorkspace, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
15
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
16
import { IModelService } from '../../../../editor/common/services/model.js';
17
import { ILanguageService } from '../../../../editor/common/languages/language.js';
18
import { localize } from '../../../../nls.js';
19
import { URI } from '../../../../base/common/uri.js';
20
import { IJSONEditingService, IJSONValue } from '../../configuration/common/jsonEditing.js';
21
import { ResourceMap } from '../../../../base/common/map.js';
22
23
export const EXTENSIONS_CONFIG = '.vscode/extensions.json';
24
25
export interface IExtensionsConfigContent {
26
recommendations?: string[];
27
unwantedRecommendations?: string[];
28
}
29
30
export const IWorkspaceExtensionsConfigService = createDecorator<IWorkspaceExtensionsConfigService>('IWorkspaceExtensionsConfigService');
31
32
export interface IWorkspaceExtensionsConfigService {
33
readonly _serviceBrand: undefined;
34
35
onDidChangeExtensionsConfigs: Event<void>;
36
getExtensionsConfigs(): Promise<IExtensionsConfigContent[]>;
37
getRecommendations(): Promise<string[]>;
38
getUnwantedRecommendations(): Promise<string[]>;
39
40
toggleRecommendation(extensionId: string): Promise<void>;
41
toggleUnwantedRecommendation(extensionId: string): Promise<void>;
42
}
43
44
export class WorkspaceExtensionsConfigService extends Disposable implements IWorkspaceExtensionsConfigService {
45
46
declare readonly _serviceBrand: undefined;
47
48
private readonly _onDidChangeExtensionsConfigs = this._register(new Emitter<void>());
49
readonly onDidChangeExtensionsConfigs = this._onDidChangeExtensionsConfigs.event;
50
51
constructor(
52
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
53
@IFileService private readonly fileService: IFileService,
54
@IQuickInputService private readonly quickInputService: IQuickInputService,
55
@IModelService private readonly modelService: IModelService,
56
@ILanguageService private readonly languageService: ILanguageService,
57
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
58
) {
59
super();
60
this._register(workspaceContextService.onDidChangeWorkspaceFolders(e => this._onDidChangeExtensionsConfigs.fire()));
61
this._register(fileService.onDidFilesChange(e => {
62
const workspace = workspaceContextService.getWorkspace();
63
if ((workspace.configuration && e.affects(workspace.configuration))
64
|| workspace.folders.some(folder => e.affects(folder.toResource(EXTENSIONS_CONFIG)))
65
) {
66
this._onDidChangeExtensionsConfigs.fire();
67
}
68
}));
69
}
70
71
async getExtensionsConfigs(): Promise<IExtensionsConfigContent[]> {
72
const workspace = this.workspaceContextService.getWorkspace();
73
const result: IExtensionsConfigContent[] = [];
74
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
75
if (workspaceExtensionsConfigContent) {
76
result.push(workspaceExtensionsConfigContent);
77
}
78
result.push(...await Promise.all(workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder))));
79
return result;
80
}
81
82
async getRecommendations(): Promise<string[]> {
83
const configs = await this.getExtensionsConfigs();
84
return distinct(configs.flatMap(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : []));
85
}
86
87
async getUnwantedRecommendations(): Promise<string[]> {
88
const configs = await this.getExtensionsConfigs();
89
return distinct(configs.flatMap(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : []));
90
}
91
92
async toggleRecommendation(extensionId: string): Promise<void> {
93
extensionId = extensionId.toLowerCase();
94
const workspace = this.workspaceContextService.getWorkspace();
95
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
96
const workspaceFolderExtensionsConfigContents = new ResourceMap<IExtensionsConfigContent>();
97
await Promise.all(workspace.folders.map(async workspaceFolder => {
98
const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder);
99
workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent);
100
}));
101
102
const isWorkspaceRecommended = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.recommendations?.some(r => r.toLowerCase() === extensionId);
103
const recommendedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.recommendations?.some(r => r.toLowerCase() === extensionId));
104
const isRecommended = isWorkspaceRecommended || recommendedWorksapceFolders.length > 0;
105
106
const workspaceOrFolders = isRecommended
107
? await this.pickWorkspaceOrFolders(recommendedWorksapceFolders, isWorkspaceRecommended ? workspace : undefined, localize('select for remove', "Remove extension recommendation from"))
108
: await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to"));
109
110
for (const workspaceOrWorkspaceFolder of workspaceOrFolders) {
111
if (isWorkspace(workspaceOrWorkspaceFolder)) {
112
await this.addOrRemoveWorkspaceRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isRecommended);
113
} else {
114
await this.addOrRemoveWorkspaceFolderRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isRecommended);
115
}
116
}
117
}
118
119
async toggleUnwantedRecommendation(extensionId: string): Promise<void> {
120
const workspace = this.workspaceContextService.getWorkspace();
121
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
122
const workspaceFolderExtensionsConfigContents = new ResourceMap<IExtensionsConfigContent>();
123
await Promise.all(workspace.folders.map(async workspaceFolder => {
124
const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder);
125
workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent);
126
}));
127
128
const isWorkspaceUnwanted = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.unwantedRecommendations?.some(r => r === extensionId);
129
const unWantedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.unwantedRecommendations?.some(r => r === extensionId));
130
const isUnwanted = isWorkspaceUnwanted || unWantedWorksapceFolders.length > 0;
131
132
const workspaceOrFolders = isUnwanted
133
? await this.pickWorkspaceOrFolders(unWantedWorksapceFolders, isWorkspaceUnwanted ? workspace : undefined, localize('select for remove', "Remove extension recommendation from"))
134
: await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to"));
135
136
for (const workspaceOrWorkspaceFolder of workspaceOrFolders) {
137
if (isWorkspace(workspaceOrWorkspaceFolder)) {
138
await this.addOrRemoveWorkspaceUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isUnwanted);
139
} else {
140
await this.addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isUnwanted);
141
}
142
}
143
}
144
145
private async addOrRemoveWorkspaceFolderRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise<void> {
146
const values: IJSONValue[] = [];
147
if (add) {
148
if (Array.isArray(extensionsConfigContent.recommendations)) {
149
values.push({ path: ['recommendations', -1], value: extensionId });
150
} else {
151
values.push({ path: ['recommendations'], value: [extensionId] });
152
}
153
const unwantedRecommendationEdit = this.getEditToRemoveValueFromArray(['unwantedRecommendations'], extensionsConfigContent.unwantedRecommendations, extensionId);
154
if (unwantedRecommendationEdit) {
155
values.push(unwantedRecommendationEdit);
156
}
157
} else if (extensionsConfigContent.recommendations) {
158
const recommendationEdit = this.getEditToRemoveValueFromArray(['recommendations'], extensionsConfigContent.recommendations, extensionId);
159
if (recommendationEdit) {
160
values.push(recommendationEdit);
161
}
162
}
163
164
if (values.length) {
165
return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true);
166
}
167
}
168
169
private async addOrRemoveWorkspaceRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise<void> {
170
const values: IJSONValue[] = [];
171
if (extensionsConfigContent) {
172
if (add) {
173
const path: JSONPath = ['extensions', 'recommendations'];
174
if (Array.isArray(extensionsConfigContent.recommendations)) {
175
values.push({ path: [...path, -1], value: extensionId });
176
} else {
177
values.push({ path, value: [extensionId] });
178
}
179
const unwantedRecommendationEdit = this.getEditToRemoveValueFromArray(['extensions', 'unwantedRecommendations'], extensionsConfigContent.unwantedRecommendations, extensionId);
180
if (unwantedRecommendationEdit) {
181
values.push(unwantedRecommendationEdit);
182
}
183
} else if (extensionsConfigContent.recommendations) {
184
const recommendationEdit = this.getEditToRemoveValueFromArray(['extensions', 'recommendations'], extensionsConfigContent.recommendations, extensionId);
185
if (recommendationEdit) {
186
values.push(recommendationEdit);
187
}
188
}
189
} else if (add) {
190
values.push({ path: ['extensions'], value: { recommendations: [extensionId] } });
191
}
192
193
if (values.length) {
194
return this.jsonEditingService.write(workspace.configuration!, values, true);
195
}
196
}
197
198
private async addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise<void> {
199
const values: IJSONValue[] = [];
200
if (add) {
201
const path: JSONPath = ['unwantedRecommendations'];
202
if (Array.isArray(extensionsConfigContent.unwantedRecommendations)) {
203
values.push({ path: [...path, -1], value: extensionId });
204
} else {
205
values.push({ path, value: [extensionId] });
206
}
207
const recommendationEdit = this.getEditToRemoveValueFromArray(['recommendations'], extensionsConfigContent.recommendations, extensionId);
208
if (recommendationEdit) {
209
values.push(recommendationEdit);
210
}
211
} else if (extensionsConfigContent.unwantedRecommendations) {
212
const unwantedRecommendationEdit = this.getEditToRemoveValueFromArray(['unwantedRecommendations'], extensionsConfigContent.unwantedRecommendations, extensionId);
213
if (unwantedRecommendationEdit) {
214
values.push(unwantedRecommendationEdit);
215
}
216
}
217
if (values.length) {
218
return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true);
219
}
220
}
221
222
private async addOrRemoveWorkspaceUnwantedRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise<void> {
223
const values: IJSONValue[] = [];
224
if (extensionsConfigContent) {
225
if (add) {
226
const path: JSONPath = ['extensions', 'unwantedRecommendations'];
227
if (Array.isArray(extensionsConfigContent.recommendations)) {
228
values.push({ path: [...path, -1], value: extensionId });
229
} else {
230
values.push({ path, value: [extensionId] });
231
}
232
const recommendationEdit = this.getEditToRemoveValueFromArray(['extensions', 'recommendations'], extensionsConfigContent.recommendations, extensionId);
233
if (recommendationEdit) {
234
values.push(recommendationEdit);
235
}
236
} else if (extensionsConfigContent.unwantedRecommendations) {
237
const unwantedRecommendationEdit = this.getEditToRemoveValueFromArray(['extensions', 'unwantedRecommendations'], extensionsConfigContent.unwantedRecommendations, extensionId);
238
if (unwantedRecommendationEdit) {
239
values.push(unwantedRecommendationEdit);
240
}
241
}
242
} else if (add) {
243
values.push({ path: ['extensions'], value: { unwantedRecommendations: [extensionId] } });
244
}
245
246
if (values.length) {
247
return this.jsonEditingService.write(workspace.configuration!, values, true);
248
}
249
}
250
251
private async pickWorkspaceOrFolders(workspaceFolders: IWorkspaceFolder[], workspace: IWorkspace | undefined, placeHolder: string): Promise<(IWorkspace | IWorkspaceFolder)[]> {
252
const workspaceOrFolders = workspace ? [...workspaceFolders, workspace] : [...workspaceFolders];
253
if (workspaceOrFolders.length === 1) {
254
return workspaceOrFolders;
255
}
256
257
const folderPicks: (IQuickPickItem & { workspaceOrFolder: IWorkspace | IWorkspaceFolder } | IQuickPickSeparator)[] = workspaceFolders.map(workspaceFolder => {
258
return {
259
label: workspaceFolder.name,
260
description: localize('workspace folder', "Workspace Folder"),
261
workspaceOrFolder: workspaceFolder,
262
iconClasses: getIconClasses(this.modelService, this.languageService, workspaceFolder.uri, FileKind.ROOT_FOLDER)
263
};
264
});
265
266
if (workspace) {
267
folderPicks.push({ type: 'separator' });
268
folderPicks.push({
269
label: localize('workspace', "Workspace"),
270
workspaceOrFolder: workspace,
271
});
272
}
273
274
const result = await this.quickInputService.pick(folderPicks, { placeHolder, canPickMany: true }) || [];
275
return result.map(r => r.workspaceOrFolder);
276
}
277
278
private async resolveWorkspaceExtensionConfig(workspaceConfigurationResource: URI): Promise<IExtensionsConfigContent | undefined> {
279
try {
280
const content = await this.fileService.readFile(workspaceConfigurationResource);
281
const extensionsConfigContent = <IExtensionsConfigContent | undefined>parse(content.value.toString())['extensions'];
282
return extensionsConfigContent ? this.parseExtensionConfig(extensionsConfigContent) : undefined;
283
} catch (e) { /* Ignore */ }
284
return undefined;
285
}
286
287
private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<IExtensionsConfigContent> {
288
try {
289
const content = await this.fileService.readFile(workspaceFolder.toResource(EXTENSIONS_CONFIG));
290
const extensionsConfigContent = <IExtensionsConfigContent>parse(content.value.toString());
291
return this.parseExtensionConfig(extensionsConfigContent);
292
} catch (e) { /* ignore */ }
293
return {};
294
}
295
296
private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent {
297
return {
298
recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),
299
unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))
300
};
301
}
302
303
private getEditToRemoveValueFromArray(path: JSONPath, array: string[] | undefined, value: string): IJSONValue | undefined {
304
const index = array?.indexOf(value);
305
if (index !== undefined && index !== -1) {
306
return { path: [...path, index], value: undefined };
307
}
308
return undefined;
309
}
310
311
}
312
313
registerSingleton(IWorkspaceExtensionsConfigService, WorkspaceExtensionsConfigService, InstantiationType.Delayed);
314
315