Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.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 { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
7
import { IExtensionManagementService, IExtensionGalleryService, InstallOperation, InstallExtensionResult } from '../../../../platform/extensionManagement/common/extensionManagement.js';
8
import { IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';
9
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
10
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
11
import { shuffle } from '../../../../base/common/arrays.js';
12
import { Emitter, Event } from '../../../../base/common/event.js';
13
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
14
import { LifecyclePhase, ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js';
15
import { ExeBasedRecommendations } from './exeBasedRecommendations.js';
16
import { WorkspaceRecommendations } from './workspaceRecommendations.js';
17
import { FileBasedRecommendations } from './fileBasedRecommendations.js';
18
import { KeymapRecommendations } from './keymapRecommendations.js';
19
import { LanguageRecommendations } from './languageRecommendations.js';
20
import { ExtensionRecommendation } from './extensionRecommendations.js';
21
import { ConfigBasedRecommendations } from './configBasedRecommendations.js';
22
import { IExtensionRecommendationNotificationService } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js';
23
import { CancelablePromise, timeout } from '../../../../base/common/async.js';
24
import { URI } from '../../../../base/common/uri.js';
25
import { WebRecommendations } from './webRecommendations.js';
26
import { IExtensionsWorkbenchService } from '../common/extensions.js';
27
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
28
import { RemoteRecommendations } from './remoteRecommendations.js';
29
import { IRemoteExtensionsScannerService } from '../../../../platform/remote/common/remoteExtensionsScanner.js';
30
import { IUserDataInitializationService } from '../../../services/userData/browser/userDataInit.js';
31
import { isString } from '../../../../base/common/types.js';
32
33
export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService {
34
35
declare readonly _serviceBrand: undefined;
36
37
// Recommendations
38
private readonly fileBasedRecommendations: FileBasedRecommendations;
39
private readonly workspaceRecommendations: WorkspaceRecommendations;
40
private readonly configBasedRecommendations: ConfigBasedRecommendations;
41
private readonly exeBasedRecommendations: ExeBasedRecommendations;
42
private readonly keymapRecommendations: KeymapRecommendations;
43
private readonly webRecommendations: WebRecommendations;
44
private readonly languageRecommendations: LanguageRecommendations;
45
private readonly remoteRecommendations: RemoteRecommendations;
46
47
public readonly activationPromise: Promise<void>;
48
private sessionSeed: number;
49
50
private _onDidChangeRecommendations = this._register(new Emitter<void>());
51
readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;
52
53
constructor(
54
@IInstantiationService instantiationService: IInstantiationService,
55
@ILifecycleService private readonly lifecycleService: ILifecycleService,
56
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
57
@ITelemetryService private readonly telemetryService: ITelemetryService,
58
@IEnvironmentService private readonly environmentService: IEnvironmentService,
59
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
60
@IExtensionIgnoredRecommendationsService private readonly extensionRecommendationsManagementService: IExtensionIgnoredRecommendationsService,
61
@IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService,
62
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
63
@IRemoteExtensionsScannerService private readonly remoteExtensionsScannerService: IRemoteExtensionsScannerService,
64
@IUserDataInitializationService private readonly userDataInitializationService: IUserDataInitializationService,
65
) {
66
super();
67
68
this.workspaceRecommendations = this._register(instantiationService.createInstance(WorkspaceRecommendations));
69
this.fileBasedRecommendations = this._register(instantiationService.createInstance(FileBasedRecommendations));
70
this.configBasedRecommendations = this._register(instantiationService.createInstance(ConfigBasedRecommendations));
71
this.exeBasedRecommendations = this._register(instantiationService.createInstance(ExeBasedRecommendations));
72
this.keymapRecommendations = this._register(instantiationService.createInstance(KeymapRecommendations));
73
this.webRecommendations = this._register(instantiationService.createInstance(WebRecommendations));
74
this.languageRecommendations = this._register(instantiationService.createInstance(LanguageRecommendations));
75
this.remoteRecommendations = this._register(instantiationService.createInstance(RemoteRecommendations));
76
77
if (!this.isEnabled()) {
78
this.sessionSeed = 0;
79
this.activationPromise = Promise.resolve();
80
return;
81
}
82
83
this.sessionSeed = +new Date();
84
85
// Activation
86
this.activationPromise = this.activate();
87
88
this._register(this.extensionManagementService.onDidInstallExtensions(e => this.onDidInstallExtensions(e)));
89
}
90
91
private async activate(): Promise<void> {
92
try {
93
await Promise.allSettled([
94
this.remoteExtensionsScannerService.whenExtensionsReady(),
95
this.userDataInitializationService.whenInitializationFinished(),
96
this.lifecycleService.when(LifecyclePhase.Restored)]);
97
} catch (error) { /* ignore */ }
98
99
// activate all recommendations
100
await Promise.all([
101
this.workspaceRecommendations.activate(),
102
this.configBasedRecommendations.activate(),
103
this.fileBasedRecommendations.activate(),
104
this.keymapRecommendations.activate(),
105
this.languageRecommendations.activate(),
106
this.webRecommendations.activate(),
107
this.remoteRecommendations.activate()
108
]);
109
110
this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire()));
111
112
this.promptWorkspaceRecommendations();
113
}
114
115
private isEnabled(): boolean {
116
return this.galleryService.isEnabled() && !this.environmentService.isExtensionDevelopment;
117
}
118
119
private async activateProactiveRecommendations(): Promise<void> {
120
await Promise.all([this.exeBasedRecommendations.activate(), this.configBasedRecommendations.activate()]);
121
}
122
123
getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string } } {
124
/* Activate proactive recommendations */
125
this.activateProactiveRecommendations();
126
127
const output: { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string } } = Object.create(null);
128
129
const allRecommendations = [
130
...this.configBasedRecommendations.recommendations,
131
...this.exeBasedRecommendations.recommendations,
132
...this.fileBasedRecommendations.recommendations,
133
...this.workspaceRecommendations.recommendations,
134
...this.keymapRecommendations.recommendations,
135
...this.languageRecommendations.recommendations,
136
...this.webRecommendations.recommendations,
137
];
138
139
for (const { extension, reason } of allRecommendations) {
140
if (isString(extension) && this.isExtensionAllowedToBeRecommended(extension)) {
141
output[extension.toLowerCase()] = reason;
142
}
143
}
144
145
return output;
146
}
147
148
async getConfigBasedRecommendations(): Promise<{ important: string[]; others: string[] }> {
149
await this.configBasedRecommendations.activate();
150
return {
151
important: this.toExtensionIds(this.configBasedRecommendations.importantRecommendations),
152
others: this.toExtensionIds(this.configBasedRecommendations.otherRecommendations)
153
};
154
}
155
156
async getOtherRecommendations(): Promise<string[]> {
157
await this.activationPromise;
158
await this.activateProactiveRecommendations();
159
160
const recommendations = [
161
...this.configBasedRecommendations.otherRecommendations,
162
...this.exeBasedRecommendations.otherRecommendations,
163
...this.webRecommendations.recommendations
164
];
165
166
const extensionIds = this.toExtensionIds(recommendations);
167
shuffle(extensionIds, this.sessionSeed);
168
return extensionIds;
169
}
170
171
async getImportantRecommendations(): Promise<string[]> {
172
await this.activateProactiveRecommendations();
173
174
const recommendations = [
175
...this.fileBasedRecommendations.importantRecommendations,
176
...this.configBasedRecommendations.importantRecommendations,
177
...this.exeBasedRecommendations.importantRecommendations,
178
];
179
180
const extensionIds = this.toExtensionIds(recommendations);
181
shuffle(extensionIds, this.sessionSeed);
182
return extensionIds;
183
}
184
185
getKeymapRecommendations(): string[] {
186
return this.toExtensionIds(this.keymapRecommendations.recommendations);
187
}
188
189
getLanguageRecommendations(): string[] {
190
return this.toExtensionIds(this.languageRecommendations.recommendations);
191
}
192
193
getRemoteRecommendations(): string[] {
194
return this.toExtensionIds(this.remoteRecommendations.recommendations);
195
}
196
197
async getWorkspaceRecommendations(): Promise<Array<string | URI>> {
198
if (!this.isEnabled()) {
199
return [];
200
}
201
await this.workspaceRecommendations.activate();
202
const result: Array<string | URI> = [];
203
for (const { extension } of this.workspaceRecommendations.recommendations) {
204
if (isString(extension)) {
205
if (!result.includes(extension.toLowerCase()) && this.isExtensionAllowedToBeRecommended(extension)) {
206
result.push(extension.toLowerCase());
207
}
208
} else {
209
result.push(extension);
210
}
211
}
212
return result;
213
}
214
215
async getExeBasedRecommendations(exe?: string): Promise<{ important: string[]; others: string[] }> {
216
await this.exeBasedRecommendations.activate();
217
const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe)
218
: { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations };
219
return { important: this.toExtensionIds(important), others: this.toExtensionIds(others) };
220
}
221
222
getFileBasedRecommendations(): string[] {
223
return this.toExtensionIds(this.fileBasedRecommendations.recommendations);
224
}
225
226
private onDidInstallExtensions(results: readonly InstallExtensionResult[]): void {
227
for (const e of results) {
228
if (e.source && !URI.isUri(e.source) && e.operation === InstallOperation.Install) {
229
const extRecommendations = this.getAllRecommendationsWithReason() || {};
230
const recommendationReason = extRecommendations[e.source.identifier.id.toLowerCase()];
231
if (recommendationReason) {
232
/* __GDPR__
233
"extensionGallery:install:recommendations" : {
234
"owner": "sandy081",
235
"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
236
"${include}": [
237
"${GalleryExtensionTelemetryData}"
238
]
239
}
240
*/
241
this.telemetryService.publicLog('extensionGallery:install:recommendations', { ...e.source.telemetryData, recommendationReason: recommendationReason.reasonId });
242
}
243
}
244
}
245
}
246
247
private toExtensionIds(recommendations: ReadonlyArray<ExtensionRecommendation>): string[] {
248
const extensionIds: string[] = [];
249
for (const { extension } of recommendations) {
250
if (isString(extension) && this.isExtensionAllowedToBeRecommended(extension) && !extensionIds.includes(extension.toLowerCase())) {
251
extensionIds.push(extension.toLowerCase());
252
}
253
}
254
return extensionIds;
255
}
256
257
private isExtensionAllowedToBeRecommended(extensionId: string): boolean {
258
return !this.extensionRecommendationsManagementService.ignoredRecommendations.includes(extensionId.toLowerCase());
259
}
260
261
private async promptWorkspaceRecommendations(): Promise<void> {
262
const installed = await this.extensionsWorkbenchService.queryLocal();
263
const allowedRecommendations = [
264
...this.workspaceRecommendations.recommendations,
265
...this.configBasedRecommendations.importantRecommendations.filter(
266
recommendation => !recommendation.whenNotInstalled || recommendation.whenNotInstalled.every(id => installed.every(local => !areSameExtensions(local.identifier, { id }))))
267
]
268
.map(({ extension }) => extension)
269
.filter(extension => !isString(extension) || this.isExtensionAllowedToBeRecommended(extension));
270
271
if (allowedRecommendations.length) {
272
await this._registerP(timeout(5000));
273
await this.extensionRecommendationNotificationService.promptWorkspaceRecommendations(allowedRecommendations);
274
}
275
}
276
277
private _registerP<T>(o: CancelablePromise<T>): CancelablePromise<T> {
278
this._register(toDisposable(() => o.cancel()));
279
return o;
280
}
281
}
282
283