Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionsViews.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 { localize } from '../../../../nls.js';
7
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { Event, Emitter } from '../../../../base/common/event.js';
9
import { isCancellationError, getErrorMessage, CancellationError } from '../../../../base/common/errors.js';
10
import { PagedModel, IPagedModel, DelayedPagedModel, IPager } from '../../../../base/common/paging.js';
11
import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo, ExtensionGalleryErrorCode, ExtensionGalleryError } from '../../../../platform/extensionManagement/common/extensionManagement.js';
12
import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js';
13
import { IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';
14
import { areSameExtensions, getExtensionDependencies } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
15
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
16
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
17
import { append, $ } from '../../../../base/browser/dom.js';
18
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { ExtensionResultsListFocused, ExtensionState, IExtension, IExtensionsViewState, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from '../common/extensions.js';
20
import { Query } from '../common/extensionQuery.js';
21
import { IExtensionService, toExtension } from '../../../services/extensions/common/extensions.js';
22
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
23
import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';
24
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
25
import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js';
26
import { WorkbenchPagedList } from '../../../../platform/list/browser/listService.js';
27
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
28
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
29
import { ViewPane, IViewPaneOptions, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.js';
30
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
31
import { coalesce, distinct, range } from '../../../../base/common/arrays.js';
32
import { alert } from '../../../../base/browser/ui/aria/aria.js';
33
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
34
import { ActionRunner } from '../../../../base/common/actions.js';
35
import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionUntrustedWorkspaceSupportType, ExtensionVirtualWorkspaceSupportType, IExtensionDescription, IExtensionIdentifier, isLanguagePackExtension } from '../../../../platform/extensions/common/extensions.js';
36
import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from '../../../../base/common/async.js';
37
import { IProductService } from '../../../../platform/product/common/productService.js';
38
import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js';
39
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
40
import { IViewDescriptorService } from '../../../common/views.js';
41
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
42
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
43
import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js';
44
import { isVirtualWorkspace } from '../../../../platform/workspace/common/virtualWorkspace.js';
45
import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';
46
import { ILogService } from '../../../../platform/log/common/log.js';
47
import { isOfflineError } from '../../../../base/parts/request/common/request.js';
48
import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js';
49
import { Registry } from '../../../../platform/registry/common/platform.js';
50
import { Extensions, IExtensionFeatureRenderer, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js';
51
import { URI } from '../../../../base/common/uri.js';
52
import { isString } from '../../../../base/common/types.js';
53
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
54
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
55
import { ExtensionsList } from './extensionsViewer.js';
56
57
export const NONE_CATEGORY = 'none';
58
59
type Message = {
60
readonly text: string;
61
readonly severity: Severity;
62
};
63
64
class ExtensionsViewState extends Disposable implements IExtensionsViewState {
65
66
private readonly _onFocus: Emitter<IExtension> = this._register(new Emitter<IExtension>());
67
readonly onFocus: Event<IExtension> = this._onFocus.event;
68
69
private readonly _onBlur: Emitter<IExtension> = this._register(new Emitter<IExtension>());
70
readonly onBlur: Event<IExtension> = this._onBlur.event;
71
72
private currentlyFocusedItems: IExtension[] = [];
73
74
filters: {
75
featureId?: string;
76
} = {};
77
78
onFocusChange(extensions: IExtension[]): void {
79
this.currentlyFocusedItems.forEach(extension => this._onBlur.fire(extension));
80
this.currentlyFocusedItems = extensions;
81
this.currentlyFocusedItems.forEach(extension => this._onFocus.fire(extension));
82
}
83
}
84
85
export interface ExtensionsListViewOptions {
86
server?: IExtensionManagementServer;
87
flexibleHeight?: boolean;
88
onDidChangeTitle?: Event<string>;
89
hideBadge?: boolean;
90
}
91
92
interface IQueryResult {
93
model: IPagedModel<IExtension>;
94
message?: { text: string; severity: Severity };
95
readonly onDidChangeModel?: Event<IPagedModel<IExtension>>;
96
readonly disposables: DisposableStore;
97
}
98
99
const enum LocalSortBy {
100
UpdateDate = 'UpdateDate',
101
}
102
103
function isLocalSortBy(value: any): value is LocalSortBy {
104
switch (value as LocalSortBy) {
105
case LocalSortBy.UpdateDate: return true;
106
}
107
}
108
109
type SortBy = LocalSortBy | GallerySortBy;
110
type IQueryOptions = Omit<IGalleryQueryOptions, 'sortBy'> & { sortBy?: SortBy };
111
112
export abstract class AbstractExtensionsListView<T> extends ViewPane {
113
abstract show(query: string, refresh?: boolean): Promise<IPagedModel<T>>;
114
}
115
116
export class ExtensionsListView extends AbstractExtensionsListView<IExtension> {
117
118
private static RECENT_UPDATE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days
119
120
private bodyTemplate: {
121
messageContainer: HTMLElement;
122
messageSeverityIcon: HTMLElement;
123
messageBox: HTMLElement;
124
extensionsList: HTMLElement;
125
} | undefined;
126
private badge: CountBadge | undefined;
127
private list: WorkbenchPagedList<IExtension> | null = null;
128
private queryRequest: { query: string; request: CancelablePromise<IPagedModel<IExtension>> } | null = null;
129
private queryResult: IQueryResult | undefined;
130
private extensionsViewState: ExtensionsViewState | undefined;
131
132
private readonly contextMenuActionRunner = this._register(new ActionRunner());
133
134
constructor(
135
protected readonly options: ExtensionsListViewOptions,
136
viewletViewOptions: IViewletViewOptions,
137
@INotificationService protected notificationService: INotificationService,
138
@IKeybindingService keybindingService: IKeybindingService,
139
@IContextMenuService contextMenuService: IContextMenuService,
140
@IInstantiationService instantiationService: IInstantiationService,
141
@IThemeService themeService: IThemeService,
142
@IExtensionService private readonly extensionService: IExtensionService,
143
@IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService,
144
@IExtensionRecommendationsService protected extensionRecommendationsService: IExtensionRecommendationsService,
145
@ITelemetryService protected readonly telemetryService: ITelemetryService,
146
@IHoverService hoverService: IHoverService,
147
@IConfigurationService configurationService: IConfigurationService,
148
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
149
@IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService,
150
@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,
151
@IWorkbenchExtensionManagementService protected readonly extensionManagementService: IWorkbenchExtensionManagementService,
152
@IWorkspaceContextService protected readonly workspaceService: IWorkspaceContextService,
153
@IProductService protected readonly productService: IProductService,
154
@IContextKeyService contextKeyService: IContextKeyService,
155
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
156
@IOpenerService openerService: IOpenerService,
157
@IStorageService private readonly storageService: IStorageService,
158
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,
159
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
160
@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,
161
@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,
162
@ILogService private readonly logService: ILogService
163
) {
164
super({
165
...(viewletViewOptions as IViewPaneOptions),
166
showActions: ViewPaneShowActions.Always,
167
maximumBodySize: options.flexibleHeight ? (storageService.getNumber(`${viewletViewOptions.id}.size`, StorageScope.PROFILE, 0) ? undefined : 0) : undefined
168
}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
169
if (this.options.onDidChangeTitle) {
170
this._register(this.options.onDidChangeTitle(title => this.updateTitle(title)));
171
}
172
173
this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && this.notificationService.error(error)));
174
this.registerActions();
175
}
176
177
protected registerActions(): void { }
178
179
protected override renderHeader(container: HTMLElement): void {
180
container.classList.add('extension-view-header');
181
super.renderHeader(container);
182
183
if (!this.options.hideBadge) {
184
this.badge = this._register(new CountBadge(append(container, $('.count-badge-wrapper')), {}, defaultCountBadgeStyles));
185
}
186
}
187
188
protected override renderBody(container: HTMLElement): void {
189
super.renderBody(container);
190
191
const messageContainer = append(container, $('.message-container'));
192
const messageSeverityIcon = append(messageContainer, $(''));
193
const messageBox = append(messageContainer, $('.message'));
194
const extensionsList = append(container, $('.extensions-list'));
195
this.extensionsViewState = this._register(new ExtensionsViewState());
196
this.list = this._register(this.instantiationService.createInstance(ExtensionsList, extensionsList, this.id, {}, this.extensionsViewState)).list;
197
ExtensionResultsListFocused.bindTo(this.list.contextKeyService);
198
this._register(this.list.onDidChangeFocus(e => this.extensionsViewState?.onFocusChange(coalesce(e.elements)), this));
199
200
this.bodyTemplate = {
201
extensionsList,
202
messageBox,
203
messageContainer,
204
messageSeverityIcon
205
};
206
207
if (this.queryResult) {
208
this.setModel(this.queryResult.model);
209
}
210
}
211
212
protected override layoutBody(height: number, width: number): void {
213
super.layoutBody(height, width);
214
if (this.bodyTemplate) {
215
this.bodyTemplate.extensionsList.style.height = height + 'px';
216
}
217
this.list?.layout(height, width);
218
}
219
220
async show(query: string, refresh?: boolean): Promise<IPagedModel<IExtension>> {
221
if (this.queryRequest) {
222
if (!refresh && this.queryRequest.query === query) {
223
return this.queryRequest.request;
224
}
225
this.queryRequest.request.cancel();
226
this.queryRequest = null;
227
}
228
229
if (this.queryResult) {
230
this.queryResult.disposables.dispose();
231
this.queryResult = undefined;
232
if (this.extensionsViewState) {
233
this.extensionsViewState.filters = {};
234
}
235
}
236
237
const parsedQuery = Query.parse(query);
238
239
const options: IQueryOptions = {
240
sortOrder: SortOrder.Default
241
};
242
243
switch (parsedQuery.sortBy) {
244
case 'installs': options.sortBy = GallerySortBy.InstallCount; break;
245
case 'rating': options.sortBy = GallerySortBy.WeightedRating; break;
246
case 'name': options.sortBy = GallerySortBy.Title; break;
247
case 'publishedDate': options.sortBy = GallerySortBy.PublishedDate; break;
248
case 'updateDate': options.sortBy = LocalSortBy.UpdateDate; break;
249
}
250
251
const request = createCancelablePromise(async token => {
252
try {
253
this.queryResult = await this.query(parsedQuery, options, token);
254
const model = this.queryResult.model;
255
this.setModel(model, this.queryResult.message);
256
if (this.queryResult.onDidChangeModel) {
257
this.queryResult.disposables.add(this.queryResult.onDidChangeModel(model => {
258
if (this.queryResult) {
259
this.queryResult.model = model;
260
this.updateModel(model);
261
}
262
}));
263
}
264
return model;
265
} catch (e) {
266
const model = new PagedModel([]);
267
if (!isCancellationError(e)) {
268
this.logService.error(e);
269
this.setModel(model, this.getMessage(e));
270
}
271
return this.list ? this.list.model : model;
272
}
273
});
274
275
request.finally(() => this.queryRequest = null);
276
this.queryRequest = { query, request };
277
return request;
278
}
279
280
count(): number {
281
return this.queryResult?.model.length ?? 0;
282
}
283
284
protected showEmptyModel(): Promise<IPagedModel<IExtension>> {
285
const emptyModel = new PagedModel([]);
286
this.setModel(emptyModel);
287
return Promise.resolve(emptyModel);
288
}
289
290
private async query(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IQueryResult> {
291
const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g;
292
const ids: string[] = [];
293
let idMatch;
294
while ((idMatch = idRegex.exec(query.value)) !== null) {
295
const name = idMatch[1];
296
ids.push(name);
297
}
298
if (ids.length) {
299
const model = await this.queryByIds(ids, options, token);
300
return { model, disposables: new DisposableStore() };
301
}
302
303
if (ExtensionsListView.isLocalExtensionsQuery(query.value, query.sortBy)) {
304
return this.queryLocal(query, options);
305
}
306
307
if (ExtensionsListView.isSearchPopularQuery(query.value)) {
308
query.value = query.value.replace('@popular', '');
309
options.sortBy = !options.sortBy ? GallerySortBy.InstallCount : options.sortBy;
310
}
311
else if (ExtensionsListView.isSearchRecentlyPublishedQuery(query.value)) {
312
query.value = query.value.replace('@recentlyPublished', '');
313
options.sortBy = !options.sortBy ? GallerySortBy.PublishedDate : options.sortBy;
314
}
315
316
const galleryQueryOptions: IGalleryQueryOptions = { ...options, sortBy: isLocalSortBy(options.sortBy) ? undefined : options.sortBy };
317
return this.queryGallery(query, galleryQueryOptions, token);
318
}
319
320
private async queryByIds(ids: string[], options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
321
const idsSet: Set<string> = ids.reduce((result, id) => { result.add(id.toLowerCase()); return result; }, new Set<string>());
322
const result = (await this.extensionsWorkbenchService.queryLocal(this.options.server))
323
.filter(e => idsSet.has(e.identifier.id.toLowerCase()));
324
325
const galleryIds = result.length ? ids.filter(id => result.every(r => !areSameExtensions(r.identifier, { id }))) : ids;
326
327
if (galleryIds.length) {
328
const galleryResult = await this.extensionsWorkbenchService.getExtensions(galleryIds.map(id => ({ id })), { source: 'queryById' }, token);
329
result.push(...galleryResult);
330
}
331
332
return new PagedModel(result);
333
}
334
335
private async queryLocal(query: Query, options: IQueryOptions): Promise<IQueryResult> {
336
const local = await this.extensionsWorkbenchService.queryLocal(this.options.server);
337
let { extensions, canIncludeInstalledExtensions, description } = await this.filterLocal(local, this.extensionService.extensions, query, options);
338
const disposables = new DisposableStore();
339
const onDidChangeModel = disposables.add(new Emitter<IPagedModel<IExtension>>());
340
341
if (canIncludeInstalledExtensions) {
342
let isDisposed: boolean = false;
343
disposables.add(toDisposable(() => isDisposed = true));
344
disposables.add(Event.debounce(Event.any(
345
Event.filter(this.extensionsWorkbenchService.onChange, e => e?.state === ExtensionState.Installed),
346
this.extensionService.onDidChangeExtensions
347
), () => undefined)(async () => {
348
const local = this.options.server ? this.extensionsWorkbenchService.installed.filter(e => e.server === this.options.server) : this.extensionsWorkbenchService.local;
349
const { extensions: newExtensions } = await this.filterLocal(local, this.extensionService.extensions, query, options);
350
if (!isDisposed) {
351
const mergedExtensions = this.mergeAddedExtensions(extensions, newExtensions);
352
if (mergedExtensions) {
353
extensions = mergedExtensions;
354
onDidChangeModel.fire(new PagedModel(extensions));
355
}
356
}
357
}));
358
}
359
360
return {
361
model: new PagedModel(extensions),
362
message: description ? { text: description, severity: Severity.Info } : undefined,
363
onDidChangeModel: onDidChangeModel.event,
364
disposables
365
};
366
}
367
368
private async filterLocal(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): Promise<{ extensions: IExtension[]; canIncludeInstalledExtensions: boolean; description?: string }> {
369
const value = query.value;
370
let extensions: IExtension[] = [];
371
let description: string | undefined;
372
const includeBuiltin = /@builtin/i.test(value);
373
const canIncludeInstalledExtensions = !includeBuiltin;
374
375
if (/@installed/i.test(value)) {
376
extensions = this.filterInstalledExtensions(local, runningExtensions, query, options);
377
}
378
379
else if (/@outdated/i.test(value)) {
380
extensions = this.filterOutdatedExtensions(local, query, options);
381
}
382
383
else if (/@disabled/i.test(value)) {
384
extensions = this.filterDisabledExtensions(local, runningExtensions, query, options, includeBuiltin);
385
}
386
387
else if (/@enabled/i.test(value)) {
388
extensions = this.filterEnabledExtensions(local, runningExtensions, query, options, includeBuiltin);
389
}
390
391
else if (/@workspaceUnsupported/i.test(value)) {
392
extensions = this.filterWorkspaceUnsupportedExtensions(local, query, options);
393
}
394
395
else if (/@deprecated/i.test(query.value)) {
396
extensions = await this.filterDeprecatedExtensions(local, query, options);
397
}
398
399
else if (/@recentlyUpdated/i.test(query.value)) {
400
extensions = this.filterRecentlyUpdatedExtensions(local, query, options);
401
}
402
403
else if (/@feature:/i.test(query.value)) {
404
const result = this.filterExtensionsByFeature(local, query);
405
if (result) {
406
extensions = result.extensions;
407
description = result.description;
408
}
409
}
410
411
else if (includeBuiltin) {
412
extensions = this.filterBuiltinExtensions(local, query, options);
413
}
414
415
return { extensions, canIncludeInstalledExtensions, description };
416
}
417
418
private filterBuiltinExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
419
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
420
value = value.replaceAll(/@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
421
422
const result = local
423
.filter(e => e.isBuiltin && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
424
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));
425
426
return this.sortExtensions(result, options);
427
}
428
429
private filterExtensionByCategory(e: IExtension, includedCategories: string[], excludedCategories: string[]): boolean {
430
if (!includedCategories.length && !excludedCategories.length) {
431
return true;
432
}
433
if (e.categories.length) {
434
if (excludedCategories.length && e.categories.some(category => excludedCategories.includes(category.toLowerCase()))) {
435
return false;
436
}
437
return e.categories.some(category => includedCategories.includes(category.toLowerCase()));
438
} else {
439
return includedCategories.includes(NONE_CATEGORY);
440
}
441
}
442
443
private parseCategories(value: string): { value: string; includedCategories: string[]; excludedCategories: string[] } {
444
const includedCategories: string[] = [];
445
const excludedCategories: string[] = [];
446
value = value.replace(/\bcategory:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedCategory, category) => {
447
const entry = (category || quotedCategory || '').toLowerCase();
448
if (entry.startsWith('-')) {
449
if (excludedCategories.indexOf(entry) === -1) {
450
excludedCategories.push(entry);
451
}
452
} else {
453
if (includedCategories.indexOf(entry) === -1) {
454
includedCategories.push(entry);
455
}
456
}
457
return '';
458
});
459
return { value, includedCategories, excludedCategories };
460
}
461
462
private filterInstalledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] {
463
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
464
465
value = value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
466
467
const matchingText = (e: IExtension) => (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1 || e.description.toLowerCase().indexOf(value) > -1)
468
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories);
469
let result;
470
471
if (options.sortBy !== undefined) {
472
result = local.filter(e => !e.isBuiltin && matchingText(e));
473
result = this.sortExtensions(result, options);
474
} else {
475
result = local.filter(e => (!e.isBuiltin || e.outdated || e.runtimeState !== undefined) && matchingText(e));
476
const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(e.identifier.value, e); return result; }, new ExtensionIdentifierMap<IExtensionDescription>());
477
478
const defaultSort = (e1: IExtension, e2: IExtension) => {
479
const running1 = runningExtensionsById.get(e1.identifier.id);
480
const isE1Running = !!running1 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running1)) === e1.server;
481
const running2 = runningExtensionsById.get(e2.identifier.id);
482
const isE2Running = running2 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running2)) === e2.server;
483
if ((isE1Running && isE2Running)) {
484
return e1.displayName.localeCompare(e2.displayName);
485
}
486
const isE1LanguagePackExtension = e1.local && isLanguagePackExtension(e1.local.manifest);
487
const isE2LanguagePackExtension = e2.local && isLanguagePackExtension(e2.local.manifest);
488
if (!isE1Running && !isE2Running) {
489
if (isE1LanguagePackExtension) {
490
return -1;
491
}
492
if (isE2LanguagePackExtension) {
493
return 1;
494
}
495
return e1.displayName.localeCompare(e2.displayName);
496
}
497
if ((isE1Running && isE2LanguagePackExtension) || (isE2Running && isE1LanguagePackExtension)) {
498
return e1.displayName.localeCompare(e2.displayName);
499
}
500
return isE1Running ? -1 : 1;
501
};
502
503
const incompatible: IExtension[] = [];
504
const deprecated: IExtension[] = [];
505
const outdated: IExtension[] = [];
506
const actionRequired: IExtension[] = [];
507
const noActionRequired: IExtension[] = [];
508
509
for (const e of result) {
510
if (e.enablementState === EnablementState.DisabledByInvalidExtension) {
511
incompatible.push(e);
512
}
513
else if (e.deprecationInfo) {
514
deprecated.push(e);
515
}
516
else if (e.outdated && this.extensionEnablementService.isEnabledEnablementState(e.enablementState)) {
517
outdated.push(e);
518
}
519
else if (e.runtimeState) {
520
actionRequired.push(e);
521
}
522
else {
523
noActionRequired.push(e);
524
}
525
}
526
527
result = [
528
...incompatible.sort(defaultSort),
529
...deprecated.sort(defaultSort),
530
...outdated.sort(defaultSort),
531
...actionRequired.sort(defaultSort),
532
...noActionRequired.sort(defaultSort)
533
];
534
}
535
return result;
536
}
537
538
private filterOutdatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
539
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
540
541
value = value.replace(/@outdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
542
543
const result = local
544
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
545
.filter(extension => extension.outdated
546
&& (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)
547
&& this.filterExtensionByCategory(extension, includedCategories, excludedCategories));
548
549
return this.sortExtensions(result, options);
550
}
551
552
private filterDisabledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions, includeBuiltin: boolean): IExtension[] {
553
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
554
555
value = value.replaceAll(/@disabled|@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
556
557
if (includeBuiltin) {
558
local = local.filter(e => e.isBuiltin);
559
}
560
const result = local
561
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
562
.filter(e => runningExtensions.every(r => !areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))
563
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
564
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));
565
566
return this.sortExtensions(result, options);
567
}
568
569
private filterEnabledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions, includeBuiltin: boolean): IExtension[] {
570
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
571
572
value = value ? value.replaceAll(/@enabled|@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase() : '';
573
574
local = local.filter(e => e.isBuiltin === includeBuiltin);
575
const result = local
576
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
577
.filter(e => runningExtensions.some(r => areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))
578
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
579
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));
580
581
return this.sortExtensions(result, options);
582
}
583
584
private filterWorkspaceUnsupportedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
585
// shows local extensions which are restricted or disabled in the current workspace because of the extension's capability
586
587
const queryString = query.value; // @sortby is already filtered out
588
589
const match = queryString.match(/^\s*@workspaceUnsupported(?::(untrusted|virtual)(Partial)?)?(?:\s+([^\s]*))?/i);
590
if (!match) {
591
return [];
592
}
593
const type = match[1]?.toLowerCase();
594
const partial = !!match[2];
595
const nameFilter = match[3]?.toLowerCase();
596
597
if (nameFilter) {
598
local = local.filter(extension => extension.name.toLowerCase().indexOf(nameFilter) > -1 || extension.displayName.toLowerCase().indexOf(nameFilter) > -1);
599
}
600
601
const hasVirtualSupportType = (extension: IExtension, supportType: ExtensionVirtualWorkspaceSupportType) => {
602
return extension.local && this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.local.manifest) === supportType;
603
};
604
605
const hasRestrictedSupportType = (extension: IExtension, supportType: ExtensionUntrustedWorkspaceSupportType) => {
606
if (!extension.local) {
607
return false;
608
}
609
610
const enablementState = this.extensionEnablementService.getEnablementState(extension.local);
611
if (enablementState !== EnablementState.EnabledGlobally && enablementState !== EnablementState.EnabledWorkspace &&
612
enablementState !== EnablementState.DisabledByTrustRequirement && enablementState !== EnablementState.DisabledByExtensionDependency) {
613
return false;
614
}
615
616
if (this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === supportType) {
617
return true;
618
}
619
620
if (supportType === false) {
621
const dependencies = getExtensionDependencies(local.map(ext => ext.local!), extension.local);
622
return dependencies.some(ext => this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(ext.manifest) === supportType);
623
}
624
625
return false;
626
};
627
628
const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace());
629
const inRestrictedWorkspace = !this.workspaceTrustManagementService.isWorkspaceTrusted();
630
631
if (type === 'virtual') {
632
// show limited and disabled extensions unless disabled because of a untrusted workspace
633
local = local.filter(extension => inVirtualWorkspace && hasVirtualSupportType(extension, partial ? 'limited' : false) && !(inRestrictedWorkspace && hasRestrictedSupportType(extension, false)));
634
} else if (type === 'untrusted') {
635
// show limited and disabled extensions unless disabled because of a virtual workspace
636
local = local.filter(extension => hasRestrictedSupportType(extension, partial ? 'limited' : false) && !(inVirtualWorkspace && hasVirtualSupportType(extension, false)));
637
} else {
638
// show extensions that are restricted or disabled in the current workspace
639
local = local.filter(extension => inVirtualWorkspace && !hasVirtualSupportType(extension, true) || inRestrictedWorkspace && !hasRestrictedSupportType(extension, true));
640
}
641
return this.sortExtensions(local, options);
642
}
643
644
private async filterDeprecatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): Promise<IExtension[]> {
645
const value = query.value.replace(/@deprecated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
646
const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest();
647
const deprecatedExtensionIds = Object.keys(extensionsControlManifest.deprecated);
648
local = local.filter(e => deprecatedExtensionIds.includes(e.identifier.id) && (!value || e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1));
649
return this.sortExtensions(local, options);
650
}
651
652
private filterRecentlyUpdatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
653
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
654
const currentTime = Date.now();
655
local = local.filter(e => !e.isBuiltin && !e.outdated && e.local?.updated && e.local?.installedTimestamp !== undefined && currentTime - e.local.installedTimestamp < ExtensionsListView.RECENT_UPDATE_DURATION);
656
657
value = value.replace(/@recentlyUpdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
658
659
const result = local.filter(e =>
660
(e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
661
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));
662
663
options.sortBy = options.sortBy ?? LocalSortBy.UpdateDate;
664
665
return this.sortExtensions(result, options);
666
}
667
668
private filterExtensionsByFeature(local: IExtension[], query: Query): { extensions: IExtension[]; description: string } | undefined {
669
const value = query.value.replace(/@feature:/g, '').trim();
670
const featureId = value.split(' ')[0];
671
const feature = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(featureId);
672
if (!feature) {
673
return undefined;
674
}
675
if (this.extensionsViewState) {
676
this.extensionsViewState.filters.featureId = featureId;
677
}
678
const renderer = feature.renderer ? this.instantiationService.createInstance<IExtensionFeatureRenderer>(feature.renderer) : undefined;
679
try {
680
const result: [IExtension, number][] = [];
681
for (const e of local) {
682
if (!e.local) {
683
continue;
684
}
685
const accessData = this.extensionFeaturesManagementService.getAccessData(new ExtensionIdentifier(e.identifier.id), featureId);
686
const shouldRender = renderer?.shouldRender(e.local.manifest);
687
if (accessData || shouldRender) {
688
result.push([e, accessData?.accessTimes.length ?? 0]);
689
}
690
}
691
return {
692
extensions: result.sort(([, a], [, b]) => b - a).map(([e]) => e),
693
description: localize('showingExtensionsForFeature', "Extensions using {0} in the last 30 days", feature.label)
694
};
695
} finally {
696
renderer?.dispose();
697
}
698
}
699
700
private mergeAddedExtensions(extensions: IExtension[], newExtensions: IExtension[]): IExtension[] | undefined {
701
const oldExtensions = [...extensions];
702
const findPreviousExtensionIndex = (from: number): number => {
703
let index = -1;
704
const previousExtensionInNew = newExtensions[from];
705
if (previousExtensionInNew) {
706
index = oldExtensions.findIndex(e => areSameExtensions(e.identifier, previousExtensionInNew.identifier));
707
if (index === -1) {
708
return findPreviousExtensionIndex(from - 1);
709
}
710
}
711
return index;
712
};
713
714
let hasChanged: boolean = false;
715
for (let index = 0; index < newExtensions.length; index++) {
716
const extension = newExtensions[index];
717
if (extensions.every(r => !areSameExtensions(r.identifier, extension.identifier))) {
718
hasChanged = true;
719
extensions.splice(findPreviousExtensionIndex(index - 1) + 1, 0, extension);
720
}
721
}
722
723
return hasChanged ? extensions : undefined;
724
}
725
726
private async queryGallery(query: Query, options: IGalleryQueryOptions, token: CancellationToken): Promise<IQueryResult> {
727
const hasUserDefinedSortOrder = options.sortBy !== undefined;
728
if (!hasUserDefinedSortOrder && !query.value.trim()) {
729
options.sortBy = GallerySortBy.InstallCount;
730
}
731
732
if (this.isRecommendationsQuery(query)) {
733
const model = await this.queryRecommendations(query, options, token);
734
return { model, disposables: new DisposableStore() };
735
}
736
737
const text = query.value;
738
739
if (!text) {
740
options.source = 'viewlet';
741
const pager = await this.extensionsWorkbenchService.queryGallery(options, token);
742
return { model: new PagedModel(pager), disposables: new DisposableStore() };
743
}
744
745
if (/\bext:([^\s]+)\b/g.test(text)) {
746
options.text = text;
747
options.source = 'file-extension-tags';
748
const pager = await this.extensionsWorkbenchService.queryGallery(options, token);
749
return { model: new PagedModel(pager), disposables: new DisposableStore() };
750
}
751
752
options.text = text.substring(0, 350);
753
options.source = 'searchText';
754
755
if (hasUserDefinedSortOrder || /\b(category|tag):([^\s]+)\b/gi.test(text) || /\bfeatured(\s+|\b|$)/gi.test(text)) {
756
const pager = await this.extensionsWorkbenchService.queryGallery(options, token);
757
return { model: new PagedModel(pager), disposables: new DisposableStore() };
758
}
759
760
try {
761
const [pager, preferredExtensions] = await Promise.all([
762
this.extensionsWorkbenchService.queryGallery(options, token),
763
this.getPreferredExtensions(options.text.toLowerCase(), token).catch(() => [])
764
]);
765
766
const model = preferredExtensions.length ? new PreferredExtensionsPagedModel(preferredExtensions, pager) : new PagedModel(pager);
767
return { model, disposables: new DisposableStore() };
768
} catch (error) {
769
if (isCancellationError(error)) {
770
throw error;
771
}
772
773
if (!(error instanceof ExtensionGalleryError)) {
774
throw error;
775
}
776
777
const searchText = options.text.toLowerCase();
778
const localExtensions = this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && (e.name.toLowerCase().indexOf(searchText) > -1 || e.displayName.toLowerCase().indexOf(searchText) > -1 || e.description.toLowerCase().indexOf(searchText) > -1));
779
if (localExtensions.length) {
780
const message = this.getMessage(error);
781
return { model: new PagedModel(localExtensions), disposables: new DisposableStore(), message: { text: localize('showing local extensions only', "{0} Showing local extensions.", message.text), severity: message.severity } };
782
}
783
784
throw error;
785
}
786
}
787
788
private async getPreferredExtensions(searchText: string, token: CancellationToken): Promise<IExtension[]> {
789
const preferredExtensions = this.extensionsWorkbenchService.local.filter(e => !e.isBuiltin && (e.name.toLowerCase().indexOf(searchText) > -1 || e.displayName.toLowerCase().indexOf(searchText) > -1 || e.description.toLowerCase().indexOf(searchText) > -1));
790
const preferredExtensionUUIDs = new Set<string>();
791
792
if (preferredExtensions.length) {
793
// Update gallery data for preferred extensions if they are not yet fetched
794
const extesionsToFetch: IExtensionIdentifier[] = [];
795
for (const extension of preferredExtensions) {
796
if (extension.identifier.uuid) {
797
preferredExtensionUUIDs.add(extension.identifier.uuid);
798
}
799
if (!extension.gallery && extension.identifier.uuid) {
800
extesionsToFetch.push(extension.identifier);
801
}
802
}
803
if (extesionsToFetch.length) {
804
this.extensionsWorkbenchService.getExtensions(extesionsToFetch, CancellationToken.None).catch(e => null/*ignore error*/);
805
}
806
}
807
808
const preferredResults: string[] = [];
809
try {
810
const manifest = await this.extensionManagementService.getExtensionsControlManifest();
811
if (Array.isArray(manifest.search)) {
812
for (const s of manifest.search) {
813
if (s.query && s.query.toLowerCase() === searchText && Array.isArray(s.preferredResults)) {
814
preferredResults.push(...s.preferredResults);
815
break;
816
}
817
}
818
}
819
if (preferredResults.length) {
820
const result = await this.extensionsWorkbenchService.getExtensions(preferredResults.map(id => ({ id })), token);
821
for (const extension of result) {
822
if (extension.identifier.uuid && !preferredExtensionUUIDs.has(extension.identifier.uuid)) {
823
preferredExtensions.push(extension);
824
}
825
}
826
}
827
} catch (e) {
828
this.logService.warn('Failed to get preferred results from the extensions control manifest.', e);
829
}
830
831
return preferredExtensions;
832
}
833
834
private sortExtensions(extensions: IExtension[], options: IQueryOptions): IExtension[] {
835
switch (options.sortBy) {
836
case GallerySortBy.InstallCount:
837
extensions = extensions.sort((e1, e2) => typeof e2.installCount === 'number' && typeof e1.installCount === 'number' ? e2.installCount - e1.installCount : NaN);
838
break;
839
case LocalSortBy.UpdateDate:
840
extensions = extensions.sort((e1, e2) =>
841
typeof e2.local?.installedTimestamp === 'number' && typeof e1.local?.installedTimestamp === 'number' ? e2.local.installedTimestamp - e1.local.installedTimestamp :
842
typeof e2.local?.installedTimestamp === 'number' ? 1 :
843
typeof e1.local?.installedTimestamp === 'number' ? -1 : NaN);
844
break;
845
case GallerySortBy.AverageRating:
846
case GallerySortBy.WeightedRating:
847
extensions = extensions.sort((e1, e2) => typeof e2.rating === 'number' && typeof e1.rating === 'number' ? e2.rating - e1.rating : NaN);
848
break;
849
default:
850
extensions = extensions.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName));
851
break;
852
}
853
if (options.sortOrder === SortOrder.Descending) {
854
extensions = extensions.reverse();
855
}
856
return extensions;
857
}
858
859
private isRecommendationsQuery(query: Query): boolean {
860
return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)
861
|| ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)
862
|| ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value)
863
|| ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)
864
|| ExtensionsListView.isRemoteRecommendedExtensionsQuery(query.value)
865
|| /@recommended:all/i.test(query.value)
866
|| ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)
867
|| ExtensionsListView.isRecommendedExtensionsQuery(query.value);
868
}
869
870
private async queryRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
871
// Workspace recommendations
872
if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {
873
return this.getWorkspaceRecommendationsModel(query, options, token);
874
}
875
876
// Keymap recommendations
877
if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
878
return this.getKeymapRecommendationsModel(query, options, token);
879
}
880
881
// Language recommendations
882
if (ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value)) {
883
return this.getLanguageRecommendationsModel(query, options, token);
884
}
885
886
// Exe recommendations
887
if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) {
888
return this.getExeRecommendationsModel(query, options, token);
889
}
890
891
// Remote recommendations
892
if (ExtensionsListView.isRemoteRecommendedExtensionsQuery(query.value)) {
893
return this.getRemoteRecommendationsModel(query, options, token);
894
}
895
896
// All recommendations
897
if (/@recommended:all/i.test(query.value)) {
898
return this.getAllRecommendationsModel(options, token);
899
}
900
901
// Search recommendations
902
if (ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) ||
903
(ExtensionsListView.isRecommendedExtensionsQuery(query.value) && options.sortBy !== undefined)) {
904
return this.searchRecommendations(query, options, token);
905
}
906
907
// Other recommendations
908
if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
909
return this.getOtherRecommendationsModel(query, options, token);
910
}
911
912
return new PagedModel([]);
913
}
914
915
protected async getInstallableRecommendations(recommendations: Array<string | URI>, options: IQueryOptions, token: CancellationToken): Promise<IExtension[]> {
916
const result: IExtension[] = [];
917
if (recommendations.length) {
918
const galleryExtensions: string[] = [];
919
const resourceExtensions: URI[] = [];
920
for (const recommendation of recommendations) {
921
if (typeof recommendation === 'string') {
922
galleryExtensions.push(recommendation);
923
} else {
924
resourceExtensions.push(recommendation);
925
}
926
}
927
if (galleryExtensions.length) {
928
try {
929
const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: options.source }, token);
930
for (const extension of extensions) {
931
if (extension.gallery && !extension.deprecationInfo
932
&& await this.extensionManagementService.canInstall(extension.gallery) === true) {
933
result.push(extension);
934
}
935
}
936
} catch (error) {
937
if (!resourceExtensions.length || !this.isOfflineError(error)) {
938
throw error;
939
}
940
}
941
}
942
if (resourceExtensions.length) {
943
const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true);
944
for (const extension of extensions) {
945
if (await this.extensionsWorkbenchService.canInstall(extension) === true) {
946
result.push(extension);
947
}
948
}
949
}
950
}
951
return result;
952
}
953
954
protected async getWorkspaceRecommendations(): Promise<Array<string | URI>> {
955
const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations();
956
const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations();
957
for (const configBasedRecommendation of important) {
958
if (!recommendations.find(extensionId => extensionId === configBasedRecommendation)) {
959
recommendations.push(configBasedRecommendation);
960
}
961
}
962
return recommendations;
963
}
964
965
private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
966
const recommendations = await this.getWorkspaceRecommendations();
967
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token));
968
return new PagedModel(installableRecommendations);
969
}
970
971
private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
972
const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase();
973
const recommendations = this.extensionRecommendationsService.getKeymapRecommendations();
974
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-keymaps' }, token))
975
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
976
return new PagedModel(installableRecommendations);
977
}
978
979
private async getLanguageRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
980
const value = query.value.replace(/@recommended:languages/g, '').trim().toLowerCase();
981
const recommendations = this.extensionRecommendationsService.getLanguageRecommendations();
982
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-languages' }, token))
983
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
984
return new PagedModel(installableRecommendations);
985
}
986
987
private async getRemoteRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
988
const value = query.value.replace(/@recommended:remotes/g, '').trim().toLowerCase();
989
const recommendations = this.extensionRecommendationsService.getRemoteRecommendations();
990
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-remotes' }, token))
991
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
992
return new PagedModel(installableRecommendations);
993
}
994
995
private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
996
const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase();
997
const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe);
998
const installableRecommendations = await this.getInstallableRecommendations([...important, ...others], { ...options, source: 'recommendations-exe' }, token);
999
return new PagedModel(installableRecommendations);
1000
}
1001
1002
private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
1003
const otherRecommendations = await this.getOtherRecommendations();
1004
const installableRecommendations = await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token);
1005
const result = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
1006
return new PagedModel(result);
1007
}
1008
1009
private async getOtherRecommendations(): Promise<string[]> {
1010
const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server))
1011
.map(e => e.identifier.id.toLowerCase());
1012
const workspaceRecommendations = (await this.getWorkspaceRecommendations())
1013
.map(extensionId => isString(extensionId) ? extensionId.toLowerCase() : extensionId);
1014
1015
return distinct(
1016
(await Promise.all([
1017
// Order is important
1018
this.extensionRecommendationsService.getImportantRecommendations(),
1019
this.extensionRecommendationsService.getFileBasedRecommendations(),
1020
this.extensionRecommendationsService.getOtherRecommendations()
1021
])).flat().filter(extensionId => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase())
1022
), extensionId => extensionId.toLowerCase());
1023
}
1024
1025
// Get All types of recommendations, trimmed to show a max of 8 at any given time
1026
private async getAllRecommendationsModel(options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
1027
const localExtensions = await this.extensionsWorkbenchService.queryLocal(this.options.server);
1028
const localExtensionIds = localExtensions.map(e => e.identifier.id.toLowerCase());
1029
1030
const allRecommendations = distinct(
1031
(await Promise.all([
1032
// Order is important
1033
this.getWorkspaceRecommendations(),
1034
this.extensionRecommendationsService.getImportantRecommendations(),
1035
this.extensionRecommendationsService.getFileBasedRecommendations(),
1036
this.extensionRecommendationsService.getOtherRecommendations()
1037
])).flat().filter(extensionId => {
1038
if (isString(extensionId)) {
1039
return !localExtensionIds.includes(extensionId.toLowerCase());
1040
}
1041
return !localExtensions.some(localExtension => localExtension.local && this.uriIdentityService.extUri.isEqual(localExtension.local.location, extensionId));
1042
}));
1043
1044
const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token);
1045
1046
const result: IExtension[] = [];
1047
for (let i = 0; i < installableRecommendations.length && result.length < 8; i++) {
1048
const recommendation = allRecommendations[i];
1049
if (isString(recommendation)) {
1050
const extension = installableRecommendations.find(extension => areSameExtensions(extension.identifier, { id: recommendation }));
1051
if (extension) {
1052
result.push(extension);
1053
}
1054
} else {
1055
const extension = installableRecommendations.find(extension => extension.resourceExtension && this.uriIdentityService.extUri.isEqual(extension.resourceExtension.location, recommendation));
1056
if (extension) {
1057
result.push(extension);
1058
}
1059
}
1060
}
1061
1062
return new PagedModel(result);
1063
}
1064
1065
private async searchRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
1066
const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
1067
const recommendations = distinct([...await this.getWorkspaceRecommendations(), ...await this.getOtherRecommendations()]);
1068
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations', sortBy: undefined }, token))
1069
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
1070
return new PagedModel(this.sortExtensions(installableRecommendations, options));
1071
}
1072
1073
private setModel(model: IPagedModel<IExtension>, message?: Message, donotResetScrollTop?: boolean) {
1074
if (this.list) {
1075
this.list.model = new DelayedPagedModel(model);
1076
this.updateBody(message);
1077
if (!donotResetScrollTop) {
1078
this.list.scrollTop = 0;
1079
}
1080
}
1081
if (this.badge) {
1082
this.badge.setCount(this.count());
1083
}
1084
}
1085
1086
private updateModel(model: IPagedModel<IExtension>) {
1087
if (this.list) {
1088
this.list.model = new DelayedPagedModel(model);
1089
this.updateBody();
1090
}
1091
if (this.badge) {
1092
this.badge.setCount(this.count());
1093
}
1094
}
1095
1096
private updateBody(message?: Message): void {
1097
if (this.bodyTemplate) {
1098
1099
const count = this.count();
1100
this.bodyTemplate.extensionsList.classList.toggle('hidden', count === 0);
1101
this.bodyTemplate.messageContainer.classList.toggle('hidden', !message && count > 0);
1102
1103
if (this.isBodyVisible()) {
1104
if (message) {
1105
this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(message.severity);
1106
this.bodyTemplate.messageBox.textContent = message.text;
1107
} else if (this.count() === 0) {
1108
this.bodyTemplate.messageSeverityIcon.className = '';
1109
this.bodyTemplate.messageBox.textContent = localize('no extensions found', "No extensions found.");
1110
}
1111
if (this.bodyTemplate.messageBox.textContent) {
1112
alert(this.bodyTemplate.messageBox.textContent);
1113
}
1114
}
1115
}
1116
1117
this.updateSize();
1118
}
1119
1120
private getMessage(error: any): Message {
1121
if (this.isOfflineError(error)) {
1122
return { text: localize('offline error', "Unable to search the Marketplace when offline, please check your network connection."), severity: Severity.Warning };
1123
} else {
1124
return { text: localize('error', "Error while fetching extensions. {0}", getErrorMessage(error)), severity: Severity.Error };
1125
}
1126
}
1127
1128
private isOfflineError(error: Error): boolean {
1129
if (error instanceof ExtensionGalleryError) {
1130
return error.code === ExtensionGalleryErrorCode.Offline;
1131
}
1132
return isOfflineError(error);
1133
}
1134
1135
protected updateSize() {
1136
if (this.options.flexibleHeight) {
1137
this.maximumBodySize = this.list?.model.length ? Number.POSITIVE_INFINITY : 0;
1138
this.storageService.store(`${this.id}.size`, this.list?.model.length || 0, StorageScope.PROFILE, StorageTarget.MACHINE);
1139
}
1140
}
1141
1142
override dispose(): void {
1143
super.dispose();
1144
if (this.queryRequest) {
1145
this.queryRequest.request.cancel();
1146
this.queryRequest = null;
1147
}
1148
if (this.queryResult) {
1149
this.queryResult.disposables.dispose();
1150
this.queryResult = undefined;
1151
}
1152
this.list = null;
1153
}
1154
1155
static isLocalExtensionsQuery(query: string, sortBy?: string): boolean {
1156
return this.isInstalledExtensionsQuery(query)
1157
|| this.isSearchInstalledExtensionsQuery(query)
1158
|| this.isOutdatedExtensionsQuery(query)
1159
|| this.isEnabledExtensionsQuery(query)
1160
|| this.isDisabledExtensionsQuery(query)
1161
|| this.isBuiltInExtensionsQuery(query)
1162
|| this.isSearchBuiltInExtensionsQuery(query)
1163
|| this.isBuiltInGroupExtensionsQuery(query)
1164
|| this.isSearchDeprecatedExtensionsQuery(query)
1165
|| this.isSearchWorkspaceUnsupportedExtensionsQuery(query)
1166
|| this.isSearchRecentlyUpdatedQuery(query)
1167
|| this.isSearchExtensionUpdatesQuery(query)
1168
|| this.isSortInstalledExtensionsQuery(query, sortBy)
1169
|| this.isFeatureExtensionsQuery(query);
1170
}
1171
1172
static isSearchBuiltInExtensionsQuery(query: string): boolean {
1173
return /@builtin\s.+|.+\s@builtin/i.test(query);
1174
}
1175
1176
static isBuiltInExtensionsQuery(query: string): boolean {
1177
return /^@builtin$/i.test(query.trim());
1178
}
1179
1180
static isBuiltInGroupExtensionsQuery(query: string): boolean {
1181
return /^@builtin:.+$/i.test(query.trim());
1182
}
1183
1184
static isSearchWorkspaceUnsupportedExtensionsQuery(query: string): boolean {
1185
return /^\s*@workspaceUnsupported(:(untrusted|virtual)(Partial)?)?(\s|$)/i.test(query);
1186
}
1187
1188
static isInstalledExtensionsQuery(query: string): boolean {
1189
return /@installed$/i.test(query);
1190
}
1191
1192
static isSearchInstalledExtensionsQuery(query: string): boolean {
1193
return /@installed\s./i.test(query) || this.isFeatureExtensionsQuery(query);
1194
}
1195
1196
static isOutdatedExtensionsQuery(query: string): boolean {
1197
return /@outdated/i.test(query);
1198
}
1199
1200
static isEnabledExtensionsQuery(query: string): boolean {
1201
return /@enabled/i.test(query) && !/@builtin/i.test(query);
1202
}
1203
1204
static isDisabledExtensionsQuery(query: string): boolean {
1205
return /@disabled/i.test(query) && !/@builtin/i.test(query);
1206
}
1207
1208
static isSearchDeprecatedExtensionsQuery(query: string): boolean {
1209
return /@deprecated\s?.*/i.test(query);
1210
}
1211
1212
static isRecommendedExtensionsQuery(query: string): boolean {
1213
return /^@recommended$/i.test(query.trim());
1214
}
1215
1216
static isSearchRecommendedExtensionsQuery(query: string): boolean {
1217
return /@recommended\s.+/i.test(query);
1218
}
1219
1220
static isWorkspaceRecommendedExtensionsQuery(query: string): boolean {
1221
return /@recommended:workspace/i.test(query);
1222
}
1223
1224
static isExeRecommendedExtensionsQuery(query: string): boolean {
1225
return /@exe:.+/i.test(query);
1226
}
1227
1228
static isRemoteRecommendedExtensionsQuery(query: string): boolean {
1229
return /@recommended:remotes/i.test(query);
1230
}
1231
1232
static isKeymapsRecommendedExtensionsQuery(query: string): boolean {
1233
return /@recommended:keymaps/i.test(query);
1234
}
1235
1236
static isLanguageRecommendedExtensionsQuery(query: string): boolean {
1237
return /@recommended:languages/i.test(query);
1238
}
1239
1240
static isSortInstalledExtensionsQuery(query: string, sortBy?: string): boolean {
1241
return (sortBy !== undefined && sortBy !== '' && query === '') || (!sortBy && /^@sort:\S*$/i.test(query));
1242
}
1243
1244
static isSearchPopularQuery(query: string): boolean {
1245
return /@popular/i.test(query);
1246
}
1247
1248
static isSearchRecentlyPublishedQuery(query: string): boolean {
1249
return /@recentlyPublished/i.test(query);
1250
}
1251
1252
static isSearchRecentlyUpdatedQuery(query: string): boolean {
1253
return /@recentlyUpdated/i.test(query);
1254
}
1255
1256
static isSearchExtensionUpdatesQuery(query: string): boolean {
1257
return /@updates/i.test(query);
1258
}
1259
1260
static isSortUpdateDateQuery(query: string): boolean {
1261
return /@sort:updateDate/i.test(query);
1262
}
1263
1264
static isFeatureExtensionsQuery(query: string): boolean {
1265
return /@feature:/i.test(query);
1266
}
1267
1268
override focus(): void {
1269
super.focus();
1270
if (!this.list) {
1271
return;
1272
}
1273
1274
if (!(this.list.getFocus().length || this.list.getSelection().length)) {
1275
this.list.focusNext();
1276
}
1277
this.list.domFocus();
1278
}
1279
}
1280
1281
export class DefaultPopularExtensionsView extends ExtensionsListView {
1282
1283
override async show(): Promise<IPagedModel<IExtension>> {
1284
const query = this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer ? '@web' : '';
1285
return super.show(query);
1286
}
1287
1288
}
1289
1290
export class ServerInstalledExtensionsView extends ExtensionsListView {
1291
1292
override async show(query: string): Promise<IPagedModel<IExtension>> {
1293
query = query ? query : '@installed';
1294
if (!ExtensionsListView.isLocalExtensionsQuery(query) || ExtensionsListView.isSortInstalledExtensionsQuery(query)) {
1295
query = query += ' @installed';
1296
}
1297
return super.show(query.trim());
1298
}
1299
1300
}
1301
1302
export class EnabledExtensionsView extends ExtensionsListView {
1303
1304
override async show(query: string): Promise<IPagedModel<IExtension>> {
1305
query = query || '@enabled';
1306
return ExtensionsListView.isEnabledExtensionsQuery(query) ? super.show(query) :
1307
ExtensionsListView.isSortInstalledExtensionsQuery(query) ? super.show('@enabled ' + query) : this.showEmptyModel();
1308
}
1309
}
1310
1311
export class DisabledExtensionsView extends ExtensionsListView {
1312
1313
override async show(query: string): Promise<IPagedModel<IExtension>> {
1314
query = query || '@disabled';
1315
return ExtensionsListView.isDisabledExtensionsQuery(query) ? super.show(query) :
1316
ExtensionsListView.isSortInstalledExtensionsQuery(query) ? super.show('@disabled ' + query) : this.showEmptyModel();
1317
}
1318
}
1319
1320
export class OutdatedExtensionsView extends ExtensionsListView {
1321
1322
override async show(query: string): Promise<IPagedModel<IExtension>> {
1323
query = query ? query : '@outdated';
1324
if (ExtensionsListView.isSearchExtensionUpdatesQuery(query)) {
1325
query = query.replace('@updates', '@outdated');
1326
}
1327
return super.show(query.trim());
1328
}
1329
1330
protected override updateSize() {
1331
super.updateSize();
1332
this.setExpanded(this.count() > 0);
1333
}
1334
1335
}
1336
1337
export class RecentlyUpdatedExtensionsView extends ExtensionsListView {
1338
1339
override async show(query: string): Promise<IPagedModel<IExtension>> {
1340
query = query ? query : '@recentlyUpdated';
1341
if (ExtensionsListView.isSearchExtensionUpdatesQuery(query)) {
1342
query = query.replace('@updates', '@recentlyUpdated');
1343
}
1344
return super.show(query.trim());
1345
}
1346
1347
}
1348
1349
export interface StaticQueryExtensionsViewOptions extends ExtensionsListViewOptions {
1350
readonly query: string;
1351
}
1352
1353
export class StaticQueryExtensionsView extends ExtensionsListView {
1354
1355
constructor(
1356
protected override readonly options: StaticQueryExtensionsViewOptions,
1357
viewletViewOptions: IViewletViewOptions,
1358
@INotificationService notificationService: INotificationService,
1359
@IKeybindingService keybindingService: IKeybindingService,
1360
@IContextMenuService contextMenuService: IContextMenuService,
1361
@IInstantiationService instantiationService: IInstantiationService,
1362
@IThemeService themeService: IThemeService,
1363
@IExtensionService extensionService: IExtensionService,
1364
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
1365
@IExtensionRecommendationsService extensionRecommendationsService: IExtensionRecommendationsService,
1366
@ITelemetryService telemetryService: ITelemetryService,
1367
@IHoverService hoverService: IHoverService,
1368
@IConfigurationService configurationService: IConfigurationService,
1369
@IWorkspaceContextService contextService: IWorkspaceContextService,
1370
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
1371
@IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService,
1372
@IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService,
1373
@IWorkspaceContextService workspaceService: IWorkspaceContextService,
1374
@IProductService productService: IProductService,
1375
@IContextKeyService contextKeyService: IContextKeyService,
1376
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
1377
@IOpenerService openerService: IOpenerService,
1378
@IStorageService storageService: IStorageService,
1379
@IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService,
1380
@IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService,
1381
@IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService,
1382
@IUriIdentityService uriIdentityService: IUriIdentityService,
1383
@ILogService logService: ILogService
1384
) {
1385
super(options, viewletViewOptions, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService,
1386
extensionsWorkbenchService, extensionRecommendationsService, telemetryService, hoverService, configurationService, contextService, extensionManagementServerService,
1387
extensionManifestPropertiesService, extensionManagementService, workspaceService, productService, contextKeyService, viewDescriptorService, openerService,
1388
storageService, workspaceTrustManagementService, extensionEnablementService, extensionFeaturesManagementService,
1389
uriIdentityService, logService);
1390
}
1391
1392
override show(): Promise<IPagedModel<IExtension>> {
1393
return super.show(this.options.query);
1394
}
1395
}
1396
1397
function toSpecificWorkspaceUnsupportedQuery(query: string, qualifier: string): string | undefined {
1398
if (!query) {
1399
return '@workspaceUnsupported:' + qualifier;
1400
}
1401
const match = query.match(new RegExp(`@workspaceUnsupported(:${qualifier})?(\\s|$)`, 'i'));
1402
if (match) {
1403
if (!match[1]) {
1404
return query.replace(/@workspaceUnsupported/gi, '@workspaceUnsupported:' + qualifier);
1405
}
1406
return query;
1407
}
1408
return undefined;
1409
}
1410
1411
1412
export class UntrustedWorkspaceUnsupportedExtensionsView extends ExtensionsListView {
1413
override async show(query: string): Promise<IPagedModel<IExtension>> {
1414
const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrusted');
1415
return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();
1416
}
1417
}
1418
1419
export class UntrustedWorkspacePartiallySupportedExtensionsView extends ExtensionsListView {
1420
override async show(query: string): Promise<IPagedModel<IExtension>> {
1421
const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrustedPartial');
1422
return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();
1423
}
1424
}
1425
1426
export class VirtualWorkspaceUnsupportedExtensionsView extends ExtensionsListView {
1427
override async show(query: string): Promise<IPagedModel<IExtension>> {
1428
const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtual');
1429
return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();
1430
}
1431
}
1432
1433
export class VirtualWorkspacePartiallySupportedExtensionsView extends ExtensionsListView {
1434
override async show(query: string): Promise<IPagedModel<IExtension>> {
1435
const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtualPartial');
1436
return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();
1437
}
1438
}
1439
1440
export class DeprecatedExtensionsView extends ExtensionsListView {
1441
override async show(query: string): Promise<IPagedModel<IExtension>> {
1442
return ExtensionsListView.isSearchDeprecatedExtensionsQuery(query) ? super.show(query) : this.showEmptyModel();
1443
}
1444
}
1445
1446
export class SearchMarketplaceExtensionsView extends ExtensionsListView {
1447
1448
private readonly reportSearchFinishedDelayer = this._register(new ThrottledDelayer(2000));
1449
private searchWaitPromise: Promise<void> = Promise.resolve();
1450
1451
override async show(query: string): Promise<IPagedModel<IExtension>> {
1452
const queryPromise = super.show(query);
1453
this.reportSearchFinishedDelayer.trigger(() => this.reportSearchFinished());
1454
this.searchWaitPromise = queryPromise.then(null, null);
1455
return queryPromise;
1456
}
1457
1458
private async reportSearchFinished(): Promise<void> {
1459
await this.searchWaitPromise;
1460
this.telemetryService.publicLog2('extensionsView:MarketplaceSearchFinished');
1461
}
1462
}
1463
1464
export class DefaultRecommendedExtensionsView extends ExtensionsListView {
1465
private readonly recommendedExtensionsQuery = '@recommended:all';
1466
1467
protected override renderBody(container: HTMLElement): void {
1468
super.renderBody(container);
1469
1470
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => {
1471
this.show('');
1472
}));
1473
}
1474
1475
override async show(query: string): Promise<IPagedModel<IExtension>> {
1476
if (query && query.trim() !== this.recommendedExtensionsQuery) {
1477
return this.showEmptyModel();
1478
}
1479
const model = await super.show(this.recommendedExtensionsQuery);
1480
if (!this.extensionsWorkbenchService.local.some(e => !e.isBuiltin)) {
1481
// This is part of popular extensions view. Collapse if no installed extensions.
1482
this.setExpanded(model.length > 0);
1483
}
1484
return model;
1485
}
1486
1487
}
1488
1489
export class RecommendedExtensionsView extends ExtensionsListView {
1490
private readonly recommendedExtensionsQuery = '@recommended';
1491
1492
protected override renderBody(container: HTMLElement): void {
1493
super.renderBody(container);
1494
1495
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => {
1496
this.show('');
1497
}));
1498
}
1499
1500
override async show(query: string): Promise<IPagedModel<IExtension>> {
1501
return (query && query.trim() !== this.recommendedExtensionsQuery) ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery);
1502
}
1503
}
1504
1505
export class WorkspaceRecommendedExtensionsView extends ExtensionsListView implements IWorkspaceRecommendedExtensionsView {
1506
private readonly recommendedExtensionsQuery = '@recommended:workspace';
1507
1508
protected override renderBody(container: HTMLElement): void {
1509
super.renderBody(container);
1510
1511
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.show(this.recommendedExtensionsQuery)));
1512
this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery)));
1513
}
1514
1515
override async show(query: string): Promise<IPagedModel<IExtension>> {
1516
const shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace';
1517
const model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery));
1518
this.setExpanded(model.length > 0);
1519
return model;
1520
}
1521
1522
private async getInstallableWorkspaceRecommendations(): Promise<IExtension[]> {
1523
const installed = (await this.extensionsWorkbenchService.queryLocal())
1524
.filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
1525
const recommendations = (await this.getWorkspaceRecommendations())
1526
.filter(recommendation => installed.every(local => isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.local?.location)));
1527
return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None);
1528
}
1529
1530
async installWorkspaceRecommendations(): Promise<void> {
1531
const installableRecommendations = await this.getInstallableWorkspaceRecommendations();
1532
if (installableRecommendations.length) {
1533
const galleryExtensions: InstallExtensionInfo[] = [];
1534
const resourceExtensions: IExtension[] = [];
1535
for (const recommendation of installableRecommendations) {
1536
if (recommendation.gallery) {
1537
galleryExtensions.push({ extension: recommendation.gallery, options: {} });
1538
} else {
1539
resourceExtensions.push(recommendation);
1540
}
1541
}
1542
await Promise.all([
1543
this.extensionManagementService.installGalleryExtensions(galleryExtensions),
1544
...resourceExtensions.map(extension => this.extensionsWorkbenchService.install(extension))
1545
]);
1546
} else {
1547
this.notificationService.notify({
1548
severity: Severity.Info,
1549
message: localize('no local extensions', "There are no extensions to install.")
1550
});
1551
}
1552
}
1553
1554
}
1555
1556
export class PreferredExtensionsPagedModel implements IPagedModel<IExtension> {
1557
1558
private readonly resolved = new Map<number, IExtension>();
1559
private preferredGalleryExtensions = new Set<string>();
1560
private resolvedGalleryExtensionsFromQuery: IExtension[] = [];
1561
private readonly pages: Array<{
1562
promise: Promise<void> | null;
1563
cts: CancellationTokenSource | null;
1564
promiseIndexes: Set<number>;
1565
}>;
1566
1567
public readonly length: number;
1568
1569
constructor(
1570
private readonly preferredExtensions: IExtension[],
1571
private readonly pager: IPager<IExtension>,
1572
) {
1573
for (let i = 0; i < this.preferredExtensions.length; i++) {
1574
this.resolved.set(i, this.preferredExtensions[i]);
1575
}
1576
1577
for (const e of preferredExtensions) {
1578
if (e.identifier.uuid) {
1579
this.preferredGalleryExtensions.add(e.identifier.uuid);
1580
}
1581
}
1582
1583
// expected that all preferred gallery extensions will be part of the query results
1584
this.length = (preferredExtensions.length - this.preferredGalleryExtensions.size) + this.pager.total;
1585
1586
const totalPages = Math.ceil(this.pager.total / this.pager.pageSize);
1587
this.populateResolvedExtensions(0, this.pager.firstPage);
1588
this.pages = range(totalPages - 1).map(() => ({
1589
promise: null,
1590
cts: null,
1591
promiseIndexes: new Set<number>(),
1592
}));
1593
}
1594
1595
isResolved(index: number): boolean {
1596
return this.resolved.has(index);
1597
}
1598
1599
get(index: number): IExtension {
1600
return this.resolved.get(index)!;
1601
}
1602
1603
async resolve(index: number, cancellationToken: CancellationToken): Promise<IExtension> {
1604
if (cancellationToken.isCancellationRequested) {
1605
throw new CancellationError();
1606
}
1607
1608
if (this.isResolved(index)) {
1609
return this.get(index);
1610
}
1611
1612
const indexInPagedModel = index - this.preferredExtensions.length + this.resolvedGalleryExtensionsFromQuery.length;
1613
const pageIndex = Math.floor(indexInPagedModel / this.pager.pageSize);
1614
const page = this.pages[pageIndex];
1615
1616
if (!page.promise) {
1617
page.cts = new CancellationTokenSource();
1618
page.promise = this.pager.getPage(pageIndex, page.cts.token)
1619
.then(extensions => this.populateResolvedExtensions(pageIndex, extensions))
1620
.catch(e => { page.promise = null; throw e; })
1621
.finally(() => page.cts = null);
1622
}
1623
1624
const listener = cancellationToken.onCancellationRequested(() => {
1625
if (!page.cts) {
1626
return;
1627
}
1628
page.promiseIndexes.delete(index);
1629
if (page.promiseIndexes.size === 0) {
1630
page.cts.cancel();
1631
}
1632
});
1633
1634
page.promiseIndexes.add(index);
1635
1636
try {
1637
await page.promise;
1638
} finally {
1639
listener.dispose();
1640
}
1641
1642
return this.get(index);
1643
}
1644
1645
private populateResolvedExtensions(pageIndex: number, extensions: IExtension[]): void {
1646
let adjustIndexOfNextPagesBy = 0;
1647
const pageStartIndex = pageIndex * this.pager.pageSize;
1648
for (let i = 0; i < extensions.length; i++) {
1649
const e = extensions[i];
1650
if (e.gallery?.identifier.uuid && this.preferredGalleryExtensions.has(e.gallery.identifier.uuid)) {
1651
this.resolvedGalleryExtensionsFromQuery.push(e);
1652
adjustIndexOfNextPagesBy++;
1653
} else {
1654
this.resolved.set(this.preferredExtensions.length - this.resolvedGalleryExtensionsFromQuery.length + pageStartIndex + i, e);
1655
}
1656
}
1657
// If this page has preferred gallery extensions, then adjust the index of the next pages
1658
// by the number of preferred gallery extensions found in this page. Because these preferred extensions
1659
// are already in the resolved list and since we did not add them now, we need to adjust the indices of the next pages.
1660
// Skip first page as the preferred extensions are always in the first page
1661
if (pageIndex !== 0 && adjustIndexOfNextPagesBy) {
1662
const nextPageStartIndex = (pageIndex + 1) * this.pager.pageSize;
1663
const indices = [...this.resolved.keys()].sort();
1664
for (const index of indices) {
1665
if (index >= nextPageStartIndex) {
1666
const e = this.resolved.get(index);
1667
if (e) {
1668
this.resolved.delete(index);
1669
this.resolved.set(index - adjustIndexOfNextPagesBy, e);
1670
}
1671
}
1672
}
1673
}
1674
}
1675
}
1676
1677