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
5259 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
readonly 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 (/@contribute:/i.test(query.value)) {
404
extensions = this.filterExtensionsByFeature(local, query);
405
}
406
407
else if (includeBuiltin) {
408
extensions = this.filterBuiltinExtensions(local, query, options);
409
}
410
411
return { extensions, canIncludeInstalledExtensions, description };
412
}
413
414
private filterBuiltinExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
415
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
416
value = value.replaceAll(/@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
417
418
const result = local
419
.filter(e => e.isBuiltin && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
420
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));
421
422
return this.sortExtensions(result, options);
423
}
424
425
private filterExtensionByCategory(e: IExtension, includedCategories: string[], excludedCategories: string[]): boolean {
426
if (!includedCategories.length && !excludedCategories.length) {
427
return true;
428
}
429
if (e.categories.length) {
430
if (excludedCategories.length && e.categories.some(category => excludedCategories.includes(category.toLowerCase()))) {
431
return false;
432
}
433
return e.categories.some(category => includedCategories.includes(category.toLowerCase()));
434
} else {
435
return includedCategories.includes(NONE_CATEGORY);
436
}
437
}
438
439
private parseCategories(value: string): { value: string; includedCategories: string[]; excludedCategories: string[] } {
440
const includedCategories: string[] = [];
441
const excludedCategories: string[] = [];
442
value = value.replace(/\bcategory:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedCategory, category) => {
443
const entry = (category || quotedCategory || '').toLowerCase();
444
if (entry.startsWith('-')) {
445
if (excludedCategories.indexOf(entry) === -1) {
446
excludedCategories.push(entry);
447
}
448
} else {
449
if (includedCategories.indexOf(entry) === -1) {
450
includedCategories.push(entry);
451
}
452
}
453
return '';
454
});
455
return { value, includedCategories, excludedCategories };
456
}
457
458
private filterInstalledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] {
459
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
460
461
value = value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
462
463
const matchingText = (e: IExtension) => (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1 || e.description.toLowerCase().indexOf(value) > -1)
464
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories);
465
let result;
466
467
if (options.sortBy !== undefined) {
468
result = local.filter(e => !e.isBuiltin && matchingText(e));
469
result = this.sortExtensions(result, options);
470
} else {
471
result = local.filter(e => (!e.isBuiltin || e.outdated || e.runtimeState !== undefined) && matchingText(e));
472
const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(e.identifier.value, e); return result; }, new ExtensionIdentifierMap<IExtensionDescription>());
473
474
const defaultSort = (e1: IExtension, e2: IExtension) => {
475
const running1 = runningExtensionsById.get(e1.identifier.id);
476
const isE1Running = !!running1 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running1)) === e1.server;
477
const running2 = runningExtensionsById.get(e2.identifier.id);
478
const isE2Running = running2 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running2)) === e2.server;
479
if ((isE1Running && isE2Running)) {
480
return e1.displayName.localeCompare(e2.displayName);
481
}
482
const isE1LanguagePackExtension = e1.local && isLanguagePackExtension(e1.local.manifest);
483
const isE2LanguagePackExtension = e2.local && isLanguagePackExtension(e2.local.manifest);
484
if (!isE1Running && !isE2Running) {
485
if (isE1LanguagePackExtension) {
486
return -1;
487
}
488
if (isE2LanguagePackExtension) {
489
return 1;
490
}
491
return e1.displayName.localeCompare(e2.displayName);
492
}
493
if ((isE1Running && isE2LanguagePackExtension) || (isE2Running && isE1LanguagePackExtension)) {
494
return e1.displayName.localeCompare(e2.displayName);
495
}
496
return isE1Running ? -1 : 1;
497
};
498
499
const incompatible: IExtension[] = [];
500
const deprecated: IExtension[] = [];
501
const outdated: IExtension[] = [];
502
const actionRequired: IExtension[] = [];
503
const noActionRequired: IExtension[] = [];
504
505
for (const e of result) {
506
if (e.enablementState === EnablementState.DisabledByInvalidExtension) {
507
incompatible.push(e);
508
}
509
else if (e.deprecationInfo) {
510
deprecated.push(e);
511
}
512
else if (e.outdated && this.extensionEnablementService.isEnabledEnablementState(e.enablementState)) {
513
outdated.push(e);
514
}
515
else if (e.runtimeState) {
516
actionRequired.push(e);
517
}
518
else {
519
noActionRequired.push(e);
520
}
521
}
522
523
result = [
524
...incompatible.sort(defaultSort),
525
...deprecated.sort(defaultSort),
526
...outdated.sort(defaultSort),
527
...actionRequired.sort(defaultSort),
528
...noActionRequired.sort(defaultSort)
529
];
530
}
531
return result;
532
}
533
534
private filterOutdatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
535
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
536
537
value = value.replace(/@outdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
538
539
const result = local
540
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
541
.filter(extension => extension.outdated
542
&& (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)
543
&& this.filterExtensionByCategory(extension, includedCategories, excludedCategories));
544
545
return this.sortExtensions(result, options);
546
}
547
548
private filterDisabledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions, includeBuiltin: boolean): IExtension[] {
549
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
550
551
value = value.replaceAll(/@disabled|@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
552
553
if (includeBuiltin) {
554
local = local.filter(e => e.isBuiltin);
555
}
556
const result = local
557
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
558
.filter(e => runningExtensions.every(r => !areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))
559
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
560
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));
561
562
return this.sortExtensions(result, options);
563
}
564
565
private filterEnabledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions, includeBuiltin: boolean): IExtension[] {
566
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
567
568
value = value ? value.replaceAll(/@enabled|@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase() : '';
569
570
local = local.filter(e => e.isBuiltin === includeBuiltin);
571
const result = local
572
.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))
573
.filter(e => runningExtensions.some(r => areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))
574
&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
575
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));
576
577
return this.sortExtensions(result, options);
578
}
579
580
private filterWorkspaceUnsupportedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
581
// shows local extensions which are restricted or disabled in the current workspace because of the extension's capability
582
583
const queryString = query.value; // @sortby is already filtered out
584
585
const match = queryString.match(/^\s*@workspaceUnsupported(?::(untrusted|virtual)(Partial)?)?(?:\s+([^\s]*))?/i);
586
if (!match) {
587
return [];
588
}
589
const type = match[1]?.toLowerCase();
590
const partial = !!match[2];
591
const nameFilter = match[3]?.toLowerCase();
592
593
if (nameFilter) {
594
local = local.filter(extension => extension.name.toLowerCase().indexOf(nameFilter) > -1 || extension.displayName.toLowerCase().indexOf(nameFilter) > -1);
595
}
596
597
const hasVirtualSupportType = (extension: IExtension, supportType: ExtensionVirtualWorkspaceSupportType) => {
598
return extension.local && this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.local.manifest) === supportType;
599
};
600
601
const hasRestrictedSupportType = (extension: IExtension, supportType: ExtensionUntrustedWorkspaceSupportType) => {
602
if (!extension.local) {
603
return false;
604
}
605
606
const enablementState = this.extensionEnablementService.getEnablementState(extension.local);
607
if (enablementState !== EnablementState.EnabledGlobally && enablementState !== EnablementState.EnabledWorkspace &&
608
enablementState !== EnablementState.DisabledByTrustRequirement && enablementState !== EnablementState.DisabledByExtensionDependency) {
609
return false;
610
}
611
612
if (this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === supportType) {
613
return true;
614
}
615
616
if (supportType === false) {
617
const dependencies = getExtensionDependencies(local.map(ext => ext.local!), extension.local);
618
return dependencies.some(ext => this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(ext.manifest) === supportType);
619
}
620
621
return false;
622
};
623
624
const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace());
625
const inRestrictedWorkspace = !this.workspaceTrustManagementService.isWorkspaceTrusted();
626
627
if (type === 'virtual') {
628
// show limited and disabled extensions unless disabled because of a untrusted workspace
629
local = local.filter(extension => inVirtualWorkspace && hasVirtualSupportType(extension, partial ? 'limited' : false) && !(inRestrictedWorkspace && hasRestrictedSupportType(extension, false)));
630
} else if (type === 'untrusted') {
631
// show limited and disabled extensions unless disabled because of a virtual workspace
632
local = local.filter(extension => hasRestrictedSupportType(extension, partial ? 'limited' : false) && !(inVirtualWorkspace && hasVirtualSupportType(extension, false)));
633
} else {
634
// show extensions that are restricted or disabled in the current workspace
635
local = local.filter(extension => inVirtualWorkspace && !hasVirtualSupportType(extension, true) || inRestrictedWorkspace && !hasRestrictedSupportType(extension, true));
636
}
637
return this.sortExtensions(local, options);
638
}
639
640
private async filterDeprecatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): Promise<IExtension[]> {
641
const value = query.value.replace(/@deprecated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
642
const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest();
643
const deprecatedExtensionIds = Object.keys(extensionsControlManifest.deprecated);
644
local = local.filter(e => deprecatedExtensionIds.includes(e.identifier.id) && (!value || e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1));
645
return this.sortExtensions(local, options);
646
}
647
648
private filterRecentlyUpdatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {
649
let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);
650
const currentTime = Date.now();
651
local = local.filter(e => !e.isBuiltin && !e.outdated && e.local?.updated && e.local?.installedTimestamp !== undefined && currentTime - e.local.installedTimestamp < ExtensionsListView.RECENT_UPDATE_DURATION);
652
653
value = value.replace(/@recentlyUpdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();
654
655
const result = local.filter(e =>
656
(e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)
657
&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));
658
659
options.sortBy = options.sortBy ?? LocalSortBy.UpdateDate;
660
661
return this.sortExtensions(result, options);
662
}
663
664
private filterExtensionsByFeature(local: IExtension[], query: Query): IExtension[] {
665
const value = query.value.replace(/@contribute:/g, '').trim();
666
const featureId = value.split(' ')[0];
667
const feature = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(featureId);
668
if (!feature) {
669
return [];
670
}
671
if (this.extensionsViewState) {
672
this.extensionsViewState.filters.featureId = featureId;
673
}
674
const renderer = feature.renderer ? this.instantiationService.createInstance<IExtensionFeatureRenderer>(feature.renderer) : undefined;
675
try {
676
const result: [IExtension, number][] = [];
677
for (const e of local) {
678
if (!e.local) {
679
continue;
680
}
681
const accessData = this.extensionFeaturesManagementService.getAccessData(new ExtensionIdentifier(e.identifier.id), featureId);
682
const shouldRender = renderer?.shouldRender(e.local.manifest);
683
if (accessData || shouldRender) {
684
result.push([e, accessData?.accessTimes.length ?? 0]);
685
}
686
}
687
return result.sort(([, a], [, b]) => b - a).map(([e]) => e);
688
} finally {
689
renderer?.dispose();
690
}
691
}
692
693
private mergeAddedExtensions(extensions: IExtension[], newExtensions: IExtension[]): IExtension[] | undefined {
694
const oldExtensions = [...extensions];
695
const findPreviousExtensionIndex = (from: number): number => {
696
let index = -1;
697
const previousExtensionInNew = newExtensions[from];
698
if (previousExtensionInNew) {
699
index = oldExtensions.findIndex(e => areSameExtensions(e.identifier, previousExtensionInNew.identifier));
700
if (index === -1) {
701
return findPreviousExtensionIndex(from - 1);
702
}
703
}
704
return index;
705
};
706
707
let hasChanged: boolean = false;
708
for (let index = 0; index < newExtensions.length; index++) {
709
const extension = newExtensions[index];
710
if (extensions.every(r => !areSameExtensions(r.identifier, extension.identifier))) {
711
hasChanged = true;
712
extensions.splice(findPreviousExtensionIndex(index - 1) + 1, 0, extension);
713
}
714
}
715
716
return hasChanged ? extensions : undefined;
717
}
718
719
private async queryGallery(query: Query, options: IGalleryQueryOptions, token: CancellationToken): Promise<IQueryResult> {
720
const hasUserDefinedSortOrder = options.sortBy !== undefined;
721
if (!hasUserDefinedSortOrder && !query.value.trim()) {
722
options.sortBy = GallerySortBy.InstallCount;
723
}
724
725
if (this.isRecommendationsQuery(query)) {
726
const model = await this.queryRecommendations(query, options, token);
727
return { model, disposables: new DisposableStore() };
728
}
729
730
const text = query.value;
731
732
if (!text) {
733
options.source = 'viewlet';
734
const pager = await this.extensionsWorkbenchService.queryGallery(options, token);
735
return { model: new PagedModel(pager), disposables: new DisposableStore() };
736
}
737
738
if (/\bext:([^\s]+)\b/g.test(text)) {
739
options.text = text;
740
options.source = 'file-extension-tags';
741
const pager = await this.extensionsWorkbenchService.queryGallery(options, token);
742
return { model: new PagedModel(pager), disposables: new DisposableStore() };
743
}
744
745
options.text = text.substring(0, 350);
746
options.source = 'searchText';
747
748
if (hasUserDefinedSortOrder || /\b(category|tag):([^\s]+)\b/gi.test(text) || /\bfeatured(\s+|\b|$)/gi.test(text)) {
749
const pager = await this.extensionsWorkbenchService.queryGallery(options, token);
750
return { model: new PagedModel(pager), disposables: new DisposableStore() };
751
}
752
753
try {
754
const [pager, preferredExtensions] = await Promise.all([
755
this.extensionsWorkbenchService.queryGallery(options, token),
756
this.getPreferredExtensions(options.text.toLowerCase(), token).catch(() => [])
757
]);
758
759
const model = preferredExtensions.length ? new PreferredExtensionsPagedModel(preferredExtensions, pager) : new PagedModel(pager);
760
return { model, disposables: new DisposableStore() };
761
} catch (error) {
762
if (isCancellationError(error)) {
763
throw error;
764
}
765
766
if (!(error instanceof ExtensionGalleryError)) {
767
throw error;
768
}
769
770
const searchText = options.text.toLowerCase();
771
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));
772
if (localExtensions.length) {
773
const message = this.getMessage(error);
774
return { model: new PagedModel(localExtensions), disposables: new DisposableStore(), message: { text: localize('showing local extensions only', "{0} Showing local extensions.", message.text), severity: message.severity } };
775
}
776
777
throw error;
778
}
779
}
780
781
private async getPreferredExtensions(searchText: string, token: CancellationToken): Promise<IExtension[]> {
782
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));
783
const preferredExtensionUUIDs = new Set<string>();
784
785
if (preferredExtensions.length) {
786
// Update gallery data for preferred extensions if they are not yet fetched
787
const extesionsToFetch: IExtensionIdentifier[] = [];
788
for (const extension of preferredExtensions) {
789
if (extension.identifier.uuid) {
790
preferredExtensionUUIDs.add(extension.identifier.uuid);
791
}
792
if (!extension.gallery && extension.identifier.uuid) {
793
extesionsToFetch.push(extension.identifier);
794
}
795
}
796
if (extesionsToFetch.length) {
797
this.extensionsWorkbenchService.getExtensions(extesionsToFetch, CancellationToken.None).catch(e => null/*ignore error*/);
798
}
799
}
800
801
const preferredResults: string[] = [];
802
try {
803
const manifest = await this.extensionManagementService.getExtensionsControlManifest();
804
if (Array.isArray(manifest.search)) {
805
for (const s of manifest.search) {
806
if (s.query && s.query.toLowerCase() === searchText && Array.isArray(s.preferredResults)) {
807
preferredResults.push(...s.preferredResults);
808
break;
809
}
810
}
811
}
812
if (preferredResults.length) {
813
const result = await this.extensionsWorkbenchService.getExtensions(preferredResults.map(id => ({ id })), token);
814
for (const extension of result) {
815
if (extension.identifier.uuid && !preferredExtensionUUIDs.has(extension.identifier.uuid)) {
816
preferredExtensions.push(extension);
817
}
818
}
819
}
820
} catch (e) {
821
this.logService.warn('Failed to get preferred results from the extensions control manifest.', e);
822
}
823
824
return preferredExtensions;
825
}
826
827
private sortExtensions(extensions: IExtension[], options: IQueryOptions): IExtension[] {
828
switch (options.sortBy) {
829
case GallerySortBy.InstallCount:
830
extensions = extensions.sort((e1, e2) => typeof e2.installCount === 'number' && typeof e1.installCount === 'number' ? e2.installCount - e1.installCount : NaN);
831
break;
832
case LocalSortBy.UpdateDate:
833
extensions = extensions.sort((e1, e2) =>
834
typeof e2.local?.installedTimestamp === 'number' && typeof e1.local?.installedTimestamp === 'number' ? e2.local.installedTimestamp - e1.local.installedTimestamp :
835
typeof e2.local?.installedTimestamp === 'number' ? 1 :
836
typeof e1.local?.installedTimestamp === 'number' ? -1 : NaN);
837
break;
838
case GallerySortBy.AverageRating:
839
case GallerySortBy.WeightedRating:
840
extensions = extensions.sort((e1, e2) => typeof e2.rating === 'number' && typeof e1.rating === 'number' ? e2.rating - e1.rating : NaN);
841
break;
842
default:
843
extensions = extensions.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName));
844
break;
845
}
846
if (options.sortOrder === SortOrder.Descending) {
847
extensions = extensions.reverse();
848
}
849
return extensions;
850
}
851
852
private isRecommendationsQuery(query: Query): boolean {
853
return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)
854
|| ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)
855
|| ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value)
856
|| ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)
857
|| ExtensionsListView.isRemoteRecommendedExtensionsQuery(query.value)
858
|| /@recommended:all/i.test(query.value)
859
|| ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)
860
|| ExtensionsListView.isRecommendedExtensionsQuery(query.value);
861
}
862
863
private async queryRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
864
// Workspace recommendations
865
if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {
866
return this.getWorkspaceRecommendationsModel(query, options, token);
867
}
868
869
// Keymap recommendations
870
if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
871
return this.getKeymapRecommendationsModel(query, options, token);
872
}
873
874
// Language recommendations
875
if (ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value)) {
876
return this.getLanguageRecommendationsModel(query, options, token);
877
}
878
879
// Exe recommendations
880
if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) {
881
return this.getExeRecommendationsModel(query, options, token);
882
}
883
884
// Remote recommendations
885
if (ExtensionsListView.isRemoteRecommendedExtensionsQuery(query.value)) {
886
return this.getRemoteRecommendationsModel(query, options, token);
887
}
888
889
// All recommendations
890
if (/@recommended:all/i.test(query.value)) {
891
return this.getAllRecommendationsModel(options, token);
892
}
893
894
// Search recommendations
895
if (ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) ||
896
(ExtensionsListView.isRecommendedExtensionsQuery(query.value) && options.sortBy !== undefined)) {
897
return this.searchRecommendations(query, options, token);
898
}
899
900
// Other recommendations
901
if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
902
return this.getOtherRecommendationsModel(query, options, token);
903
}
904
905
return new PagedModel([]);
906
}
907
908
protected async getInstallableRecommendations(recommendations: Array<string | URI>, options: IQueryOptions, token: CancellationToken): Promise<IExtension[]> {
909
const result: IExtension[] = [];
910
if (recommendations.length) {
911
const galleryExtensions: string[] = [];
912
const resourceExtensions: URI[] = [];
913
for (const recommendation of recommendations) {
914
if (typeof recommendation === 'string') {
915
galleryExtensions.push(recommendation);
916
} else {
917
resourceExtensions.push(recommendation);
918
}
919
}
920
if (galleryExtensions.length) {
921
try {
922
const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: options.source }, token);
923
for (const extension of extensions) {
924
if (extension.gallery && !extension.deprecationInfo
925
&& await this.extensionManagementService.canInstall(extension.gallery) === true) {
926
result.push(extension);
927
}
928
}
929
} catch (error) {
930
if (!resourceExtensions.length || !this.isOfflineError(error)) {
931
throw error;
932
}
933
}
934
}
935
if (resourceExtensions.length) {
936
const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true);
937
for (const extension of extensions) {
938
if (await this.extensionsWorkbenchService.canInstall(extension) === true) {
939
result.push(extension);
940
}
941
}
942
}
943
}
944
return result;
945
}
946
947
protected async getWorkspaceRecommendations(): Promise<Array<string | URI>> {
948
const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations();
949
const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations();
950
for (const configBasedRecommendation of important) {
951
if (!recommendations.find(extensionId => extensionId === configBasedRecommendation)) {
952
recommendations.push(configBasedRecommendation);
953
}
954
}
955
return recommendations;
956
}
957
958
private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
959
const recommendations = await this.getWorkspaceRecommendations();
960
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token));
961
return new PagedModel(installableRecommendations);
962
}
963
964
private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
965
const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase();
966
const recommendations = this.extensionRecommendationsService.getKeymapRecommendations();
967
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-keymaps' }, token))
968
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
969
return new PagedModel(installableRecommendations);
970
}
971
972
private async getLanguageRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
973
const value = query.value.replace(/@recommended:languages/g, '').trim().toLowerCase();
974
const recommendations = this.extensionRecommendationsService.getLanguageRecommendations();
975
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-languages' }, token))
976
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
977
return new PagedModel(installableRecommendations);
978
}
979
980
private async getRemoteRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
981
const value = query.value.replace(/@recommended:remotes/g, '').trim().toLowerCase();
982
const recommendations = this.extensionRecommendationsService.getRemoteRecommendations();
983
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-remotes' }, token))
984
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
985
return new PagedModel(installableRecommendations);
986
}
987
988
private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
989
const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase();
990
const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe);
991
const installableRecommendations = await this.getInstallableRecommendations([...important, ...others], { ...options, source: 'recommendations-exe' }, token);
992
return new PagedModel(installableRecommendations);
993
}
994
995
private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
996
const otherRecommendations = await this.getOtherRecommendations();
997
const installableRecommendations = await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token);
998
const result = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
999
return new PagedModel(result);
1000
}
1001
1002
private async getOtherRecommendations(): Promise<string[]> {
1003
const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server))
1004
.map(e => e.identifier.id.toLowerCase());
1005
const workspaceRecommendations = (await this.getWorkspaceRecommendations())
1006
.map(extensionId => isString(extensionId) ? extensionId.toLowerCase() : extensionId);
1007
1008
return distinct(
1009
(await Promise.all([
1010
// Order is important
1011
this.extensionRecommendationsService.getImportantRecommendations(),
1012
this.extensionRecommendationsService.getFileBasedRecommendations(),
1013
this.extensionRecommendationsService.getOtherRecommendations()
1014
])).flat().filter(extensionId => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase())
1015
), extensionId => extensionId.toLowerCase());
1016
}
1017
1018
// Get All types of recommendations, trimmed to show a max of 8 at any given time
1019
private async getAllRecommendationsModel(options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
1020
const localExtensions = await this.extensionsWorkbenchService.queryLocal(this.options.server);
1021
const localExtensionIds = localExtensions.map(e => e.identifier.id.toLowerCase());
1022
1023
const allRecommendations = distinct(
1024
(await Promise.all([
1025
// Order is important
1026
this.getWorkspaceRecommendations(),
1027
this.extensionRecommendationsService.getImportantRecommendations(),
1028
this.extensionRecommendationsService.getFileBasedRecommendations(),
1029
this.extensionRecommendationsService.getOtherRecommendations()
1030
])).flat().filter(extensionId => {
1031
if (isString(extensionId)) {
1032
return !localExtensionIds.includes(extensionId.toLowerCase());
1033
}
1034
return !localExtensions.some(localExtension => localExtension.local && this.uriIdentityService.extUri.isEqual(localExtension.local.location, extensionId));
1035
}));
1036
1037
const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token);
1038
1039
const result: IExtension[] = [];
1040
for (let i = 0; i < installableRecommendations.length && result.length < 8; i++) {
1041
const recommendation = allRecommendations[i];
1042
if (isString(recommendation)) {
1043
const extension = installableRecommendations.find(extension => areSameExtensions(extension.identifier, { id: recommendation }));
1044
if (extension) {
1045
result.push(extension);
1046
}
1047
} else {
1048
const extension = installableRecommendations.find(extension => extension.resourceExtension && this.uriIdentityService.extUri.isEqual(extension.resourceExtension.location, recommendation));
1049
if (extension) {
1050
result.push(extension);
1051
}
1052
}
1053
}
1054
1055
return new PagedModel(result);
1056
}
1057
1058
private async searchRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
1059
const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
1060
const recommendations = distinct([...await this.getWorkspaceRecommendations(), ...await this.getOtherRecommendations()]);
1061
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations', sortBy: undefined }, token))
1062
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
1063
return new PagedModel(this.sortExtensions(installableRecommendations, options));
1064
}
1065
1066
private setModel(model: IPagedModel<IExtension>, message?: Message, donotResetScrollTop?: boolean) {
1067
if (this.list) {
1068
this.list.model = new DelayedPagedModel(model);
1069
this.updateBody(message);
1070
if (!donotResetScrollTop) {
1071
this.list.scrollTop = 0;
1072
}
1073
}
1074
if (this.badge) {
1075
this.badge.setCount(this.count());
1076
}
1077
}
1078
1079
private updateModel(model: IPagedModel<IExtension>) {
1080
if (this.list) {
1081
this.list.model = new DelayedPagedModel(model);
1082
this.updateBody();
1083
}
1084
if (this.badge) {
1085
this.badge.setCount(this.count());
1086
}
1087
}
1088
1089
private updateBody(message?: Message): void {
1090
if (this.bodyTemplate) {
1091
1092
const count = this.count();
1093
this.bodyTemplate.extensionsList.classList.toggle('hidden', count === 0);
1094
this.bodyTemplate.messageContainer.classList.toggle('hidden', !message && count > 0);
1095
1096
if (this.isBodyVisible()) {
1097
if (message) {
1098
this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(message.severity);
1099
this.bodyTemplate.messageBox.textContent = message.text;
1100
} else if (this.count() === 0) {
1101
this.bodyTemplate.messageSeverityIcon.className = '';
1102
this.bodyTemplate.messageBox.textContent = localize('no extensions found', "No extensions found.");
1103
}
1104
if (this.bodyTemplate.messageBox.textContent) {
1105
alert(this.bodyTemplate.messageBox.textContent);
1106
}
1107
}
1108
}
1109
1110
this.updateSize();
1111
}
1112
1113
private getMessage(error: any): Message {
1114
if (this.isOfflineError(error)) {
1115
return { text: localize('offline error', "Unable to search the Marketplace when offline, please check your network connection."), severity: Severity.Warning };
1116
} else {
1117
return { text: localize('error', "Error while fetching extensions. {0}", getErrorMessage(error)), severity: Severity.Error };
1118
}
1119
}
1120
1121
private isOfflineError(error: Error): boolean {
1122
if (error instanceof ExtensionGalleryError) {
1123
return error.code === ExtensionGalleryErrorCode.Offline;
1124
}
1125
return isOfflineError(error);
1126
}
1127
1128
protected updateSize() {
1129
if (this.options.flexibleHeight) {
1130
this.maximumBodySize = this.list?.model.length ? Number.POSITIVE_INFINITY : 0;
1131
this.storageService.store(`${this.id}.size`, this.list?.model.length || 0, StorageScope.PROFILE, StorageTarget.MACHINE);
1132
}
1133
}
1134
1135
override dispose(): void {
1136
super.dispose();
1137
if (this.queryRequest) {
1138
this.queryRequest.request.cancel();
1139
this.queryRequest = null;
1140
}
1141
if (this.queryResult) {
1142
this.queryResult.disposables.dispose();
1143
this.queryResult = undefined;
1144
}
1145
this.list = null;
1146
}
1147
1148
static isLocalExtensionsQuery(query: string, sortBy?: string): boolean {
1149
return this.isInstalledExtensionsQuery(query)
1150
|| this.isSearchInstalledExtensionsQuery(query)
1151
|| this.isOutdatedExtensionsQuery(query)
1152
|| this.isEnabledExtensionsQuery(query)
1153
|| this.isDisabledExtensionsQuery(query)
1154
|| this.isBuiltInExtensionsQuery(query)
1155
|| this.isSearchBuiltInExtensionsQuery(query)
1156
|| this.isBuiltInGroupExtensionsQuery(query)
1157
|| this.isSearchDeprecatedExtensionsQuery(query)
1158
|| this.isSearchWorkspaceUnsupportedExtensionsQuery(query)
1159
|| this.isSearchRecentlyUpdatedQuery(query)
1160
|| this.isSearchExtensionUpdatesQuery(query)
1161
|| this.isSortInstalledExtensionsQuery(query, sortBy)
1162
|| this.isFeatureExtensionsQuery(query);
1163
}
1164
1165
static isSearchBuiltInExtensionsQuery(query: string): boolean {
1166
return /@builtin\s.+|.+\s@builtin/i.test(query);
1167
}
1168
1169
static isBuiltInExtensionsQuery(query: string): boolean {
1170
return /^@builtin$/i.test(query.trim());
1171
}
1172
1173
static isBuiltInGroupExtensionsQuery(query: string): boolean {
1174
return /^@builtin:.+$/i.test(query.trim());
1175
}
1176
1177
static isSearchWorkspaceUnsupportedExtensionsQuery(query: string): boolean {
1178
return /^\s*@workspaceUnsupported(:(untrusted|virtual)(Partial)?)?(\s|$)/i.test(query);
1179
}
1180
1181
static isInstalledExtensionsQuery(query: string): boolean {
1182
return /@installed$/i.test(query);
1183
}
1184
1185
static isSearchInstalledExtensionsQuery(query: string): boolean {
1186
return /@installed\s./i.test(query) || this.isFeatureExtensionsQuery(query);
1187
}
1188
1189
static isOutdatedExtensionsQuery(query: string): boolean {
1190
return /@outdated/i.test(query);
1191
}
1192
1193
static isEnabledExtensionsQuery(query: string): boolean {
1194
return /@enabled/i.test(query) && !/@builtin/i.test(query);
1195
}
1196
1197
static isDisabledExtensionsQuery(query: string): boolean {
1198
return /@disabled/i.test(query) && !/@builtin/i.test(query);
1199
}
1200
1201
static isSearchDeprecatedExtensionsQuery(query: string): boolean {
1202
return /@deprecated\s?.*/i.test(query);
1203
}
1204
1205
static isRecommendedExtensionsQuery(query: string): boolean {
1206
return /^@recommended$/i.test(query.trim());
1207
}
1208
1209
static isSearchRecommendedExtensionsQuery(query: string): boolean {
1210
return /@recommended\s.+/i.test(query);
1211
}
1212
1213
static isWorkspaceRecommendedExtensionsQuery(query: string): boolean {
1214
return /@recommended:workspace/i.test(query);
1215
}
1216
1217
static isExeRecommendedExtensionsQuery(query: string): boolean {
1218
return /@exe:.+/i.test(query);
1219
}
1220
1221
static isRemoteRecommendedExtensionsQuery(query: string): boolean {
1222
return /@recommended:remotes/i.test(query);
1223
}
1224
1225
static isKeymapsRecommendedExtensionsQuery(query: string): boolean {
1226
return /@recommended:keymaps/i.test(query);
1227
}
1228
1229
static isLanguageRecommendedExtensionsQuery(query: string): boolean {
1230
return /@recommended:languages/i.test(query);
1231
}
1232
1233
static isSortInstalledExtensionsQuery(query: string, sortBy?: string): boolean {
1234
return (sortBy !== undefined && sortBy !== '' && query === '') || (!sortBy && /^@sort:\S*$/i.test(query));
1235
}
1236
1237
static isSearchPopularQuery(query: string): boolean {
1238
return /@popular/i.test(query);
1239
}
1240
1241
static isSearchRecentlyPublishedQuery(query: string): boolean {
1242
return /@recentlyPublished/i.test(query);
1243
}
1244
1245
static isSearchRecentlyUpdatedQuery(query: string): boolean {
1246
return /@recentlyUpdated/i.test(query);
1247
}
1248
1249
static isSearchExtensionUpdatesQuery(query: string): boolean {
1250
return /@updates/i.test(query);
1251
}
1252
1253
static isSortUpdateDateQuery(query: string): boolean {
1254
return /@sort:updateDate/i.test(query);
1255
}
1256
1257
static isFeatureExtensionsQuery(query: string): boolean {
1258
return /@contribute:/i.test(query);
1259
}
1260
1261
override focus(): void {
1262
super.focus();
1263
if (!this.list) {
1264
return;
1265
}
1266
1267
if (!(this.list.getFocus().length || this.list.getSelection().length)) {
1268
this.list.focusNext();
1269
}
1270
this.list.domFocus();
1271
}
1272
}
1273
1274
export class DefaultPopularExtensionsView extends ExtensionsListView {
1275
1276
override async show(): Promise<IPagedModel<IExtension>> {
1277
const query = this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer ? '@web' : '';
1278
return super.show(query);
1279
}
1280
1281
}
1282
1283
export class ServerInstalledExtensionsView extends ExtensionsListView {
1284
1285
override async show(query: string): Promise<IPagedModel<IExtension>> {
1286
query = query ? query : '@installed';
1287
if (!ExtensionsListView.isLocalExtensionsQuery(query) || ExtensionsListView.isSortInstalledExtensionsQuery(query)) {
1288
query = query += ' @installed';
1289
}
1290
return super.show(query.trim());
1291
}
1292
1293
}
1294
1295
export class EnabledExtensionsView extends ExtensionsListView {
1296
1297
override async show(query: string): Promise<IPagedModel<IExtension>> {
1298
query = query || '@enabled';
1299
return ExtensionsListView.isEnabledExtensionsQuery(query) ? super.show(query) :
1300
ExtensionsListView.isSortInstalledExtensionsQuery(query) ? super.show('@enabled ' + query) : this.showEmptyModel();
1301
}
1302
}
1303
1304
export class DisabledExtensionsView extends ExtensionsListView {
1305
1306
override async show(query: string): Promise<IPagedModel<IExtension>> {
1307
query = query || '@disabled';
1308
return ExtensionsListView.isDisabledExtensionsQuery(query) ? super.show(query) :
1309
ExtensionsListView.isSortInstalledExtensionsQuery(query) ? super.show('@disabled ' + query) : this.showEmptyModel();
1310
}
1311
}
1312
1313
export class OutdatedExtensionsView extends ExtensionsListView {
1314
1315
override async show(query: string): Promise<IPagedModel<IExtension>> {
1316
query = query ? query : '@outdated';
1317
if (ExtensionsListView.isSearchExtensionUpdatesQuery(query)) {
1318
query = query.replace('@updates', '@outdated');
1319
}
1320
return super.show(query.trim());
1321
}
1322
1323
protected override updateSize() {
1324
super.updateSize();
1325
this.setExpanded(this.count() > 0);
1326
}
1327
1328
}
1329
1330
export class RecentlyUpdatedExtensionsView extends ExtensionsListView {
1331
1332
override async show(query: string): Promise<IPagedModel<IExtension>> {
1333
query = query ? query : '@recentlyUpdated';
1334
if (ExtensionsListView.isSearchExtensionUpdatesQuery(query)) {
1335
query = query.replace('@updates', '@recentlyUpdated');
1336
}
1337
return super.show(query.trim());
1338
}
1339
1340
}
1341
1342
export interface StaticQueryExtensionsViewOptions extends ExtensionsListViewOptions {
1343
readonly query: string;
1344
}
1345
1346
export class StaticQueryExtensionsView extends ExtensionsListView {
1347
1348
constructor(
1349
protected override readonly options: StaticQueryExtensionsViewOptions,
1350
viewletViewOptions: IViewletViewOptions,
1351
@INotificationService notificationService: INotificationService,
1352
@IKeybindingService keybindingService: IKeybindingService,
1353
@IContextMenuService contextMenuService: IContextMenuService,
1354
@IInstantiationService instantiationService: IInstantiationService,
1355
@IThemeService themeService: IThemeService,
1356
@IExtensionService extensionService: IExtensionService,
1357
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
1358
@IExtensionRecommendationsService extensionRecommendationsService: IExtensionRecommendationsService,
1359
@ITelemetryService telemetryService: ITelemetryService,
1360
@IHoverService hoverService: IHoverService,
1361
@IConfigurationService configurationService: IConfigurationService,
1362
@IWorkspaceContextService contextService: IWorkspaceContextService,
1363
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
1364
@IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService,
1365
@IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService,
1366
@IWorkspaceContextService workspaceService: IWorkspaceContextService,
1367
@IProductService productService: IProductService,
1368
@IContextKeyService contextKeyService: IContextKeyService,
1369
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
1370
@IOpenerService openerService: IOpenerService,
1371
@IStorageService storageService: IStorageService,
1372
@IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService,
1373
@IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService,
1374
@IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService,
1375
@IUriIdentityService uriIdentityService: IUriIdentityService,
1376
@ILogService logService: ILogService
1377
) {
1378
super(options, viewletViewOptions, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService,
1379
extensionsWorkbenchService, extensionRecommendationsService, telemetryService, hoverService, configurationService, contextService, extensionManagementServerService,
1380
extensionManifestPropertiesService, extensionManagementService, workspaceService, productService, contextKeyService, viewDescriptorService, openerService,
1381
storageService, workspaceTrustManagementService, extensionEnablementService, extensionFeaturesManagementService,
1382
uriIdentityService, logService);
1383
}
1384
1385
override show(): Promise<IPagedModel<IExtension>> {
1386
return super.show(this.options.query);
1387
}
1388
}
1389
1390
function toSpecificWorkspaceUnsupportedQuery(query: string, qualifier: string): string | undefined {
1391
if (!query) {
1392
return '@workspaceUnsupported:' + qualifier;
1393
}
1394
const match = query.match(new RegExp(`@workspaceUnsupported(:${qualifier})?(\\s|$)`, 'i'));
1395
if (match) {
1396
if (!match[1]) {
1397
return query.replace(/@workspaceUnsupported/gi, '@workspaceUnsupported:' + qualifier);
1398
}
1399
return query;
1400
}
1401
return undefined;
1402
}
1403
1404
1405
export class UntrustedWorkspaceUnsupportedExtensionsView extends ExtensionsListView {
1406
override async show(query: string): Promise<IPagedModel<IExtension>> {
1407
const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrusted');
1408
return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();
1409
}
1410
}
1411
1412
export class UntrustedWorkspacePartiallySupportedExtensionsView extends ExtensionsListView {
1413
override async show(query: string): Promise<IPagedModel<IExtension>> {
1414
const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrustedPartial');
1415
return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();
1416
}
1417
}
1418
1419
export class VirtualWorkspaceUnsupportedExtensionsView extends ExtensionsListView {
1420
override async show(query: string): Promise<IPagedModel<IExtension>> {
1421
const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtual');
1422
return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();
1423
}
1424
}
1425
1426
export class VirtualWorkspacePartiallySupportedExtensionsView extends ExtensionsListView {
1427
override async show(query: string): Promise<IPagedModel<IExtension>> {
1428
const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtualPartial');
1429
return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();
1430
}
1431
}
1432
1433
export class DeprecatedExtensionsView extends ExtensionsListView {
1434
override async show(query: string): Promise<IPagedModel<IExtension>> {
1435
return ExtensionsListView.isSearchDeprecatedExtensionsQuery(query) ? super.show(query) : this.showEmptyModel();
1436
}
1437
}
1438
1439
export class SearchMarketplaceExtensionsView extends ExtensionsListView {
1440
1441
private readonly reportSearchFinishedDelayer = this._register(new ThrottledDelayer(2000));
1442
private searchWaitPromise: Promise<void> = Promise.resolve();
1443
1444
override async show(query: string): Promise<IPagedModel<IExtension>> {
1445
const queryPromise = super.show(query);
1446
this.reportSearchFinishedDelayer.trigger(() => this.reportSearchFinished());
1447
this.searchWaitPromise = queryPromise.then(null, null);
1448
return queryPromise;
1449
}
1450
1451
private async reportSearchFinished(): Promise<void> {
1452
await this.searchWaitPromise;
1453
this.telemetryService.publicLog2('extensionsView:MarketplaceSearchFinished');
1454
}
1455
}
1456
1457
export class DefaultRecommendedExtensionsView extends ExtensionsListView {
1458
private readonly recommendedExtensionsQuery = '@recommended:all';
1459
1460
protected override renderBody(container: HTMLElement): void {
1461
super.renderBody(container);
1462
1463
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => {
1464
this.show('');
1465
}));
1466
}
1467
1468
override async show(query: string): Promise<IPagedModel<IExtension>> {
1469
if (query && query.trim() !== this.recommendedExtensionsQuery) {
1470
return this.showEmptyModel();
1471
}
1472
const model = await super.show(this.recommendedExtensionsQuery);
1473
if (!this.extensionsWorkbenchService.local.some(e => !e.isBuiltin)) {
1474
// This is part of popular extensions view. Collapse if no installed extensions.
1475
this.setExpanded(model.length > 0);
1476
}
1477
return model;
1478
}
1479
1480
}
1481
1482
export class RecommendedExtensionsView extends ExtensionsListView {
1483
private readonly recommendedExtensionsQuery = '@recommended';
1484
1485
protected override renderBody(container: HTMLElement): void {
1486
super.renderBody(container);
1487
1488
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => {
1489
this.show('');
1490
}));
1491
}
1492
1493
override async show(query: string): Promise<IPagedModel<IExtension>> {
1494
return (query && query.trim() !== this.recommendedExtensionsQuery) ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery);
1495
}
1496
}
1497
1498
export class WorkspaceRecommendedExtensionsView extends ExtensionsListView implements IWorkspaceRecommendedExtensionsView {
1499
private readonly recommendedExtensionsQuery = '@recommended:workspace';
1500
1501
protected override renderBody(container: HTMLElement): void {
1502
super.renderBody(container);
1503
1504
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.show(this.recommendedExtensionsQuery)));
1505
this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery)));
1506
}
1507
1508
override async show(query: string): Promise<IPagedModel<IExtension>> {
1509
const shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace';
1510
const model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery));
1511
this.setExpanded(model.length > 0);
1512
return model;
1513
}
1514
1515
private async getInstallableWorkspaceRecommendations(): Promise<IExtension[]> {
1516
const installed = (await this.extensionsWorkbenchService.queryLocal())
1517
.filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
1518
const recommendations = (await this.getWorkspaceRecommendations())
1519
.filter(recommendation => installed.every(local => isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.local?.location)));
1520
return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None);
1521
}
1522
1523
async installWorkspaceRecommendations(): Promise<void> {
1524
const installableRecommendations = await this.getInstallableWorkspaceRecommendations();
1525
if (installableRecommendations.length) {
1526
const galleryExtensions: InstallExtensionInfo[] = [];
1527
const resourceExtensions: IExtension[] = [];
1528
for (const recommendation of installableRecommendations) {
1529
if (recommendation.gallery) {
1530
galleryExtensions.push({ extension: recommendation.gallery, options: {} });
1531
} else {
1532
resourceExtensions.push(recommendation);
1533
}
1534
}
1535
await Promise.all([
1536
this.extensionManagementService.installGalleryExtensions(galleryExtensions),
1537
...resourceExtensions.map(extension => this.extensionsWorkbenchService.install(extension))
1538
]);
1539
} else {
1540
this.notificationService.notify({
1541
severity: Severity.Info,
1542
message: localize('no local extensions', "There are no extensions to install.")
1543
});
1544
}
1545
}
1546
1547
}
1548
1549
export class PreferredExtensionsPagedModel implements IPagedModel<IExtension> {
1550
1551
private readonly resolved = new Map<number, IExtension>();
1552
private preferredGalleryExtensions = new Set<string>();
1553
private resolvedGalleryExtensionsFromQuery: IExtension[] = [];
1554
private readonly pages: Array<{
1555
promise: Promise<void> | null;
1556
cts: CancellationTokenSource | null;
1557
promiseIndexes: Set<number>;
1558
}>;
1559
1560
public readonly length: number;
1561
1562
get onDidIncrementLength(): Event<number> {
1563
return Event.None;
1564
}
1565
1566
constructor(
1567
private readonly preferredExtensions: IExtension[],
1568
private readonly pager: IPager<IExtension>,
1569
) {
1570
for (let i = 0; i < this.preferredExtensions.length; i++) {
1571
this.resolved.set(i, this.preferredExtensions[i]);
1572
}
1573
1574
for (const e of preferredExtensions) {
1575
if (e.identifier.uuid) {
1576
this.preferredGalleryExtensions.add(e.identifier.uuid);
1577
}
1578
}
1579
1580
// expected that all preferred gallery extensions will be part of the query results
1581
this.length = (preferredExtensions.length - this.preferredGalleryExtensions.size) + this.pager.total;
1582
1583
const totalPages = Math.ceil(this.pager.total / this.pager.pageSize);
1584
this.populateResolvedExtensions(0, this.pager.firstPage);
1585
this.pages = range(totalPages - 1).map(() => ({
1586
promise: null,
1587
cts: null,
1588
promiseIndexes: new Set<number>(),
1589
}));
1590
}
1591
1592
isResolved(index: number): boolean {
1593
return this.resolved.has(index);
1594
}
1595
1596
get(index: number): IExtension {
1597
return this.resolved.get(index)!;
1598
}
1599
1600
async resolve(index: number, cancellationToken: CancellationToken): Promise<IExtension> {
1601
if (cancellationToken.isCancellationRequested) {
1602
throw new CancellationError();
1603
}
1604
1605
if (this.isResolved(index)) {
1606
return this.get(index);
1607
}
1608
1609
const indexInPagedModel = index - this.preferredExtensions.length + this.resolvedGalleryExtensionsFromQuery.length;
1610
const pageIndex = Math.floor(indexInPagedModel / this.pager.pageSize);
1611
const page = this.pages[pageIndex];
1612
1613
if (!page.promise) {
1614
page.cts = new CancellationTokenSource();
1615
page.promise = this.pager.getPage(pageIndex, page.cts.token)
1616
.then(extensions => this.populateResolvedExtensions(pageIndex, extensions))
1617
.catch(e => { page.promise = null; throw e; })
1618
.finally(() => page.cts = null);
1619
}
1620
1621
const listener = cancellationToken.onCancellationRequested(() => {
1622
if (!page.cts) {
1623
return;
1624
}
1625
page.promiseIndexes.delete(index);
1626
if (page.promiseIndexes.size === 0) {
1627
page.cts.cancel();
1628
}
1629
});
1630
1631
page.promiseIndexes.add(index);
1632
1633
try {
1634
await page.promise;
1635
} finally {
1636
listener.dispose();
1637
}
1638
1639
return this.get(index);
1640
}
1641
1642
private populateResolvedExtensions(pageIndex: number, extensions: IExtension[]): void {
1643
let adjustIndexOfNextPagesBy = 0;
1644
const pageStartIndex = pageIndex * this.pager.pageSize;
1645
for (let i = 0; i < extensions.length; i++) {
1646
const e = extensions[i];
1647
if (e.gallery?.identifier.uuid && this.preferredGalleryExtensions.has(e.gallery.identifier.uuid)) {
1648
this.resolvedGalleryExtensionsFromQuery.push(e);
1649
adjustIndexOfNextPagesBy++;
1650
} else {
1651
this.resolved.set(this.preferredExtensions.length - this.resolvedGalleryExtensionsFromQuery.length + pageStartIndex + i, e);
1652
}
1653
}
1654
// If this page has preferred gallery extensions, then adjust the index of the next pages
1655
// by the number of preferred gallery extensions found in this page. Because these preferred extensions
1656
// are already in the resolved list and since we did not add them now, we need to adjust the indices of the next pages.
1657
// Skip first page as the preferred extensions are always in the first page
1658
if (pageIndex !== 0 && adjustIndexOfNextPagesBy) {
1659
const nextPageStartIndex = (pageIndex + 1) * this.pager.pageSize;
1660
const indices = [...this.resolved.keys()].sort();
1661
for (const index of indices) {
1662
if (index >= nextPageStartIndex) {
1663
const e = this.resolved.get(index);
1664
if (e) {
1665
this.resolved.delete(index);
1666
this.resolved.set(index - adjustIndexOfNextPagesBy, e);
1667
}
1668
}
1669
}
1670
}
1671
}
1672
}
1673
1674