Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { localize } from '../../../../nls.js';6import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';7import { Event, Emitter } from '../../../../base/common/event.js';8import { isCancellationError, getErrorMessage, CancellationError } from '../../../../base/common/errors.js';9import { PagedModel, IPagedModel, DelayedPagedModel, IPager } from '../../../../base/common/paging.js';10import { SortOrder, IQueryOptions as IGalleryQueryOptions, SortBy as GallerySortBy, InstallExtensionInfo, ExtensionGalleryErrorCode, ExtensionGalleryError } from '../../../../platform/extensionManagement/common/extensionManagement.js';11import { IExtensionManagementServer, IExtensionManagementServerService, EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js';12import { IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';13import { areSameExtensions, getExtensionDependencies } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';14import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';15import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';16import { append, $ } from '../../../../base/browser/dom.js';17import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';18import { ExtensionResultsListFocused, ExtensionState, IExtension, IExtensionsViewState, IExtensionsWorkbenchService, IWorkspaceRecommendedExtensionsView } from '../common/extensions.js';19import { Query } from '../common/extensionQuery.js';20import { IExtensionService, toExtension } from '../../../services/extensions/common/extensions.js';21import { IThemeService } from '../../../../platform/theme/common/themeService.js';22import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';23import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';24import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js';25import { WorkbenchPagedList } from '../../../../platform/list/browser/listService.js';26import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';27import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';28import { ViewPane, IViewPaneOptions, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.js';29import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';30import { coalesce, distinct, range } from '../../../../base/common/arrays.js';31import { alert } from '../../../../base/browser/ui/aria/aria.js';32import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';33import { ActionRunner } from '../../../../base/common/actions.js';34import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionUntrustedWorkspaceSupportType, ExtensionVirtualWorkspaceSupportType, IExtensionDescription, IExtensionIdentifier, isLanguagePackExtension } from '../../../../platform/extensions/common/extensions.js';35import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from '../../../../base/common/async.js';36import { IProductService } from '../../../../platform/product/common/productService.js';37import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js';38import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';39import { IViewDescriptorService } from '../../../common/views.js';40import { IOpenerService } from '../../../../platform/opener/common/opener.js';41import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';42import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js';43import { isVirtualWorkspace } from '../../../../platform/workspace/common/virtualWorkspace.js';44import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';45import { ILogService } from '../../../../platform/log/common/log.js';46import { isOfflineError } from '../../../../base/parts/request/common/request.js';47import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js';48import { Registry } from '../../../../platform/registry/common/platform.js';49import { Extensions, IExtensionFeatureRenderer, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js';50import { URI } from '../../../../base/common/uri.js';51import { isString } from '../../../../base/common/types.js';52import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';53import { IHoverService } from '../../../../platform/hover/browser/hover.js';54import { ExtensionsList } from './extensionsViewer.js';5556export const NONE_CATEGORY = 'none';5758type Message = {59readonly text: string;60readonly severity: Severity;61};6263class ExtensionsViewState extends Disposable implements IExtensionsViewState {6465private readonly _onFocus: Emitter<IExtension> = this._register(new Emitter<IExtension>());66readonly onFocus: Event<IExtension> = this._onFocus.event;6768private readonly _onBlur: Emitter<IExtension> = this._register(new Emitter<IExtension>());69readonly onBlur: Event<IExtension> = this._onBlur.event;7071private currentlyFocusedItems: IExtension[] = [];7273filters: {74featureId?: string;75} = {};7677onFocusChange(extensions: IExtension[]): void {78this.currentlyFocusedItems.forEach(extension => this._onBlur.fire(extension));79this.currentlyFocusedItems = extensions;80this.currentlyFocusedItems.forEach(extension => this._onFocus.fire(extension));81}82}8384export interface ExtensionsListViewOptions {85server?: IExtensionManagementServer;86flexibleHeight?: boolean;87onDidChangeTitle?: Event<string>;88hideBadge?: boolean;89}9091interface IQueryResult {92model: IPagedModel<IExtension>;93message?: { text: string; severity: Severity };94readonly onDidChangeModel?: Event<IPagedModel<IExtension>>;95readonly disposables: DisposableStore;96}9798const enum LocalSortBy {99UpdateDate = 'UpdateDate',100}101102function isLocalSortBy(value: any): value is LocalSortBy {103switch (value as LocalSortBy) {104case LocalSortBy.UpdateDate: return true;105}106}107108type SortBy = LocalSortBy | GallerySortBy;109type IQueryOptions = Omit<IGalleryQueryOptions, 'sortBy'> & { sortBy?: SortBy };110111export abstract class AbstractExtensionsListView<T> extends ViewPane {112abstract show(query: string, refresh?: boolean): Promise<IPagedModel<T>>;113}114115export class ExtensionsListView extends AbstractExtensionsListView<IExtension> {116117private static RECENT_UPDATE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days118119private bodyTemplate: {120messageContainer: HTMLElement;121messageSeverityIcon: HTMLElement;122messageBox: HTMLElement;123extensionsList: HTMLElement;124} | undefined;125private badge: CountBadge | undefined;126private list: WorkbenchPagedList<IExtension> | null = null;127private queryRequest: { query: string; request: CancelablePromise<IPagedModel<IExtension>> } | null = null;128private queryResult: IQueryResult | undefined;129private extensionsViewState: ExtensionsViewState | undefined;130131private readonly contextMenuActionRunner = this._register(new ActionRunner());132133constructor(134protected readonly options: ExtensionsListViewOptions,135viewletViewOptions: IViewletViewOptions,136@INotificationService protected notificationService: INotificationService,137@IKeybindingService keybindingService: IKeybindingService,138@IContextMenuService contextMenuService: IContextMenuService,139@IInstantiationService instantiationService: IInstantiationService,140@IThemeService themeService: IThemeService,141@IExtensionService private readonly extensionService: IExtensionService,142@IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService,143@IExtensionRecommendationsService protected extensionRecommendationsService: IExtensionRecommendationsService,144@ITelemetryService protected readonly telemetryService: ITelemetryService,145@IHoverService hoverService: IHoverService,146@IConfigurationService configurationService: IConfigurationService,147@IWorkspaceContextService protected contextService: IWorkspaceContextService,148@IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService,149@IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService,150@IWorkbenchExtensionManagementService protected readonly extensionManagementService: IWorkbenchExtensionManagementService,151@IWorkspaceContextService protected readonly workspaceService: IWorkspaceContextService,152@IProductService protected readonly productService: IProductService,153@IContextKeyService contextKeyService: IContextKeyService,154@IViewDescriptorService viewDescriptorService: IViewDescriptorService,155@IOpenerService openerService: IOpenerService,156@IStorageService private readonly storageService: IStorageService,157@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,158@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,159@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,160@IUriIdentityService protected readonly uriIdentityService: IUriIdentityService,161@ILogService private readonly logService: ILogService162) {163super({164...(viewletViewOptions as IViewPaneOptions),165showActions: ViewPaneShowActions.Always,166maximumBodySize: options.flexibleHeight ? (storageService.getNumber(`${viewletViewOptions.id}.size`, StorageScope.PROFILE, 0) ? undefined : 0) : undefined167}, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);168if (this.options.onDidChangeTitle) {169this._register(this.options.onDidChangeTitle(title => this.updateTitle(title)));170}171172this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && this.notificationService.error(error)));173this.registerActions();174}175176protected registerActions(): void { }177178protected override renderHeader(container: HTMLElement): void {179container.classList.add('extension-view-header');180super.renderHeader(container);181182if (!this.options.hideBadge) {183this.badge = this._register(new CountBadge(append(container, $('.count-badge-wrapper')), {}, defaultCountBadgeStyles));184}185}186187protected override renderBody(container: HTMLElement): void {188super.renderBody(container);189190const messageContainer = append(container, $('.message-container'));191const messageSeverityIcon = append(messageContainer, $(''));192const messageBox = append(messageContainer, $('.message'));193const extensionsList = append(container, $('.extensions-list'));194this.extensionsViewState = this._register(new ExtensionsViewState());195this.list = this._register(this.instantiationService.createInstance(ExtensionsList, extensionsList, this.id, {}, this.extensionsViewState)).list;196ExtensionResultsListFocused.bindTo(this.list.contextKeyService);197this._register(this.list.onDidChangeFocus(e => this.extensionsViewState?.onFocusChange(coalesce(e.elements)), this));198199this.bodyTemplate = {200extensionsList,201messageBox,202messageContainer,203messageSeverityIcon204};205206if (this.queryResult) {207this.setModel(this.queryResult.model);208}209}210211protected override layoutBody(height: number, width: number): void {212super.layoutBody(height, width);213if (this.bodyTemplate) {214this.bodyTemplate.extensionsList.style.height = height + 'px';215}216this.list?.layout(height, width);217}218219async show(query: string, refresh?: boolean): Promise<IPagedModel<IExtension>> {220if (this.queryRequest) {221if (!refresh && this.queryRequest.query === query) {222return this.queryRequest.request;223}224this.queryRequest.request.cancel();225this.queryRequest = null;226}227228if (this.queryResult) {229this.queryResult.disposables.dispose();230this.queryResult = undefined;231if (this.extensionsViewState) {232this.extensionsViewState.filters = {};233}234}235236const parsedQuery = Query.parse(query);237238const options: IQueryOptions = {239sortOrder: SortOrder.Default240};241242switch (parsedQuery.sortBy) {243case 'installs': options.sortBy = GallerySortBy.InstallCount; break;244case 'rating': options.sortBy = GallerySortBy.WeightedRating; break;245case 'name': options.sortBy = GallerySortBy.Title; break;246case 'publishedDate': options.sortBy = GallerySortBy.PublishedDate; break;247case 'updateDate': options.sortBy = LocalSortBy.UpdateDate; break;248}249250const request = createCancelablePromise(async token => {251try {252this.queryResult = await this.query(parsedQuery, options, token);253const model = this.queryResult.model;254this.setModel(model, this.queryResult.message);255if (this.queryResult.onDidChangeModel) {256this.queryResult.disposables.add(this.queryResult.onDidChangeModel(model => {257if (this.queryResult) {258this.queryResult.model = model;259this.updateModel(model);260}261}));262}263return model;264} catch (e) {265const model = new PagedModel([]);266if (!isCancellationError(e)) {267this.logService.error(e);268this.setModel(model, this.getMessage(e));269}270return this.list ? this.list.model : model;271}272});273274request.finally(() => this.queryRequest = null);275this.queryRequest = { query, request };276return request;277}278279count(): number {280return this.queryResult?.model.length ?? 0;281}282283protected showEmptyModel(): Promise<IPagedModel<IExtension>> {284const emptyModel = new PagedModel([]);285this.setModel(emptyModel);286return Promise.resolve(emptyModel);287}288289private async query(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IQueryResult> {290const idRegex = /@id:(([a-z0-9A-Z][a-z0-9\-A-Z]*)\.([a-z0-9A-Z][a-z0-9\-A-Z]*))/g;291const ids: string[] = [];292let idMatch;293while ((idMatch = idRegex.exec(query.value)) !== null) {294const name = idMatch[1];295ids.push(name);296}297if (ids.length) {298const model = await this.queryByIds(ids, options, token);299return { model, disposables: new DisposableStore() };300}301302if (ExtensionsListView.isLocalExtensionsQuery(query.value, query.sortBy)) {303return this.queryLocal(query, options);304}305306if (ExtensionsListView.isSearchPopularQuery(query.value)) {307query.value = query.value.replace('@popular', '');308options.sortBy = !options.sortBy ? GallerySortBy.InstallCount : options.sortBy;309}310else if (ExtensionsListView.isSearchRecentlyPublishedQuery(query.value)) {311query.value = query.value.replace('@recentlyPublished', '');312options.sortBy = !options.sortBy ? GallerySortBy.PublishedDate : options.sortBy;313}314315const galleryQueryOptions: IGalleryQueryOptions = { ...options, sortBy: isLocalSortBy(options.sortBy) ? undefined : options.sortBy };316return this.queryGallery(query, galleryQueryOptions, token);317}318319private async queryByIds(ids: string[], options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {320const idsSet: Set<string> = ids.reduce((result, id) => { result.add(id.toLowerCase()); return result; }, new Set<string>());321const result = (await this.extensionsWorkbenchService.queryLocal(this.options.server))322.filter(e => idsSet.has(e.identifier.id.toLowerCase()));323324const galleryIds = result.length ? ids.filter(id => result.every(r => !areSameExtensions(r.identifier, { id }))) : ids;325326if (galleryIds.length) {327const galleryResult = await this.extensionsWorkbenchService.getExtensions(galleryIds.map(id => ({ id })), { source: 'queryById' }, token);328result.push(...galleryResult);329}330331return new PagedModel(result);332}333334private async queryLocal(query: Query, options: IQueryOptions): Promise<IQueryResult> {335const local = await this.extensionsWorkbenchService.queryLocal(this.options.server);336let { extensions, canIncludeInstalledExtensions, description } = await this.filterLocal(local, this.extensionService.extensions, query, options);337const disposables = new DisposableStore();338const onDidChangeModel = disposables.add(new Emitter<IPagedModel<IExtension>>());339340if (canIncludeInstalledExtensions) {341let isDisposed: boolean = false;342disposables.add(toDisposable(() => isDisposed = true));343disposables.add(Event.debounce(Event.any(344Event.filter(this.extensionsWorkbenchService.onChange, e => e?.state === ExtensionState.Installed),345this.extensionService.onDidChangeExtensions346), () => undefined)(async () => {347const local = this.options.server ? this.extensionsWorkbenchService.installed.filter(e => e.server === this.options.server) : this.extensionsWorkbenchService.local;348const { extensions: newExtensions } = await this.filterLocal(local, this.extensionService.extensions, query, options);349if (!isDisposed) {350const mergedExtensions = this.mergeAddedExtensions(extensions, newExtensions);351if (mergedExtensions) {352extensions = mergedExtensions;353onDidChangeModel.fire(new PagedModel(extensions));354}355}356}));357}358359return {360model: new PagedModel(extensions),361message: description ? { text: description, severity: Severity.Info } : undefined,362onDidChangeModel: onDidChangeModel.event,363disposables364};365}366367private async filterLocal(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): Promise<{ extensions: IExtension[]; canIncludeInstalledExtensions: boolean; description?: string }> {368const value = query.value;369let extensions: IExtension[] = [];370let description: string | undefined;371const includeBuiltin = /@builtin/i.test(value);372const canIncludeInstalledExtensions = !includeBuiltin;373374if (/@installed/i.test(value)) {375extensions = this.filterInstalledExtensions(local, runningExtensions, query, options);376}377378else if (/@outdated/i.test(value)) {379extensions = this.filterOutdatedExtensions(local, query, options);380}381382else if (/@disabled/i.test(value)) {383extensions = this.filterDisabledExtensions(local, runningExtensions, query, options, includeBuiltin);384}385386else if (/@enabled/i.test(value)) {387extensions = this.filterEnabledExtensions(local, runningExtensions, query, options, includeBuiltin);388}389390else if (/@workspaceUnsupported/i.test(value)) {391extensions = this.filterWorkspaceUnsupportedExtensions(local, query, options);392}393394else if (/@deprecated/i.test(query.value)) {395extensions = await this.filterDeprecatedExtensions(local, query, options);396}397398else if (/@recentlyUpdated/i.test(query.value)) {399extensions = this.filterRecentlyUpdatedExtensions(local, query, options);400}401402else if (/@feature:/i.test(query.value)) {403const result = this.filterExtensionsByFeature(local, query);404if (result) {405extensions = result.extensions;406description = result.description;407}408}409410else if (includeBuiltin) {411extensions = this.filterBuiltinExtensions(local, query, options);412}413414return { extensions, canIncludeInstalledExtensions, description };415}416417private filterBuiltinExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {418let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);419value = value.replaceAll(/@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();420421const result = local422.filter(e => e.isBuiltin && (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)423&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));424425return this.sortExtensions(result, options);426}427428private filterExtensionByCategory(e: IExtension, includedCategories: string[], excludedCategories: string[]): boolean {429if (!includedCategories.length && !excludedCategories.length) {430return true;431}432if (e.categories.length) {433if (excludedCategories.length && e.categories.some(category => excludedCategories.includes(category.toLowerCase()))) {434return false;435}436return e.categories.some(category => includedCategories.includes(category.toLowerCase()));437} else {438return includedCategories.includes(NONE_CATEGORY);439}440}441442private parseCategories(value: string): { value: string; includedCategories: string[]; excludedCategories: string[] } {443const includedCategories: string[] = [];444const excludedCategories: string[] = [];445value = value.replace(/\bcategory:("([^"]*)"|([^"]\S*))(\s+|\b|$)/g, (_, quotedCategory, category) => {446const entry = (category || quotedCategory || '').toLowerCase();447if (entry.startsWith('-')) {448if (excludedCategories.indexOf(entry) === -1) {449excludedCategories.push(entry);450}451} else {452if (includedCategories.indexOf(entry) === -1) {453includedCategories.push(entry);454}455}456return '';457});458return { value, includedCategories, excludedCategories };459}460461private filterInstalledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions): IExtension[] {462let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);463464value = value.replace(/@installed/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();465466const matchingText = (e: IExtension) => (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1 || e.description.toLowerCase().indexOf(value) > -1)467&& this.filterExtensionByCategory(e, includedCategories, excludedCategories);468let result;469470if (options.sortBy !== undefined) {471result = local.filter(e => !e.isBuiltin && matchingText(e));472result = this.sortExtensions(result, options);473} else {474result = local.filter(e => (!e.isBuiltin || e.outdated || e.runtimeState !== undefined) && matchingText(e));475const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(e.identifier.value, e); return result; }, new ExtensionIdentifierMap<IExtensionDescription>());476477const defaultSort = (e1: IExtension, e2: IExtension) => {478const running1 = runningExtensionsById.get(e1.identifier.id);479const isE1Running = !!running1 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running1)) === e1.server;480const running2 = runningExtensionsById.get(e2.identifier.id);481const isE2Running = running2 && this.extensionManagementServerService.getExtensionManagementServer(toExtension(running2)) === e2.server;482if ((isE1Running && isE2Running)) {483return e1.displayName.localeCompare(e2.displayName);484}485const isE1LanguagePackExtension = e1.local && isLanguagePackExtension(e1.local.manifest);486const isE2LanguagePackExtension = e2.local && isLanguagePackExtension(e2.local.manifest);487if (!isE1Running && !isE2Running) {488if (isE1LanguagePackExtension) {489return -1;490}491if (isE2LanguagePackExtension) {492return 1;493}494return e1.displayName.localeCompare(e2.displayName);495}496if ((isE1Running && isE2LanguagePackExtension) || (isE2Running && isE1LanguagePackExtension)) {497return e1.displayName.localeCompare(e2.displayName);498}499return isE1Running ? -1 : 1;500};501502const incompatible: IExtension[] = [];503const deprecated: IExtension[] = [];504const outdated: IExtension[] = [];505const actionRequired: IExtension[] = [];506const noActionRequired: IExtension[] = [];507508for (const e of result) {509if (e.enablementState === EnablementState.DisabledByInvalidExtension) {510incompatible.push(e);511}512else if (e.deprecationInfo) {513deprecated.push(e);514}515else if (e.outdated && this.extensionEnablementService.isEnabledEnablementState(e.enablementState)) {516outdated.push(e);517}518else if (e.runtimeState) {519actionRequired.push(e);520}521else {522noActionRequired.push(e);523}524}525526result = [527...incompatible.sort(defaultSort),528...deprecated.sort(defaultSort),529...outdated.sort(defaultSort),530...actionRequired.sort(defaultSort),531...noActionRequired.sort(defaultSort)532];533}534return result;535}536537private filterOutdatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {538let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);539540value = value.replace(/@outdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();541542const result = local543.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))544.filter(extension => extension.outdated545&& (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)546&& this.filterExtensionByCategory(extension, includedCategories, excludedCategories));547548return this.sortExtensions(result, options);549}550551private filterDisabledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions, includeBuiltin: boolean): IExtension[] {552let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);553554value = value.replaceAll(/@disabled|@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();555556if (includeBuiltin) {557local = local.filter(e => e.isBuiltin);558}559const result = local560.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))561.filter(e => runningExtensions.every(r => !areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))562&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)563&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));564565return this.sortExtensions(result, options);566}567568private filterEnabledExtensions(local: IExtension[], runningExtensions: readonly IExtensionDescription[], query: Query, options: IQueryOptions, includeBuiltin: boolean): IExtension[] {569let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);570571value = value ? value.replaceAll(/@enabled|@builtin/gi, '').replaceAll(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase() : '';572573local = local.filter(e => e.isBuiltin === includeBuiltin);574const result = local575.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName))576.filter(e => runningExtensions.some(r => areSameExtensions({ id: r.identifier.value, uuid: r.uuid }, e.identifier))577&& (e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)578&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));579580return this.sortExtensions(result, options);581}582583private filterWorkspaceUnsupportedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {584// shows local extensions which are restricted or disabled in the current workspace because of the extension's capability585586const queryString = query.value; // @sortby is already filtered out587588const match = queryString.match(/^\s*@workspaceUnsupported(?::(untrusted|virtual)(Partial)?)?(?:\s+([^\s]*))?/i);589if (!match) {590return [];591}592const type = match[1]?.toLowerCase();593const partial = !!match[2];594const nameFilter = match[3]?.toLowerCase();595596if (nameFilter) {597local = local.filter(extension => extension.name.toLowerCase().indexOf(nameFilter) > -1 || extension.displayName.toLowerCase().indexOf(nameFilter) > -1);598}599600const hasVirtualSupportType = (extension: IExtension, supportType: ExtensionVirtualWorkspaceSupportType) => {601return extension.local && this.extensionManifestPropertiesService.getExtensionVirtualWorkspaceSupportType(extension.local.manifest) === supportType;602};603604const hasRestrictedSupportType = (extension: IExtension, supportType: ExtensionUntrustedWorkspaceSupportType) => {605if (!extension.local) {606return false;607}608609const enablementState = this.extensionEnablementService.getEnablementState(extension.local);610if (enablementState !== EnablementState.EnabledGlobally && enablementState !== EnablementState.EnabledWorkspace &&611enablementState !== EnablementState.DisabledByTrustRequirement && enablementState !== EnablementState.DisabledByExtensionDependency) {612return false;613}614615if (this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(extension.local.manifest) === supportType) {616return true;617}618619if (supportType === false) {620const dependencies = getExtensionDependencies(local.map(ext => ext.local!), extension.local);621return dependencies.some(ext => this.extensionManifestPropertiesService.getExtensionUntrustedWorkspaceSupportType(ext.manifest) === supportType);622}623624return false;625};626627const inVirtualWorkspace = isVirtualWorkspace(this.workspaceService.getWorkspace());628const inRestrictedWorkspace = !this.workspaceTrustManagementService.isWorkspaceTrusted();629630if (type === 'virtual') {631// show limited and disabled extensions unless disabled because of a untrusted workspace632local = local.filter(extension => inVirtualWorkspace && hasVirtualSupportType(extension, partial ? 'limited' : false) && !(inRestrictedWorkspace && hasRestrictedSupportType(extension, false)));633} else if (type === 'untrusted') {634// show limited and disabled extensions unless disabled because of a virtual workspace635local = local.filter(extension => hasRestrictedSupportType(extension, partial ? 'limited' : false) && !(inVirtualWorkspace && hasVirtualSupportType(extension, false)));636} else {637// show extensions that are restricted or disabled in the current workspace638local = local.filter(extension => inVirtualWorkspace && !hasVirtualSupportType(extension, true) || inRestrictedWorkspace && !hasRestrictedSupportType(extension, true));639}640return this.sortExtensions(local, options);641}642643private async filterDeprecatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): Promise<IExtension[]> {644const value = query.value.replace(/@deprecated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();645const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest();646const deprecatedExtensionIds = Object.keys(extensionsControlManifest.deprecated);647local = local.filter(e => deprecatedExtensionIds.includes(e.identifier.id) && (!value || e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1));648return this.sortExtensions(local, options);649}650651private filterRecentlyUpdatedExtensions(local: IExtension[], query: Query, options: IQueryOptions): IExtension[] {652let { value, includedCategories, excludedCategories } = this.parseCategories(query.value);653const currentTime = Date.now();654local = local.filter(e => !e.isBuiltin && !e.outdated && e.local?.updated && e.local?.installedTimestamp !== undefined && currentTime - e.local.installedTimestamp < ExtensionsListView.RECENT_UPDATE_DURATION);655656value = value.replace(/@recentlyUpdated/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase();657658const result = local.filter(e =>659(e.name.toLowerCase().indexOf(value) > -1 || e.displayName.toLowerCase().indexOf(value) > -1)660&& this.filterExtensionByCategory(e, includedCategories, excludedCategories));661662options.sortBy = options.sortBy ?? LocalSortBy.UpdateDate;663664return this.sortExtensions(result, options);665}666667private filterExtensionsByFeature(local: IExtension[], query: Query): { extensions: IExtension[]; description: string } | undefined {668const value = query.value.replace(/@feature:/g, '').trim();669const featureId = value.split(' ')[0];670const feature = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(featureId);671if (!feature) {672return undefined;673}674if (this.extensionsViewState) {675this.extensionsViewState.filters.featureId = featureId;676}677const renderer = feature.renderer ? this.instantiationService.createInstance<IExtensionFeatureRenderer>(feature.renderer) : undefined;678try {679const result: [IExtension, number][] = [];680for (const e of local) {681if (!e.local) {682continue;683}684const accessData = this.extensionFeaturesManagementService.getAccessData(new ExtensionIdentifier(e.identifier.id), featureId);685const shouldRender = renderer?.shouldRender(e.local.manifest);686if (accessData || shouldRender) {687result.push([e, accessData?.accessTimes.length ?? 0]);688}689}690return {691extensions: result.sort(([, a], [, b]) => b - a).map(([e]) => e),692description: localize('showingExtensionsForFeature', "Extensions using {0} in the last 30 days", feature.label)693};694} finally {695renderer?.dispose();696}697}698699private mergeAddedExtensions(extensions: IExtension[], newExtensions: IExtension[]): IExtension[] | undefined {700const oldExtensions = [...extensions];701const findPreviousExtensionIndex = (from: number): number => {702let index = -1;703const previousExtensionInNew = newExtensions[from];704if (previousExtensionInNew) {705index = oldExtensions.findIndex(e => areSameExtensions(e.identifier, previousExtensionInNew.identifier));706if (index === -1) {707return findPreviousExtensionIndex(from - 1);708}709}710return index;711};712713let hasChanged: boolean = false;714for (let index = 0; index < newExtensions.length; index++) {715const extension = newExtensions[index];716if (extensions.every(r => !areSameExtensions(r.identifier, extension.identifier))) {717hasChanged = true;718extensions.splice(findPreviousExtensionIndex(index - 1) + 1, 0, extension);719}720}721722return hasChanged ? extensions : undefined;723}724725private async queryGallery(query: Query, options: IGalleryQueryOptions, token: CancellationToken): Promise<IQueryResult> {726const hasUserDefinedSortOrder = options.sortBy !== undefined;727if (!hasUserDefinedSortOrder && !query.value.trim()) {728options.sortBy = GallerySortBy.InstallCount;729}730731if (this.isRecommendationsQuery(query)) {732const model = await this.queryRecommendations(query, options, token);733return { model, disposables: new DisposableStore() };734}735736const text = query.value;737738if (!text) {739options.source = 'viewlet';740const pager = await this.extensionsWorkbenchService.queryGallery(options, token);741return { model: new PagedModel(pager), disposables: new DisposableStore() };742}743744if (/\bext:([^\s]+)\b/g.test(text)) {745options.text = text;746options.source = 'file-extension-tags';747const pager = await this.extensionsWorkbenchService.queryGallery(options, token);748return { model: new PagedModel(pager), disposables: new DisposableStore() };749}750751options.text = text.substring(0, 350);752options.source = 'searchText';753754if (hasUserDefinedSortOrder || /\b(category|tag):([^\s]+)\b/gi.test(text) || /\bfeatured(\s+|\b|$)/gi.test(text)) {755const pager = await this.extensionsWorkbenchService.queryGallery(options, token);756return { model: new PagedModel(pager), disposables: new DisposableStore() };757}758759try {760const [pager, preferredExtensions] = await Promise.all([761this.extensionsWorkbenchService.queryGallery(options, token),762this.getPreferredExtensions(options.text.toLowerCase(), token).catch(() => [])763]);764765const model = preferredExtensions.length ? new PreferredExtensionsPagedModel(preferredExtensions, pager) : new PagedModel(pager);766return { model, disposables: new DisposableStore() };767} catch (error) {768if (isCancellationError(error)) {769throw error;770}771772if (!(error instanceof ExtensionGalleryError)) {773throw error;774}775776const searchText = options.text.toLowerCase();777const 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));778if (localExtensions.length) {779const message = this.getMessage(error);780return { model: new PagedModel(localExtensions), disposables: new DisposableStore(), message: { text: localize('showing local extensions only', "{0} Showing local extensions.", message.text), severity: message.severity } };781}782783throw error;784}785}786787private async getPreferredExtensions(searchText: string, token: CancellationToken): Promise<IExtension[]> {788const 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));789const preferredExtensionUUIDs = new Set<string>();790791if (preferredExtensions.length) {792// Update gallery data for preferred extensions if they are not yet fetched793const extesionsToFetch: IExtensionIdentifier[] = [];794for (const extension of preferredExtensions) {795if (extension.identifier.uuid) {796preferredExtensionUUIDs.add(extension.identifier.uuid);797}798if (!extension.gallery && extension.identifier.uuid) {799extesionsToFetch.push(extension.identifier);800}801}802if (extesionsToFetch.length) {803this.extensionsWorkbenchService.getExtensions(extesionsToFetch, CancellationToken.None).catch(e => null/*ignore error*/);804}805}806807const preferredResults: string[] = [];808try {809const manifest = await this.extensionManagementService.getExtensionsControlManifest();810if (Array.isArray(manifest.search)) {811for (const s of manifest.search) {812if (s.query && s.query.toLowerCase() === searchText && Array.isArray(s.preferredResults)) {813preferredResults.push(...s.preferredResults);814break;815}816}817}818if (preferredResults.length) {819const result = await this.extensionsWorkbenchService.getExtensions(preferredResults.map(id => ({ id })), token);820for (const extension of result) {821if (extension.identifier.uuid && !preferredExtensionUUIDs.has(extension.identifier.uuid)) {822preferredExtensions.push(extension);823}824}825}826} catch (e) {827this.logService.warn('Failed to get preferred results from the extensions control manifest.', e);828}829830return preferredExtensions;831}832833private sortExtensions(extensions: IExtension[], options: IQueryOptions): IExtension[] {834switch (options.sortBy) {835case GallerySortBy.InstallCount:836extensions = extensions.sort((e1, e2) => typeof e2.installCount === 'number' && typeof e1.installCount === 'number' ? e2.installCount - e1.installCount : NaN);837break;838case LocalSortBy.UpdateDate:839extensions = extensions.sort((e1, e2) =>840typeof e2.local?.installedTimestamp === 'number' && typeof e1.local?.installedTimestamp === 'number' ? e2.local.installedTimestamp - e1.local.installedTimestamp :841typeof e2.local?.installedTimestamp === 'number' ? 1 :842typeof e1.local?.installedTimestamp === 'number' ? -1 : NaN);843break;844case GallerySortBy.AverageRating:845case GallerySortBy.WeightedRating:846extensions = extensions.sort((e1, e2) => typeof e2.rating === 'number' && typeof e1.rating === 'number' ? e2.rating - e1.rating : NaN);847break;848default:849extensions = extensions.sort((e1, e2) => e1.displayName.localeCompare(e2.displayName));850break;851}852if (options.sortOrder === SortOrder.Descending) {853extensions = extensions.reverse();854}855return extensions;856}857858private isRecommendationsQuery(query: Query): boolean {859return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)860|| ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)861|| ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value)862|| ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)863|| ExtensionsListView.isRemoteRecommendedExtensionsQuery(query.value)864|| /@recommended:all/i.test(query.value)865|| ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)866|| ExtensionsListView.isRecommendedExtensionsQuery(query.value);867}868869private async queryRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {870// Workspace recommendations871if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {872return this.getWorkspaceRecommendationsModel(query, options, token);873}874875// Keymap recommendations876if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {877return this.getKeymapRecommendationsModel(query, options, token);878}879880// Language recommendations881if (ExtensionsListView.isLanguageRecommendedExtensionsQuery(query.value)) {882return this.getLanguageRecommendationsModel(query, options, token);883}884885// Exe recommendations886if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) {887return this.getExeRecommendationsModel(query, options, token);888}889890// Remote recommendations891if (ExtensionsListView.isRemoteRecommendedExtensionsQuery(query.value)) {892return this.getRemoteRecommendationsModel(query, options, token);893}894895// All recommendations896if (/@recommended:all/i.test(query.value)) {897return this.getAllRecommendationsModel(options, token);898}899900// Search recommendations901if (ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) ||902(ExtensionsListView.isRecommendedExtensionsQuery(query.value) && options.sortBy !== undefined)) {903return this.searchRecommendations(query, options, token);904}905906// Other recommendations907if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {908return this.getOtherRecommendationsModel(query, options, token);909}910911return new PagedModel([]);912}913914protected async getInstallableRecommendations(recommendations: Array<string | URI>, options: IQueryOptions, token: CancellationToken): Promise<IExtension[]> {915const result: IExtension[] = [];916if (recommendations.length) {917const galleryExtensions: string[] = [];918const resourceExtensions: URI[] = [];919for (const recommendation of recommendations) {920if (typeof recommendation === 'string') {921galleryExtensions.push(recommendation);922} else {923resourceExtensions.push(recommendation);924}925}926if (galleryExtensions.length) {927try {928const extensions = await this.extensionsWorkbenchService.getExtensions(galleryExtensions.map(id => ({ id })), { source: options.source }, token);929for (const extension of extensions) {930if (extension.gallery && !extension.deprecationInfo931&& await this.extensionManagementService.canInstall(extension.gallery) === true) {932result.push(extension);933}934}935} catch (error) {936if (!resourceExtensions.length || !this.isOfflineError(error)) {937throw error;938}939}940}941if (resourceExtensions.length) {942const extensions = await this.extensionsWorkbenchService.getResourceExtensions(resourceExtensions, true);943for (const extension of extensions) {944if (await this.extensionsWorkbenchService.canInstall(extension) === true) {945result.push(extension);946}947}948}949}950return result;951}952953protected async getWorkspaceRecommendations(): Promise<Array<string | URI>> {954const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations();955const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations();956for (const configBasedRecommendation of important) {957if (!recommendations.find(extensionId => extensionId === configBasedRecommendation)) {958recommendations.push(configBasedRecommendation);959}960}961return recommendations;962}963964private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {965const recommendations = await this.getWorkspaceRecommendations();966const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token));967return new PagedModel(installableRecommendations);968}969970private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {971const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase();972const recommendations = this.extensionRecommendationsService.getKeymapRecommendations();973const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-keymaps' }, token))974.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);975return new PagedModel(installableRecommendations);976}977978private async getLanguageRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {979const value = query.value.replace(/@recommended:languages/g, '').trim().toLowerCase();980const recommendations = this.extensionRecommendationsService.getLanguageRecommendations();981const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-languages' }, token))982.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);983return new PagedModel(installableRecommendations);984}985986private async getRemoteRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {987const value = query.value.replace(/@recommended:remotes/g, '').trim().toLowerCase();988const recommendations = this.extensionRecommendationsService.getRemoteRecommendations();989const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-remotes' }, token))990.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);991return new PagedModel(installableRecommendations);992}993994private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {995const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase();996const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe);997const installableRecommendations = await this.getInstallableRecommendations([...important, ...others], { ...options, source: 'recommendations-exe' }, token);998return new PagedModel(installableRecommendations);999}10001001private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {1002const otherRecommendations = await this.getOtherRecommendations();1003const installableRecommendations = await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token);1004const result = coalesce(otherRecommendations.map(id => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));1005return new PagedModel(result);1006}10071008private async getOtherRecommendations(): Promise<string[]> {1009const local = (await this.extensionsWorkbenchService.queryLocal(this.options.server))1010.map(e => e.identifier.id.toLowerCase());1011const workspaceRecommendations = (await this.getWorkspaceRecommendations())1012.map(extensionId => isString(extensionId) ? extensionId.toLowerCase() : extensionId);10131014return distinct(1015(await Promise.all([1016// Order is important1017this.extensionRecommendationsService.getImportantRecommendations(),1018this.extensionRecommendationsService.getFileBasedRecommendations(),1019this.extensionRecommendationsService.getOtherRecommendations()1020])).flat().filter(extensionId => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase())1021), extensionId => extensionId.toLowerCase());1022}10231024// Get All types of recommendations, trimmed to show a max of 8 at any given time1025private async getAllRecommendationsModel(options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {1026const localExtensions = await this.extensionsWorkbenchService.queryLocal(this.options.server);1027const localExtensionIds = localExtensions.map(e => e.identifier.id.toLowerCase());10281029const allRecommendations = distinct(1030(await Promise.all([1031// Order is important1032this.getWorkspaceRecommendations(),1033this.extensionRecommendationsService.getImportantRecommendations(),1034this.extensionRecommendationsService.getFileBasedRecommendations(),1035this.extensionRecommendationsService.getOtherRecommendations()1036])).flat().filter(extensionId => {1037if (isString(extensionId)) {1038return !localExtensionIds.includes(extensionId.toLowerCase());1039}1040return !localExtensions.some(localExtension => localExtension.local && this.uriIdentityService.extUri.isEqual(localExtension.local.location, extensionId));1041}));10421043const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token);10441045const result: IExtension[] = [];1046for (let i = 0; i < installableRecommendations.length && result.length < 8; i++) {1047const recommendation = allRecommendations[i];1048if (isString(recommendation)) {1049const extension = installableRecommendations.find(extension => areSameExtensions(extension.identifier, { id: recommendation }));1050if (extension) {1051result.push(extension);1052}1053} else {1054const extension = installableRecommendations.find(extension => extension.resourceExtension && this.uriIdentityService.extUri.isEqual(extension.resourceExtension.location, recommendation));1055if (extension) {1056result.push(extension);1057}1058}1059}10601061return new PagedModel(result);1062}10631064private async searchRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {1065const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();1066const recommendations = distinct([...await this.getWorkspaceRecommendations(), ...await this.getOtherRecommendations()]);1067const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations', sortBy: undefined }, token))1068.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);1069return new PagedModel(this.sortExtensions(installableRecommendations, options));1070}10711072private setModel(model: IPagedModel<IExtension>, message?: Message, donotResetScrollTop?: boolean) {1073if (this.list) {1074this.list.model = new DelayedPagedModel(model);1075this.updateBody(message);1076if (!donotResetScrollTop) {1077this.list.scrollTop = 0;1078}1079}1080if (this.badge) {1081this.badge.setCount(this.count());1082}1083}10841085private updateModel(model: IPagedModel<IExtension>) {1086if (this.list) {1087this.list.model = new DelayedPagedModel(model);1088this.updateBody();1089}1090if (this.badge) {1091this.badge.setCount(this.count());1092}1093}10941095private updateBody(message?: Message): void {1096if (this.bodyTemplate) {10971098const count = this.count();1099this.bodyTemplate.extensionsList.classList.toggle('hidden', count === 0);1100this.bodyTemplate.messageContainer.classList.toggle('hidden', !message && count > 0);11011102if (this.isBodyVisible()) {1103if (message) {1104this.bodyTemplate.messageSeverityIcon.className = SeverityIcon.className(message.severity);1105this.bodyTemplate.messageBox.textContent = message.text;1106} else if (this.count() === 0) {1107this.bodyTemplate.messageSeverityIcon.className = '';1108this.bodyTemplate.messageBox.textContent = localize('no extensions found', "No extensions found.");1109}1110if (this.bodyTemplate.messageBox.textContent) {1111alert(this.bodyTemplate.messageBox.textContent);1112}1113}1114}11151116this.updateSize();1117}11181119private getMessage(error: any): Message {1120if (this.isOfflineError(error)) {1121return { text: localize('offline error', "Unable to search the Marketplace when offline, please check your network connection."), severity: Severity.Warning };1122} else {1123return { text: localize('error', "Error while fetching extensions. {0}", getErrorMessage(error)), severity: Severity.Error };1124}1125}11261127private isOfflineError(error: Error): boolean {1128if (error instanceof ExtensionGalleryError) {1129return error.code === ExtensionGalleryErrorCode.Offline;1130}1131return isOfflineError(error);1132}11331134protected updateSize() {1135if (this.options.flexibleHeight) {1136this.maximumBodySize = this.list?.model.length ? Number.POSITIVE_INFINITY : 0;1137this.storageService.store(`${this.id}.size`, this.list?.model.length || 0, StorageScope.PROFILE, StorageTarget.MACHINE);1138}1139}11401141override dispose(): void {1142super.dispose();1143if (this.queryRequest) {1144this.queryRequest.request.cancel();1145this.queryRequest = null;1146}1147if (this.queryResult) {1148this.queryResult.disposables.dispose();1149this.queryResult = undefined;1150}1151this.list = null;1152}11531154static isLocalExtensionsQuery(query: string, sortBy?: string): boolean {1155return this.isInstalledExtensionsQuery(query)1156|| this.isSearchInstalledExtensionsQuery(query)1157|| this.isOutdatedExtensionsQuery(query)1158|| this.isEnabledExtensionsQuery(query)1159|| this.isDisabledExtensionsQuery(query)1160|| this.isBuiltInExtensionsQuery(query)1161|| this.isSearchBuiltInExtensionsQuery(query)1162|| this.isBuiltInGroupExtensionsQuery(query)1163|| this.isSearchDeprecatedExtensionsQuery(query)1164|| this.isSearchWorkspaceUnsupportedExtensionsQuery(query)1165|| this.isSearchRecentlyUpdatedQuery(query)1166|| this.isSearchExtensionUpdatesQuery(query)1167|| this.isSortInstalledExtensionsQuery(query, sortBy)1168|| this.isFeatureExtensionsQuery(query);1169}11701171static isSearchBuiltInExtensionsQuery(query: string): boolean {1172return /@builtin\s.+|.+\s@builtin/i.test(query);1173}11741175static isBuiltInExtensionsQuery(query: string): boolean {1176return /^@builtin$/i.test(query.trim());1177}11781179static isBuiltInGroupExtensionsQuery(query: string): boolean {1180return /^@builtin:.+$/i.test(query.trim());1181}11821183static isSearchWorkspaceUnsupportedExtensionsQuery(query: string): boolean {1184return /^\s*@workspaceUnsupported(:(untrusted|virtual)(Partial)?)?(\s|$)/i.test(query);1185}11861187static isInstalledExtensionsQuery(query: string): boolean {1188return /@installed$/i.test(query);1189}11901191static isSearchInstalledExtensionsQuery(query: string): boolean {1192return /@installed\s./i.test(query) || this.isFeatureExtensionsQuery(query);1193}11941195static isOutdatedExtensionsQuery(query: string): boolean {1196return /@outdated/i.test(query);1197}11981199static isEnabledExtensionsQuery(query: string): boolean {1200return /@enabled/i.test(query) && !/@builtin/i.test(query);1201}12021203static isDisabledExtensionsQuery(query: string): boolean {1204return /@disabled/i.test(query) && !/@builtin/i.test(query);1205}12061207static isSearchDeprecatedExtensionsQuery(query: string): boolean {1208return /@deprecated\s?.*/i.test(query);1209}12101211static isRecommendedExtensionsQuery(query: string): boolean {1212return /^@recommended$/i.test(query.trim());1213}12141215static isSearchRecommendedExtensionsQuery(query: string): boolean {1216return /@recommended\s.+/i.test(query);1217}12181219static isWorkspaceRecommendedExtensionsQuery(query: string): boolean {1220return /@recommended:workspace/i.test(query);1221}12221223static isExeRecommendedExtensionsQuery(query: string): boolean {1224return /@exe:.+/i.test(query);1225}12261227static isRemoteRecommendedExtensionsQuery(query: string): boolean {1228return /@recommended:remotes/i.test(query);1229}12301231static isKeymapsRecommendedExtensionsQuery(query: string): boolean {1232return /@recommended:keymaps/i.test(query);1233}12341235static isLanguageRecommendedExtensionsQuery(query: string): boolean {1236return /@recommended:languages/i.test(query);1237}12381239static isSortInstalledExtensionsQuery(query: string, sortBy?: string): boolean {1240return (sortBy !== undefined && sortBy !== '' && query === '') || (!sortBy && /^@sort:\S*$/i.test(query));1241}12421243static isSearchPopularQuery(query: string): boolean {1244return /@popular/i.test(query);1245}12461247static isSearchRecentlyPublishedQuery(query: string): boolean {1248return /@recentlyPublished/i.test(query);1249}12501251static isSearchRecentlyUpdatedQuery(query: string): boolean {1252return /@recentlyUpdated/i.test(query);1253}12541255static isSearchExtensionUpdatesQuery(query: string): boolean {1256return /@updates/i.test(query);1257}12581259static isSortUpdateDateQuery(query: string): boolean {1260return /@sort:updateDate/i.test(query);1261}12621263static isFeatureExtensionsQuery(query: string): boolean {1264return /@feature:/i.test(query);1265}12661267override focus(): void {1268super.focus();1269if (!this.list) {1270return;1271}12721273if (!(this.list.getFocus().length || this.list.getSelection().length)) {1274this.list.focusNext();1275}1276this.list.domFocus();1277}1278}12791280export class DefaultPopularExtensionsView extends ExtensionsListView {12811282override async show(): Promise<IPagedModel<IExtension>> {1283const query = this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer ? '@web' : '';1284return super.show(query);1285}12861287}12881289export class ServerInstalledExtensionsView extends ExtensionsListView {12901291override async show(query: string): Promise<IPagedModel<IExtension>> {1292query = query ? query : '@installed';1293if (!ExtensionsListView.isLocalExtensionsQuery(query) || ExtensionsListView.isSortInstalledExtensionsQuery(query)) {1294query = query += ' @installed';1295}1296return super.show(query.trim());1297}12981299}13001301export class EnabledExtensionsView extends ExtensionsListView {13021303override async show(query: string): Promise<IPagedModel<IExtension>> {1304query = query || '@enabled';1305return ExtensionsListView.isEnabledExtensionsQuery(query) ? super.show(query) :1306ExtensionsListView.isSortInstalledExtensionsQuery(query) ? super.show('@enabled ' + query) : this.showEmptyModel();1307}1308}13091310export class DisabledExtensionsView extends ExtensionsListView {13111312override async show(query: string): Promise<IPagedModel<IExtension>> {1313query = query || '@disabled';1314return ExtensionsListView.isDisabledExtensionsQuery(query) ? super.show(query) :1315ExtensionsListView.isSortInstalledExtensionsQuery(query) ? super.show('@disabled ' + query) : this.showEmptyModel();1316}1317}13181319export class OutdatedExtensionsView extends ExtensionsListView {13201321override async show(query: string): Promise<IPagedModel<IExtension>> {1322query = query ? query : '@outdated';1323if (ExtensionsListView.isSearchExtensionUpdatesQuery(query)) {1324query = query.replace('@updates', '@outdated');1325}1326return super.show(query.trim());1327}13281329protected override updateSize() {1330super.updateSize();1331this.setExpanded(this.count() > 0);1332}13331334}13351336export class RecentlyUpdatedExtensionsView extends ExtensionsListView {13371338override async show(query: string): Promise<IPagedModel<IExtension>> {1339query = query ? query : '@recentlyUpdated';1340if (ExtensionsListView.isSearchExtensionUpdatesQuery(query)) {1341query = query.replace('@updates', '@recentlyUpdated');1342}1343return super.show(query.trim());1344}13451346}13471348export interface StaticQueryExtensionsViewOptions extends ExtensionsListViewOptions {1349readonly query: string;1350}13511352export class StaticQueryExtensionsView extends ExtensionsListView {13531354constructor(1355protected override readonly options: StaticQueryExtensionsViewOptions,1356viewletViewOptions: IViewletViewOptions,1357@INotificationService notificationService: INotificationService,1358@IKeybindingService keybindingService: IKeybindingService,1359@IContextMenuService contextMenuService: IContextMenuService,1360@IInstantiationService instantiationService: IInstantiationService,1361@IThemeService themeService: IThemeService,1362@IExtensionService extensionService: IExtensionService,1363@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,1364@IExtensionRecommendationsService extensionRecommendationsService: IExtensionRecommendationsService,1365@ITelemetryService telemetryService: ITelemetryService,1366@IHoverService hoverService: IHoverService,1367@IConfigurationService configurationService: IConfigurationService,1368@IWorkspaceContextService contextService: IWorkspaceContextService,1369@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,1370@IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService,1371@IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService,1372@IWorkspaceContextService workspaceService: IWorkspaceContextService,1373@IProductService productService: IProductService,1374@IContextKeyService contextKeyService: IContextKeyService,1375@IViewDescriptorService viewDescriptorService: IViewDescriptorService,1376@IOpenerService openerService: IOpenerService,1377@IStorageService storageService: IStorageService,1378@IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService,1379@IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService,1380@IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService,1381@IUriIdentityService uriIdentityService: IUriIdentityService,1382@ILogService logService: ILogService1383) {1384super(options, viewletViewOptions, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService,1385extensionsWorkbenchService, extensionRecommendationsService, telemetryService, hoverService, configurationService, contextService, extensionManagementServerService,1386extensionManifestPropertiesService, extensionManagementService, workspaceService, productService, contextKeyService, viewDescriptorService, openerService,1387storageService, workspaceTrustManagementService, extensionEnablementService, extensionFeaturesManagementService,1388uriIdentityService, logService);1389}13901391override show(): Promise<IPagedModel<IExtension>> {1392return super.show(this.options.query);1393}1394}13951396function toSpecificWorkspaceUnsupportedQuery(query: string, qualifier: string): string | undefined {1397if (!query) {1398return '@workspaceUnsupported:' + qualifier;1399}1400const match = query.match(new RegExp(`@workspaceUnsupported(:${qualifier})?(\\s|$)`, 'i'));1401if (match) {1402if (!match[1]) {1403return query.replace(/@workspaceUnsupported/gi, '@workspaceUnsupported:' + qualifier);1404}1405return query;1406}1407return undefined;1408}140914101411export class UntrustedWorkspaceUnsupportedExtensionsView extends ExtensionsListView {1412override async show(query: string): Promise<IPagedModel<IExtension>> {1413const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrusted');1414return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();1415}1416}14171418export class UntrustedWorkspacePartiallySupportedExtensionsView extends ExtensionsListView {1419override async show(query: string): Promise<IPagedModel<IExtension>> {1420const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'untrustedPartial');1421return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();1422}1423}14241425export class VirtualWorkspaceUnsupportedExtensionsView extends ExtensionsListView {1426override async show(query: string): Promise<IPagedModel<IExtension>> {1427const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtual');1428return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();1429}1430}14311432export class VirtualWorkspacePartiallySupportedExtensionsView extends ExtensionsListView {1433override async show(query: string): Promise<IPagedModel<IExtension>> {1434const updatedQuery = toSpecificWorkspaceUnsupportedQuery(query, 'virtualPartial');1435return updatedQuery ? super.show(updatedQuery) : this.showEmptyModel();1436}1437}14381439export class DeprecatedExtensionsView extends ExtensionsListView {1440override async show(query: string): Promise<IPagedModel<IExtension>> {1441return ExtensionsListView.isSearchDeprecatedExtensionsQuery(query) ? super.show(query) : this.showEmptyModel();1442}1443}14441445export class SearchMarketplaceExtensionsView extends ExtensionsListView {14461447private readonly reportSearchFinishedDelayer = this._register(new ThrottledDelayer(2000));1448private searchWaitPromise: Promise<void> = Promise.resolve();14491450override async show(query: string): Promise<IPagedModel<IExtension>> {1451const queryPromise = super.show(query);1452this.reportSearchFinishedDelayer.trigger(() => this.reportSearchFinished());1453this.searchWaitPromise = queryPromise.then(null, null);1454return queryPromise;1455}14561457private async reportSearchFinished(): Promise<void> {1458await this.searchWaitPromise;1459this.telemetryService.publicLog2('extensionsView:MarketplaceSearchFinished');1460}1461}14621463export class DefaultRecommendedExtensionsView extends ExtensionsListView {1464private readonly recommendedExtensionsQuery = '@recommended:all';14651466protected override renderBody(container: HTMLElement): void {1467super.renderBody(container);14681469this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => {1470this.show('');1471}));1472}14731474override async show(query: string): Promise<IPagedModel<IExtension>> {1475if (query && query.trim() !== this.recommendedExtensionsQuery) {1476return this.showEmptyModel();1477}1478const model = await super.show(this.recommendedExtensionsQuery);1479if (!this.extensionsWorkbenchService.local.some(e => !e.isBuiltin)) {1480// This is part of popular extensions view. Collapse if no installed extensions.1481this.setExpanded(model.length > 0);1482}1483return model;1484}14851486}14871488export class RecommendedExtensionsView extends ExtensionsListView {1489private readonly recommendedExtensionsQuery = '@recommended';14901491protected override renderBody(container: HTMLElement): void {1492super.renderBody(container);14931494this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => {1495this.show('');1496}));1497}14981499override async show(query: string): Promise<IPagedModel<IExtension>> {1500return (query && query.trim() !== this.recommendedExtensionsQuery) ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery);1501}1502}15031504export class WorkspaceRecommendedExtensionsView extends ExtensionsListView implements IWorkspaceRecommendedExtensionsView {1505private readonly recommendedExtensionsQuery = '@recommended:workspace';15061507protected override renderBody(container: HTMLElement): void {1508super.renderBody(container);15091510this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.show(this.recommendedExtensionsQuery)));1511this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery)));1512}15131514override async show(query: string): Promise<IPagedModel<IExtension>> {1515const shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace';1516const model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery));1517this.setExpanded(model.length > 0);1518return model;1519}15201521private async getInstallableWorkspaceRecommendations(): Promise<IExtension[]> {1522const installed = (await this.extensionsWorkbenchService.queryLocal())1523.filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind1524const recommendations = (await this.getWorkspaceRecommendations())1525.filter(recommendation => installed.every(local => isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.local?.location)));1526return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None);1527}15281529async installWorkspaceRecommendations(): Promise<void> {1530const installableRecommendations = await this.getInstallableWorkspaceRecommendations();1531if (installableRecommendations.length) {1532const galleryExtensions: InstallExtensionInfo[] = [];1533const resourceExtensions: IExtension[] = [];1534for (const recommendation of installableRecommendations) {1535if (recommendation.gallery) {1536galleryExtensions.push({ extension: recommendation.gallery, options: {} });1537} else {1538resourceExtensions.push(recommendation);1539}1540}1541await Promise.all([1542this.extensionManagementService.installGalleryExtensions(galleryExtensions),1543...resourceExtensions.map(extension => this.extensionsWorkbenchService.install(extension))1544]);1545} else {1546this.notificationService.notify({1547severity: Severity.Info,1548message: localize('no local extensions', "There are no extensions to install.")1549});1550}1551}15521553}15541555export class PreferredExtensionsPagedModel implements IPagedModel<IExtension> {15561557private readonly resolved = new Map<number, IExtension>();1558private preferredGalleryExtensions = new Set<string>();1559private resolvedGalleryExtensionsFromQuery: IExtension[] = [];1560private readonly pages: Array<{1561promise: Promise<void> | null;1562cts: CancellationTokenSource | null;1563promiseIndexes: Set<number>;1564}>;15651566public readonly length: number;15671568constructor(1569private readonly preferredExtensions: IExtension[],1570private readonly pager: IPager<IExtension>,1571) {1572for (let i = 0; i < this.preferredExtensions.length; i++) {1573this.resolved.set(i, this.preferredExtensions[i]);1574}15751576for (const e of preferredExtensions) {1577if (e.identifier.uuid) {1578this.preferredGalleryExtensions.add(e.identifier.uuid);1579}1580}15811582// expected that all preferred gallery extensions will be part of the query results1583this.length = (preferredExtensions.length - this.preferredGalleryExtensions.size) + this.pager.total;15841585const totalPages = Math.ceil(this.pager.total / this.pager.pageSize);1586this.populateResolvedExtensions(0, this.pager.firstPage);1587this.pages = range(totalPages - 1).map(() => ({1588promise: null,1589cts: null,1590promiseIndexes: new Set<number>(),1591}));1592}15931594isResolved(index: number): boolean {1595return this.resolved.has(index);1596}15971598get(index: number): IExtension {1599return this.resolved.get(index)!;1600}16011602async resolve(index: number, cancellationToken: CancellationToken): Promise<IExtension> {1603if (cancellationToken.isCancellationRequested) {1604throw new CancellationError();1605}16061607if (this.isResolved(index)) {1608return this.get(index);1609}16101611const indexInPagedModel = index - this.preferredExtensions.length + this.resolvedGalleryExtensionsFromQuery.length;1612const pageIndex = Math.floor(indexInPagedModel / this.pager.pageSize);1613const page = this.pages[pageIndex];16141615if (!page.promise) {1616page.cts = new CancellationTokenSource();1617page.promise = this.pager.getPage(pageIndex, page.cts.token)1618.then(extensions => this.populateResolvedExtensions(pageIndex, extensions))1619.catch(e => { page.promise = null; throw e; })1620.finally(() => page.cts = null);1621}16221623const listener = cancellationToken.onCancellationRequested(() => {1624if (!page.cts) {1625return;1626}1627page.promiseIndexes.delete(index);1628if (page.promiseIndexes.size === 0) {1629page.cts.cancel();1630}1631});16321633page.promiseIndexes.add(index);16341635try {1636await page.promise;1637} finally {1638listener.dispose();1639}16401641return this.get(index);1642}16431644private populateResolvedExtensions(pageIndex: number, extensions: IExtension[]): void {1645let adjustIndexOfNextPagesBy = 0;1646const pageStartIndex = pageIndex * this.pager.pageSize;1647for (let i = 0; i < extensions.length; i++) {1648const e = extensions[i];1649if (e.gallery?.identifier.uuid && this.preferredGalleryExtensions.has(e.gallery.identifier.uuid)) {1650this.resolvedGalleryExtensionsFromQuery.push(e);1651adjustIndexOfNextPagesBy++;1652} else {1653this.resolved.set(this.preferredExtensions.length - this.resolvedGalleryExtensionsFromQuery.length + pageStartIndex + i, e);1654}1655}1656// If this page has preferred gallery extensions, then adjust the index of the next pages1657// by the number of preferred gallery extensions found in this page. Because these preferred extensions1658// are already in the resolved list and since we did not add them now, we need to adjust the indices of the next pages.1659// Skip first page as the preferred extensions are always in the first page1660if (pageIndex !== 0 && adjustIndexOfNextPagesBy) {1661const nextPageStartIndex = (pageIndex + 1) * this.pager.pageSize;1662const indices = [...this.resolved.keys()].sort();1663for (const index of indices) {1664if (index >= nextPageStartIndex) {1665const e = this.resolved.get(index);1666if (e) {1667this.resolved.delete(index);1668this.resolved.set(index - adjustIndexOfNextPagesBy, e);1669}1670}1671}1672}1673}1674}167516761677