Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.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 { CancelablePromise, createCancelablePromise, Promises, raceCancellablePromises, raceCancellation, timeout } from '../../../../base/common/async.js';
8
import { CancellationToken } from '../../../../base/common/cancellation.js';
9
import { isCancellationError } from '../../../../base/common/errors.js';
10
import { Emitter, Event } from '../../../../base/common/event.js';
11
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
12
import { isString } from '../../../../base/common/types.js';
13
import { URI } from '../../../../base/common/uri.js';
14
import { localize } from '../../../../nls.js';
15
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
16
import { IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
17
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
18
import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js';
19
import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js';
20
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
21
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
22
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
23
import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js';
24
import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js';
25
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
26
import { EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js';
27
import { IExtensionIgnoredRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';
28
29
type ExtensionRecommendationsNotificationClassification = {
30
owner: 'sandy081';
31
comment: 'Response information when an extension is recommended';
32
userReaction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'User reaction after showing the recommendation prompt. Eg., install, cancel, show, neverShowAgain' };
33
extensionId?: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Id of the extension that is recommended' };
34
source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source from which this recommendation is coming from. Eg., file, exe.,' };
35
};
36
37
type ExtensionWorkspaceRecommendationsNotificationClassification = {
38
owner: 'sandy081';
39
comment: 'Response information when a recommendation from workspace is recommended';
40
userReaction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'User reaction after showing the recommendation prompt. Eg., install, cancel, show, neverShowAgain' };
41
};
42
43
const ignoreImportantExtensionRecommendationStorageKey = 'extensionsAssistant/importantRecommendationsIgnore';
44
const donotShowWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
45
46
type RecommendationsNotificationActions = {
47
onDidInstallRecommendedExtensions(extensions: IExtension[]): void;
48
onDidShowRecommendedExtensions(extensions: IExtension[]): void;
49
onDidCancelRecommendedExtensions(extensions: IExtension[]): void;
50
onDidNeverShowRecommendedExtensionsAgain(extensions: IExtension[]): void;
51
};
52
53
type ExtensionRecommendations = Omit<IExtensionRecommendations, 'extensions'> & { extensions: Array<string | URI> };
54
55
class RecommendationsNotification extends Disposable {
56
57
private _onDidClose = this._register(new Emitter<void>());
58
readonly onDidClose = this._onDidClose.event;
59
60
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
61
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
62
63
private notificationHandle: INotificationHandle | undefined;
64
private cancelled: boolean = false;
65
66
constructor(
67
private readonly severity: Severity,
68
private readonly message: string,
69
private readonly choices: IPromptChoice[],
70
private readonly notificationService: INotificationService
71
) {
72
super();
73
}
74
75
show(): void {
76
if (!this.notificationHandle) {
77
this.updateNotificationHandle(this.notificationService.prompt(this.severity, this.message, this.choices, { sticky: true, priority: NotificationPriority.OPTIONAL, onCancel: () => this.cancelled = true }));
78
}
79
}
80
81
hide(): void {
82
if (this.notificationHandle) {
83
this.onDidCloseDisposable.clear();
84
this.notificationHandle.close();
85
this.cancelled = false;
86
this.updateNotificationHandle(this.notificationService.prompt(this.severity, this.message, this.choices, { priority: NotificationPriority.SILENT, onCancel: () => this.cancelled = true }));
87
}
88
}
89
90
isCancelled(): boolean {
91
return this.cancelled;
92
}
93
94
private readonly onDidCloseDisposable = this._register(new MutableDisposable());
95
private readonly onDidChangeVisibilityDisposable = this._register(new MutableDisposable());
96
private updateNotificationHandle(notificationHandle: INotificationHandle) {
97
this.onDidCloseDisposable.clear();
98
this.onDidChangeVisibilityDisposable.clear();
99
this.notificationHandle = notificationHandle;
100
101
this.onDidCloseDisposable.value = this.notificationHandle.onDidClose(() => {
102
this.onDidCloseDisposable.dispose();
103
this.onDidChangeVisibilityDisposable.dispose();
104
105
this._onDidClose.fire();
106
107
this._onDidClose.dispose();
108
this._onDidChangeVisibility.dispose();
109
});
110
this.onDidChangeVisibilityDisposable.value = this.notificationHandle.onDidChangeVisibility((e) => this._onDidChangeVisibility.fire(e));
111
}
112
}
113
114
type PendingRecommendationsNotification = { recommendationsNotification: RecommendationsNotification; source: RecommendationSource; token: CancellationToken };
115
type VisibleRecommendationsNotification = { recommendationsNotification: RecommendationsNotification; source: RecommendationSource; from: number };
116
117
export class ExtensionRecommendationNotificationService extends Disposable implements IExtensionRecommendationNotificationService {
118
119
declare readonly _serviceBrand: undefined;
120
121
// Ignored Important Recommendations
122
get ignoredRecommendations(): string[] {
123
return distinct([...(<string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendationStorageKey, StorageScope.PROFILE, '[]')))].map(i => i.toLowerCase()));
124
}
125
126
private recommendedExtensions: string[] = [];
127
private recommendationSources: RecommendationSource[] = [];
128
129
private hideVisibleNotificationPromise: CancelablePromise<void> | undefined;
130
private visibleNotification: VisibleRecommendationsNotification | undefined;
131
private pendingNotificaitons: PendingRecommendationsNotification[] = [];
132
133
constructor(
134
@IConfigurationService private readonly configurationService: IConfigurationService,
135
@IStorageService private readonly storageService: IStorageService,
136
@INotificationService private readonly notificationService: INotificationService,
137
@ITelemetryService private readonly telemetryService: ITelemetryService,
138
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
139
@IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService,
140
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
141
@IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService,
142
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
143
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
144
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
145
) {
146
super();
147
}
148
149
hasToIgnoreRecommendationNotifications(): boolean {
150
const config = this.configurationService.getValue<{ ignoreRecommendations: boolean; showRecommendationsOnlyOnDemand?: boolean }>('extensions');
151
return config.ignoreRecommendations || !!config.showRecommendationsOnlyOnDemand;
152
}
153
154
async promptImportantExtensionsInstallNotification(extensionRecommendations: IExtensionRecommendations): Promise<RecommendationsNotificationResult> {
155
const ignoredRecommendations = [...this.extensionIgnoredRecommendationsService.ignoredRecommendations, ...this.ignoredRecommendations];
156
const extensions = extensionRecommendations.extensions.filter(id => !ignoredRecommendations.includes(id));
157
if (!extensions.length) {
158
return RecommendationsNotificationResult.Ignored;
159
}
160
161
return this.promptRecommendationsNotification({ ...extensionRecommendations, extensions }, {
162
onDidInstallRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string; extensionId: string; source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id, source: RecommendationSourceToString(extensionRecommendations.source) })),
163
onDidShowRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string; extensionId: string; source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id, source: RecommendationSourceToString(extensionRecommendations.source) })),
164
onDidCancelRecommendedExtensions: (extensions: IExtension[]) => extensions.forEach(extension => this.telemetryService.publicLog2<{ userReaction: string; extensionId: string; source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id, source: RecommendationSourceToString(extensionRecommendations.source) })),
165
onDidNeverShowRecommendedExtensionsAgain: (extensions: IExtension[]) => {
166
for (const extension of extensions) {
167
this.addToImportantRecommendationsIgnore(extension.identifier.id);
168
this.telemetryService.publicLog2<{ userReaction: string; extensionId: string; source: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id, source: RecommendationSourceToString(extensionRecommendations.source) });
169
}
170
this.notificationService.prompt(
171
Severity.Info,
172
localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"),
173
[{
174
label: localize('ignoreAll', "Yes, Ignore All"),
175
run: () => this.setIgnoreRecommendationsConfig(true)
176
}, {
177
label: localize('no', "No"),
178
run: () => this.setIgnoreRecommendationsConfig(false)
179
}]
180
);
181
},
182
});
183
}
184
185
async promptWorkspaceRecommendations(recommendations: Array<string | URI>): Promise<void> {
186
if (this.storageService.getBoolean(donotShowWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false)) {
187
return;
188
}
189
190
let installed = await this.extensionManagementService.getInstalled();
191
installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
192
recommendations = recommendations.filter(recommendation => installed.every(local =>
193
isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.location)
194
));
195
if (!recommendations.length) {
196
return;
197
}
198
199
await this.promptRecommendationsNotification({ extensions: recommendations, source: RecommendationSource.WORKSPACE, name: localize({ key: 'this repository', comment: ['this repository means the current repository that is opened'] }, "this repository") }, {
200
onDidInstallRecommendedExtensions: () => this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }),
201
onDidShowRecommendedExtensions: () => this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }),
202
onDidCancelRecommendedExtensions: () => this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }),
203
onDidNeverShowRecommendedExtensionsAgain: () => {
204
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' });
205
this.storageService.store(donotShowWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
206
},
207
});
208
209
}
210
211
private async promptRecommendationsNotification({ extensions: extensionIds, source, name, searchValue }: ExtensionRecommendations, recommendationsNotificationActions: RecommendationsNotificationActions): Promise<RecommendationsNotificationResult> {
212
213
if (this.hasToIgnoreRecommendationNotifications()) {
214
return RecommendationsNotificationResult.Ignored;
215
}
216
217
// Do not show exe based recommendations in remote window
218
if (source === RecommendationSource.EXE && this.workbenchEnvironmentService.remoteAuthority) {
219
return RecommendationsNotificationResult.IncompatibleWindow;
220
}
221
222
// Ignore exe recommendation if the window
223
// => has shown an exe based recommendation already
224
// => or has shown any two recommendations already
225
if (source === RecommendationSource.EXE && (this.recommendationSources.includes(RecommendationSource.EXE) || this.recommendationSources.length >= 2)) {
226
return RecommendationsNotificationResult.TooMany;
227
}
228
229
this.recommendationSources.push(source);
230
231
// Ignore exe recommendation if recommendations are already shown
232
if (source === RecommendationSource.EXE && extensionIds.every(id => isString(id) && this.recommendedExtensions.includes(id))) {
233
return RecommendationsNotificationResult.Ignored;
234
}
235
236
const extensions = await this.getInstallableExtensions(extensionIds);
237
if (!extensions.length) {
238
return RecommendationsNotificationResult.Ignored;
239
}
240
241
this.recommendedExtensions = distinct([...this.recommendedExtensions, ...extensionIds.filter(isString)]);
242
243
let extensionsMessage = '';
244
if (extensions.length === 1) {
245
extensionsMessage = localize('extensionFromPublisher', "'{0}' extension from {1}", extensions[0].displayName, extensions[0].publisherDisplayName);
246
} else {
247
const publishers = [...extensions.reduce((result, extension) => result.add(extension.publisherDisplayName), new Set<string>())];
248
if (publishers.length > 2) {
249
extensionsMessage = localize('extensionsFromMultiplePublishers', "extensions from {0}, {1} and others", publishers[0], publishers[1]);
250
} else if (publishers.length === 2) {
251
extensionsMessage = localize('extensionsFromPublishers', "extensions from {0} and {1}", publishers[0], publishers[1]);
252
} else {
253
extensionsMessage = localize('extensionsFromPublisher', "extensions from {0}", publishers[0]);
254
}
255
}
256
257
let message = localize('recommended', "Do you want to install the recommended {0} for {1}?", extensionsMessage, name);
258
if (source === RecommendationSource.EXE) {
259
message = localize({ key: 'exeRecommended', comment: ['Placeholder string is the name of the software that is installed.'] }, "You have {0} installed on your system. Do you want to install the recommended {1} for it?", name, extensionsMessage);
260
}
261
if (!searchValue) {
262
searchValue = source === RecommendationSource.WORKSPACE ? '@recommended' : extensions.map(extensionId => `@id:${extensionId.identifier.id}`).join(' ');
263
}
264
265
const donotShowAgainLabel = source === RecommendationSource.WORKSPACE ? localize('donotShowAgain', "Don't Show Again for this Repository")
266
: extensions.length > 1 ? localize('donotShowAgainExtension', "Don't Show Again for these Extensions") : localize('donotShowAgainExtensionSingle', "Don't Show Again for this Extension");
267
268
return raceCancellablePromises([
269
this._registerP(this.showRecommendationsNotification(extensions, message, searchValue, donotShowAgainLabel, source, recommendationsNotificationActions)),
270
this._registerP(this.waitUntilRecommendationsAreInstalled(extensions))
271
]);
272
273
}
274
275
private showRecommendationsNotification(extensions: IExtension[], message: string, searchValue: string, donotShowAgainLabel: string, source: RecommendationSource,
276
{ onDidInstallRecommendedExtensions, onDidShowRecommendedExtensions, onDidCancelRecommendedExtensions, onDidNeverShowRecommendedExtensionsAgain }: RecommendationsNotificationActions): CancelablePromise<RecommendationsNotificationResult> {
277
return createCancelablePromise<RecommendationsNotificationResult>(async token => {
278
let accepted = false;
279
const choices: (IPromptChoice | IPromptChoiceWithMenu)[] = [];
280
const installExtensions = async (isMachineScoped: boolean) => {
281
this.extensionsWorkbenchService.openSearch(searchValue);
282
onDidInstallRecommendedExtensions(extensions);
283
const galleryExtensions: IGalleryExtension[] = [], resourceExtensions: IExtension[] = [];
284
for (const extension of extensions) {
285
if (extension.gallery) {
286
galleryExtensions.push(extension.gallery);
287
} else if (extension.resourceExtension) {
288
resourceExtensions.push(extension);
289
}
290
}
291
await Promises.settled<any>([
292
Promises.settled(extensions.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))),
293
galleryExtensions.length ? this.extensionManagementService.installGalleryExtensions(galleryExtensions.map(e => ({ extension: e, options: { isMachineScoped } }))) : Promise.resolve(),
294
resourceExtensions.length ? Promise.allSettled(resourceExtensions.map(r => this.extensionsWorkbenchService.install(r))) : Promise.resolve()
295
]);
296
};
297
choices.push({
298
label: localize('install', "Install"),
299
run: () => installExtensions(false),
300
menu: this.userDataSyncEnablementService.isEnabled() && this.userDataSyncEnablementService.isResourceEnabled(SyncResource.Extensions) ? [{
301
label: localize('install and do no sync', "Install (Do not sync)"),
302
run: () => installExtensions(true)
303
}] : undefined,
304
});
305
choices.push(...[{
306
label: localize('show recommendations', "Show Recommendations"),
307
run: async () => {
308
onDidShowRecommendedExtensions(extensions);
309
for (const extension of extensions) {
310
this.extensionsWorkbenchService.open(extension, { pinned: true });
311
}
312
this.extensionsWorkbenchService.openSearch(searchValue);
313
}
314
}, {
315
label: donotShowAgainLabel,
316
isSecondary: true,
317
run: () => {
318
onDidNeverShowRecommendedExtensionsAgain(extensions);
319
}
320
}]);
321
try {
322
accepted = await this.doShowRecommendationsNotification(Severity.Info, message, choices, source, token);
323
} catch (error) {
324
if (!isCancellationError(error)) {
325
throw error;
326
}
327
}
328
329
if (accepted) {
330
return RecommendationsNotificationResult.Accepted;
331
} else {
332
onDidCancelRecommendedExtensions(extensions);
333
return RecommendationsNotificationResult.Cancelled;
334
}
335
336
});
337
}
338
339
private waitUntilRecommendationsAreInstalled(extensions: IExtension[]): CancelablePromise<RecommendationsNotificationResult.Accepted> {
340
const installedExtensions: string[] = [];
341
const disposables = new DisposableStore();
342
return createCancelablePromise(async token => {
343
disposables.add(token.onCancellationRequested(e => disposables.dispose()));
344
return new Promise<RecommendationsNotificationResult.Accepted>((c, e) => {
345
disposables.add(this.extensionManagementService.onInstallExtension(e => {
346
installedExtensions.push(e.identifier.id.toLowerCase());
347
if (extensions.every(e => installedExtensions.includes(e.identifier.id.toLowerCase()))) {
348
c(RecommendationsNotificationResult.Accepted);
349
}
350
}));
351
});
352
});
353
}
354
355
/**
356
* Show recommendations in Queue
357
* At any time only one recommendation is shown
358
* If a new recommendation comes in
359
* => If no recommendation is visible, show it immediately
360
* => Otherwise, add to the pending queue
361
* => If it is not exe based and has higher or same priority as current, hide the current notification after showing it for 3s.
362
* => Otherwise wait until the current notification is hidden.
363
*/
364
private async doShowRecommendationsNotification(severity: Severity, message: string, choices: IPromptChoice[], source: RecommendationSource, token: CancellationToken): Promise<boolean> {
365
const disposables = new DisposableStore();
366
try {
367
const recommendationsNotification = disposables.add(new RecommendationsNotification(severity, message, choices, this.notificationService));
368
disposables.add(Event.once(Event.filter(recommendationsNotification.onDidChangeVisibility, e => !e))(() => this.showNextNotification()));
369
if (this.visibleNotification) {
370
const index = this.pendingNotificaitons.length;
371
disposables.add(token.onCancellationRequested(() => this.pendingNotificaitons.splice(index, 1)));
372
this.pendingNotificaitons.push({ recommendationsNotification, source, token });
373
if (source !== RecommendationSource.EXE && source <= this.visibleNotification.source) {
374
this.hideVisibleNotification(3000);
375
}
376
} else {
377
this.visibleNotification = { recommendationsNotification, source, from: Date.now() };
378
recommendationsNotification.show();
379
}
380
await raceCancellation(new Promise(c => disposables.add(Event.once(recommendationsNotification.onDidClose)(c))), token);
381
return !recommendationsNotification.isCancelled();
382
} finally {
383
disposables.dispose();
384
}
385
}
386
387
private showNextNotification(): void {
388
const index = this.getNextPendingNotificationIndex();
389
const [nextNotificaiton] = index > -1 ? this.pendingNotificaitons.splice(index, 1) : [];
390
391
// Show the next notification after a delay of 500ms (after the current notification is dismissed)
392
timeout(nextNotificaiton ? 500 : 0)
393
.then(() => {
394
this.unsetVisibileNotification();
395
if (nextNotificaiton) {
396
this.visibleNotification = { recommendationsNotification: nextNotificaiton.recommendationsNotification, source: nextNotificaiton.source, from: Date.now() };
397
nextNotificaiton.recommendationsNotification.show();
398
}
399
});
400
}
401
402
/**
403
* Return the recent high priroity pending notification
404
*/
405
private getNextPendingNotificationIndex(): number {
406
let index = this.pendingNotificaitons.length - 1;
407
if (this.pendingNotificaitons.length) {
408
for (let i = 0; i < this.pendingNotificaitons.length; i++) {
409
if (this.pendingNotificaitons[i].source <= this.pendingNotificaitons[index].source) {
410
index = i;
411
}
412
}
413
}
414
return index;
415
}
416
417
private hideVisibleNotification(timeInMillis: number): void {
418
if (this.visibleNotification && !this.hideVisibleNotificationPromise) {
419
const visibleNotification = this.visibleNotification;
420
this.hideVisibleNotificationPromise = timeout(Math.max(timeInMillis - (Date.now() - visibleNotification.from), 0));
421
this.hideVisibleNotificationPromise.then(() => visibleNotification.recommendationsNotification.hide());
422
}
423
}
424
425
private unsetVisibileNotification(): void {
426
this.hideVisibleNotificationPromise?.cancel();
427
this.hideVisibleNotificationPromise = undefined;
428
this.visibleNotification = undefined;
429
}
430
431
private async getInstallableExtensions(recommendations: Array<string | URI>): Promise<IExtension[]> {
432
const result: IExtension[] = [];
433
if (recommendations.length) {
434
const galleryExtensions: string[] = [];
435
const resourceExtensions: URI[] = [];
436
for (const recommendation of recommendations) {
437
if (typeof recommendation === 'string') {
438
galleryExtensions.push(recommendation);
439
} else {
440
resourceExtensions.push(recommendation);
441
}
442
}
443
if (galleryExtensions.length) {
444
const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: 'install-recommendations' }, CancellationToken.None);
445
for (const extension of extensions) {
446
if (extension.gallery && await this.extensionManagementService.canInstall(extension.gallery) === true) {
447
result.push(extension);
448
}
449
}
450
}
451
if (resourceExtensions.length) {
452
const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true);
453
for (const extension of extensions) {
454
if (await this.extensionsWorkbenchService.canInstall(extension) === true) {
455
result.push(extension);
456
}
457
}
458
}
459
}
460
return result;
461
}
462
463
private addToImportantRecommendationsIgnore(id: string) {
464
const importantRecommendationsIgnoreList = [...this.ignoredRecommendations];
465
if (!importantRecommendationsIgnoreList.includes(id.toLowerCase())) {
466
importantRecommendationsIgnoreList.push(id.toLowerCase());
467
this.storageService.store(ignoreImportantExtensionRecommendationStorageKey, JSON.stringify(importantRecommendationsIgnoreList), StorageScope.PROFILE, StorageTarget.USER);
468
}
469
}
470
471
private setIgnoreRecommendationsConfig(configVal: boolean) {
472
this.configurationService.updateValue('extensions.ignoreRecommendations', configVal);
473
}
474
475
private _registerP<T>(o: CancelablePromise<T>): CancelablePromise<T> {
476
this._register(toDisposable(() => o.cancel()));
477
return o;
478
}
479
}
480
481