Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.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 { EXTENSION_IDENTIFIER_PATTERN } from '../../../../platform/extensionManagement/common/extensionManagement.js';
7
import { distinct, equals } from '../../../../base/common/arrays.js';
8
import { ExtensionRecommendations, ExtensionRecommendation } from './extensionRecommendations.js';
9
import { INotificationService } from '../../../../platform/notification/common/notification.js';
10
import { ExtensionRecommendationReason } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';
11
import { localize } from '../../../../nls.js';
12
import { Emitter } from '../../../../base/common/event.js';
13
import { IExtensionsConfigContent, IWorkspaceExtensionsConfigService } from '../../../services/extensionRecommendations/common/workspaceExtensionsConfig.js';
14
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
15
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
16
import { FileChangeType, IFileService } from '../../../../platform/files/common/files.js';
17
import { URI } from '../../../../base/common/uri.js';
18
import { RunOnceScheduler } from '../../../../base/common/async.js';
19
import { IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js';
20
21
const WORKSPACE_EXTENSIONS_FOLDER = '.vscode/extensions';
22
23
export class WorkspaceRecommendations extends ExtensionRecommendations {
24
25
private _recommendations: ExtensionRecommendation[] = [];
26
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
27
28
private _onDidChangeRecommendations = this._register(new Emitter<void>());
29
readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;
30
31
private _ignoredRecommendations: string[] = [];
32
get ignoredRecommendations(): ReadonlyArray<string> { return this._ignoredRecommendations; }
33
34
private workspaceExtensions: URI[] = [];
35
private readonly onDidChangeWorkspaceExtensionsScheduler: RunOnceScheduler;
36
37
constructor(
38
@IWorkspaceExtensionsConfigService private readonly workspaceExtensionsConfigService: IWorkspaceExtensionsConfigService,
39
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
40
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
41
@IFileService private readonly fileService: IFileService,
42
@IWorkbenchExtensionManagementService private readonly workbenchExtensionManagementService: IWorkbenchExtensionManagementService,
43
@INotificationService private readonly notificationService: INotificationService,
44
) {
45
super();
46
this.onDidChangeWorkspaceExtensionsScheduler = this._register(new RunOnceScheduler(() => this.onDidChangeWorkspaceExtensionsFolders(), 1000));
47
}
48
49
protected async doActivate(): Promise<void> {
50
this.workspaceExtensions = await this.fetchWorkspaceExtensions();
51
await this.fetch();
52
53
this._register(this.workspaceExtensionsConfigService.onDidChangeExtensionsConfigs(() => this.onDidChangeExtensionsConfigs()));
54
for (const folder of this.contextService.getWorkspace().folders) {
55
this._register(this.fileService.watch(this.uriIdentityService.extUri.joinPath(folder.uri, WORKSPACE_EXTENSIONS_FOLDER)));
56
}
57
58
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.onDidChangeWorkspaceExtensionsScheduler.schedule()));
59
60
this._register(this.fileService.onDidFilesChange(e => {
61
if (this.contextService.getWorkspace().folders.some(folder =>
62
e.affects(this.uriIdentityService.extUri.joinPath(folder.uri, WORKSPACE_EXTENSIONS_FOLDER), FileChangeType.ADDED, FileChangeType.DELETED))
63
) {
64
this.onDidChangeWorkspaceExtensionsScheduler.schedule();
65
}
66
}));
67
}
68
69
private async onDidChangeWorkspaceExtensionsFolders(): Promise<void> {
70
const existing = this.workspaceExtensions;
71
this.workspaceExtensions = await this.fetchWorkspaceExtensions();
72
if (!equals(existing, this.workspaceExtensions, (a, b) => this.uriIdentityService.extUri.isEqual(a, b))) {
73
this.onDidChangeExtensionsConfigs();
74
}
75
}
76
77
private async fetchWorkspaceExtensions(): Promise<URI[]> {
78
const workspaceExtensions: URI[] = [];
79
for (const workspaceFolder of this.contextService.getWorkspace().folders) {
80
const extensionsLocaiton = this.uriIdentityService.extUri.joinPath(workspaceFolder.uri, WORKSPACE_EXTENSIONS_FOLDER);
81
try {
82
const stat = await this.fileService.resolve(extensionsLocaiton);
83
for (const extension of stat.children ?? []) {
84
if (!extension.isDirectory) {
85
continue;
86
}
87
workspaceExtensions.push(extension.resource);
88
}
89
} catch (error) {
90
// ignore
91
}
92
}
93
if (workspaceExtensions.length) {
94
const resourceExtensions = await this.workbenchExtensionManagementService.getExtensions(workspaceExtensions);
95
return resourceExtensions.map(extension => extension.location);
96
}
97
return [];
98
}
99
100
/**
101
* Parse all extensions.json files, fetch workspace recommendations, filter out invalid and unwanted ones
102
*/
103
private async fetch(): Promise<void> {
104
105
const extensionsConfigs = await this.workspaceExtensionsConfigService.getExtensionsConfigs();
106
107
const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigs);
108
if (invalidRecommendations.length) {
109
this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`);
110
}
111
112
this._recommendations = [];
113
this._ignoredRecommendations = [];
114
115
for (const extensionsConfig of extensionsConfigs) {
116
if (extensionsConfig.unwantedRecommendations) {
117
for (const unwantedRecommendation of extensionsConfig.unwantedRecommendations) {
118
if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) {
119
this._ignoredRecommendations.push(unwantedRecommendation);
120
}
121
}
122
}
123
if (extensionsConfig.recommendations) {
124
for (const extensionId of extensionsConfig.recommendations) {
125
if (invalidRecommendations.indexOf(extensionId) === -1) {
126
this._recommendations.push({
127
extension: extensionId,
128
reason: {
129
reasonId: ExtensionRecommendationReason.Workspace,
130
reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")
131
}
132
});
133
}
134
}
135
}
136
}
137
138
for (const extension of this.workspaceExtensions) {
139
this._recommendations.push({
140
extension,
141
reason: {
142
reasonId: ExtensionRecommendationReason.Workspace,
143
reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")
144
}
145
});
146
}
147
}
148
149
private async validateExtensions(contents: IExtensionsConfigContent[]): Promise<{ validRecommendations: string[]; invalidRecommendations: string[]; message: string }> {
150
151
const validExtensions: string[] = [];
152
const invalidExtensions: string[] = [];
153
let message = '';
154
155
const allRecommendations = distinct(contents.flatMap(({ recommendations }) => recommendations || []));
156
const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN);
157
for (const extensionId of allRecommendations) {
158
if (regEx.test(extensionId)) {
159
validExtensions.push(extensionId);
160
} else {
161
invalidExtensions.push(extensionId);
162
message += `${extensionId} (bad format) Expected: <provider>.<name>\n`;
163
}
164
}
165
166
return { validRecommendations: validExtensions, invalidRecommendations: invalidExtensions, message };
167
}
168
169
private async onDidChangeExtensionsConfigs(): Promise<void> {
170
await this.fetch();
171
this._onDidChangeRecommendations.fire();
172
}
173
174
}
175
176
177