Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts
5221 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 './media/extensionsWidgets.css';6import * as semver from '../../../../base/common/semver/semver.js';7import { Disposable, toDisposable, DisposableStore, MutableDisposable, IDisposable } from '../../../../base/common/lifecycle.js';8import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, ExtensionState, ExtensionEditorTab, IExtensionsViewState } from '../common/extensions.js';9import { append, $, reset, addDisposableListener, EventType, finalHandler } from '../../../../base/browser/dom.js';10import * as platform from '../../../../base/common/platform.js';11import { localize } from '../../../../nls.js';12import { IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js';13import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';14import { ILabelService } from '../../../../platform/label/common/label.js';15import { extensionButtonProminentBackground, ExtensionStatusAction } from './extensionsActions.js';16import { IThemeService, registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';17import { ThemeIcon } from '../../../../base/common/themables.js';18import { EXTENSION_BADGE_BACKGROUND, EXTENSION_BADGE_FOREGROUND } from '../../../common/theme.js';19import { Emitter, Event } from '../../../../base/common/event.js';20import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';21import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js';22import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';23import { IUserDataSyncEnablementService } from '../../../../platform/userDataSync/common/userDataSync.js';24import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, privateExtensionIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, warningIcon } from './extensionsIcons.js';25import { registerColor, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js';26import { IHoverService } from '../../../../platform/hover/browser/hover.js';27import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';28import { createCommandUri, MarkdownString } from '../../../../base/common/htmlContent.js';29import { URI } from '../../../../base/common/uri.js';30import { IExtensionService } from '../../../services/extensions/common/extensions.js';31import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';32import Severity from '../../../../base/common/severity.js';33import { Color } from '../../../../base/common/color.js';34import { IOpenerService } from '../../../../platform/opener/common/opener.js';35import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';36import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';37import { KeyCode } from '../../../../base/common/keyCodes.js';38import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js';39import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';40import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';41import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';42import { Registry } from '../../../../platform/registry/common/platform.js';43import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js';44import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';45import { extensionDefaultIcon, extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js';46import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';47import { IExplorerService } from '../../files/browser/files.js';48import { IViewsService } from '../../../services/views/common/viewsService.js';49import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js';50import { IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';51import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';5253export abstract class ExtensionWidget extends Disposable implements IExtensionContainer {54private _extension: IExtension | null = null;55get extension(): IExtension | null { return this._extension; }56set extension(extension: IExtension | null) { this._extension = extension; this.update(); }57update(): void { this.render(); }58abstract render(): void;59}6061export function onClick(element: HTMLElement, callback: () => void): IDisposable {62const disposables: DisposableStore = new DisposableStore();63disposables.add(addDisposableListener(element, EventType.CLICK, finalHandler(callback)));64disposables.add(addDisposableListener(element, EventType.KEY_UP, e => {65const keyboardEvent = new StandardKeyboardEvent(e);66if (keyboardEvent.equals(KeyCode.Space) || keyboardEvent.equals(KeyCode.Enter)) {67e.preventDefault();68e.stopPropagation();69callback();70}71}));72return disposables;73}7475export class ExtensionIconWidget extends ExtensionWidget {7677private readonly iconLoadingDisposable = this._register(new MutableDisposable());78private readonly iconErrorDisposable = this._register(new MutableDisposable());79private readonly element: HTMLElement;80private readonly iconElement: HTMLImageElement;81private readonly defaultIconElement: HTMLElement;8283private iconUrl: string | undefined;8485constructor(86container: HTMLElement,87) {88super();89this.element = append(container, $('.extension-icon'));9091this.iconElement = append(this.element, $('img.icon', { alt: '' }));92this.iconElement.style.display = 'none';9394this.defaultIconElement = append(this.element, $(ThemeIcon.asCSSSelector(extensionDefaultIcon)));95this.defaultIconElement.style.display = 'none';9697this.render();98this._register(toDisposable(() => this.clear()));99}100101private clear(): void {102this.iconUrl = undefined;103this.iconElement.src = '';104this.iconElement.style.display = 'none';105this.defaultIconElement.style.display = 'none';106this.iconErrorDisposable.clear();107this.iconLoadingDisposable.clear();108}109110render(): void {111if (!this.extension) {112this.clear();113return;114}115116if (this.extension.iconUrl) {117if (this.iconUrl !== this.extension.iconUrl) {118this.iconElement.style.display = 'inherit';119this.defaultIconElement.style.display = 'none';120this.iconUrl = this.extension.iconUrl;121this.iconErrorDisposable.value = addDisposableListener(this.iconElement, 'error', () => {122if (this.extension?.iconUrlFallback) {123this.iconElement.src = this.extension.iconUrlFallback;124} else {125this.iconElement.style.display = 'none';126this.defaultIconElement.style.display = 'inherit';127}128}, { once: true });129this.iconElement.src = this.iconUrl;130if (!this.iconElement.complete) {131this.iconElement.style.visibility = 'hidden';132this.iconLoadingDisposable.value = addDisposableListener(this.iconElement, 'load', () => {133this.iconElement.style.visibility = 'inherit';134});135} else {136this.iconElement.style.visibility = 'inherit';137}138}139} else {140this.iconUrl = undefined;141this.iconElement.style.display = 'none';142this.iconElement.src = '';143this.defaultIconElement.style.display = 'inherit';144this.iconErrorDisposable.clear();145this.iconLoadingDisposable.clear();146}147}148}149150export class InstallCountWidget extends ExtensionWidget {151152private readonly disposables = this._register(new DisposableStore());153154constructor(155readonly container: HTMLElement,156private small: boolean,157@IHoverService private readonly hoverService: IHoverService,158) {159super();160this.render();161162this._register(toDisposable(() => this.clear()));163}164165private clear(): void {166this.container.innerText = '';167this.disposables.clear();168}169170render(): void {171this.clear();172173if (!this.extension) {174return;175}176177if (this.small && this.extension.state !== ExtensionState.Uninstalled) {178return;179}180181const installLabel = InstallCountWidget.getInstallLabel(this.extension, this.small);182if (!installLabel) {183return;184}185186const parent = this.small ? this.container : append(this.container, $('span.install', { tabIndex: 0 }));187append(parent, $('span' + ThemeIcon.asCSSSelector(installCountIcon)));188const count = append(parent, $('span.count'));189count.textContent = installLabel;190191if (!this.small) {192this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.container, localize('install count', "Install count")));193}194}195196static getInstallLabel(extension: IExtension, small: boolean): string | undefined {197const installCount = extension.installCount;198199if (!installCount) {200return undefined;201}202203let installLabel: string;204205if (small) {206if (installCount > 1000000) {207installLabel = `${Math.floor(installCount / 100000) / 10}M`;208} else if (installCount > 1000) {209installLabel = `${Math.floor(installCount / 1000)}K`;210} else {211installLabel = String(installCount);212}213}214else {215installLabel = installCount.toLocaleString(platform.language);216}217218return installLabel;219}220}221222export class RatingsWidget extends ExtensionWidget {223224private containerHover: IManagedHover | undefined;225private readonly disposables = this._register(new DisposableStore());226227constructor(228readonly container: HTMLElement,229private small: boolean,230@IHoverService private readonly hoverService: IHoverService,231@IOpenerService private readonly openerService: IOpenerService,232) {233super();234container.classList.add('extension-ratings');235236if (this.small) {237container.classList.add('small');238}239240this.render();241this._register(toDisposable(() => this.clear()));242}243244private clear(): void {245this.container.innerText = '';246this.disposables.clear();247}248249render(): void {250this.clear();251252if (!this.extension) {253return;254}255256if (this.small && this.extension.state !== ExtensionState.Uninstalled) {257return;258}259260if (this.extension.rating === undefined) {261return;262}263264if (this.small && !this.extension.ratingCount) {265return;266}267268if (!this.extension.url) {269return;270}271272const rating = Math.round(this.extension.rating * 2) / 2;273if (this.small) {274append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon)));275276const count = append(this.container, $('span.count'));277count.textContent = String(rating);278} else {279const element = append(this.container, $('span.rating.clickable', { tabIndex: 0 }));280for (let i = 1; i <= 5; i++) {281if (rating >= i) {282append(element, $('span' + ThemeIcon.asCSSSelector(starFullIcon)));283} else if (rating >= i - 0.5) {284append(element, $('span' + ThemeIcon.asCSSSelector(starHalfIcon)));285} else {286append(element, $('span' + ThemeIcon.asCSSSelector(starEmptyIcon)));287}288}289if (this.extension.ratingCount) {290const ratingCountElemet = append(element, $('span', undefined, ` (${this.extension.ratingCount})`));291ratingCountElemet.style.paddingLeft = '1px';292}293294this.containerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, ''));295this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating));296element.setAttribute('role', 'link');297if (this.extension.ratingUrl) {298this.disposables.add(onClick(element, () => this.openerService.open(URI.parse(this.extension!.ratingUrl!))));299}300}301}302303}304305export class PublisherWidget extends ExtensionWidget {306307private element: HTMLElement | undefined;308private containerHover: IManagedHover | undefined;309310private readonly disposables = this._register(new DisposableStore());311312constructor(313readonly container: HTMLElement,314private small: boolean,315@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,316@IHoverService private readonly hoverService: IHoverService,317@IOpenerService private readonly openerService: IOpenerService,318) {319super();320321this.render();322this._register(toDisposable(() => this.clear()));323}324325private clear(): void {326this.element?.remove();327this.disposables.clear();328}329330render(): void {331this.clear();332if (!this.extension) {333return;334}335336if (this.extension.resourceExtension) {337return;338}339340if (this.extension.local?.source === 'resource') {341return;342}343344this.element = append(this.container, $('.publisher'));345const publisherDisplayName = $('.publisher-name.ellipsis');346publisherDisplayName.textContent = this.extension.publisherDisplayName;347348const verifiedPublisher = $('.verified-publisher');349append(verifiedPublisher, $('span.extension-verified-publisher.clickable'), renderIcon(verifiedPublisherIcon));350351if (this.small) {352if (this.extension.publisherDomain?.verified) {353append(this.element, verifiedPublisher);354}355append(this.element, publisherDisplayName);356} else {357this.element.classList.toggle('clickable', !!this.extension.url);358this.element.setAttribute('role', 'button');359this.element.tabIndex = 0;360361this.containerHover = this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('publisher', "Publisher ({0})", this.extension.publisherDisplayName)));362append(this.element, publisherDisplayName);363364if (this.extension.publisherDomain?.verified) {365append(this.element, verifiedPublisher);366const publisherDomainLink = URI.parse(this.extension.publisherDomain.link);367verifiedPublisher.tabIndex = 0;368verifiedPublisher.setAttribute('role', 'button');369this.containerHover.update(localize('verified publisher', "This publisher has verified ownership of {0}", this.extension.publisherDomain.link));370verifiedPublisher.setAttribute('role', 'link');371372append(verifiedPublisher, $('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority));373this.disposables.add(onClick(verifiedPublisher, () => this.openerService.open(publisherDomainLink)));374}375376if (this.extension.url) {377this.disposables.add(onClick(this.element, () => this.extensionsWorkbenchService.openSearch(`publisher:"${this.extension?.publisherDisplayName}"`)));378}379}380381}382383}384385export class SponsorWidget extends ExtensionWidget {386387private readonly disposables = this._register(new DisposableStore());388389constructor(390readonly container: HTMLElement,391@IHoverService private readonly hoverService: IHoverService,392@IOpenerService private readonly openerService: IOpenerService,393) {394super();395this.render();396}397398render(): void {399reset(this.container);400this.disposables.clear();401if (!this.extension?.publisherSponsorLink) {402return;403}404405const sponsor = append(this.container, $('span.sponsor.clickable', { tabIndex: 0 }));406this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), sponsor, this.extension?.publisherSponsorLink.toString() ?? ''));407sponsor.setAttribute('role', 'link'); // #132645408const sponsorIconElement = renderIcon(sponsorIcon);409const label = $('span', undefined, localize('sponsor', "Sponsor"));410append(sponsor, sponsorIconElement, label);411this.disposables.add(onClick(sponsor, () => {412this.openerService.open(this.extension!.publisherSponsorLink!);413}));414}415}416417export class RecommendationWidget extends ExtensionWidget {418419private element?: HTMLElement;420private readonly disposables = this._register(new DisposableStore());421422constructor(423private parent: HTMLElement,424@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService425) {426super();427this.render();428this._register(toDisposable(() => this.clear()));429this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.render()));430}431432private clear(): void {433this.element?.remove();434this.element = undefined;435this.disposables.clear();436}437438render(): void {439this.clear();440if (!this.extension || this.extension.state === ExtensionState.Installed || this.extension.deprecationInfo) {441return;442}443const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();444if (extRecommendations[this.extension.identifier.id.toLowerCase()]) {445this.element = append(this.parent, $('div.extension-bookmark'));446const recommendation = append(this.element, $('.recommendation'));447append(recommendation, $('span' + ThemeIcon.asCSSSelector(ratingIcon)));448}449}450451}452453export class PreReleaseBookmarkWidget extends ExtensionWidget {454455private element?: HTMLElement;456private readonly disposables = this._register(new DisposableStore());457458constructor(459private parent: HTMLElement,460) {461super();462this.render();463this._register(toDisposable(() => this.clear()));464}465466private clear(): void {467this.element?.remove();468this.element = undefined;469this.disposables.clear();470}471472render(): void {473this.clear();474if (this.extension?.state === ExtensionState.Installed ? this.extension.preRelease : this.extension?.hasPreReleaseVersion) {475this.element = append(this.parent, $('div.extension-bookmark'));476const preRelease = append(this.element, $('.pre-release'));477append(preRelease, $('span' + ThemeIcon.asCSSSelector(preReleaseIcon)));478}479}480481}482483export class RemoteBadgeWidget extends ExtensionWidget {484485private readonly remoteBadge = this._register(new MutableDisposable<ExtensionIconBadge>());486487private element: HTMLElement;488489constructor(490parent: HTMLElement,491private readonly tooltip: boolean,492@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,493@IInstantiationService private readonly instantiationService: IInstantiationService494) {495super();496this.element = append(parent, $(''));497this.render();498this._register(toDisposable(() => this.clear()));499}500501private clear(): void {502this.remoteBadge.value?.element.remove();503this.remoteBadge.clear();504}505506render(): void {507this.clear();508if (!this.extension || !this.extension.local || !this.extension.server || !(this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) || this.extension.server !== this.extensionManagementServerService.remoteExtensionManagementServer) {509return;510}511let tooltip: string | undefined;512if (this.tooltip && this.extensionManagementServerService.remoteExtensionManagementServer) {513tooltip = localize('remote extension title', "Extension in {0}", this.extensionManagementServerService.remoteExtensionManagementServer.label);514}515this.remoteBadge.value = this.instantiationService.createInstance(ExtensionIconBadge, remoteIcon, tooltip);516append(this.element, this.remoteBadge.value.element);517}518}519520export class ExtensionIconBadge extends Disposable {521522readonly element: HTMLElement;523readonly elementHover: IManagedHover;524525constructor(526private readonly icon: ThemeIcon,527private readonly tooltip: string | undefined,528@IHoverService hoverService: IHoverService,529@ILabelService private readonly labelService: ILabelService,530@IThemeService private readonly themeService: IThemeService,531) {532super();533this.element = $('div.extension-badge.extension-icon-badge');534this.elementHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, ''));535this.render();536}537538private render(): void {539append(this.element, $('span' + ThemeIcon.asCSSSelector(this.icon)));540541const applyBadgeStyle = () => {542if (!this.element) {543return;544}545const bgColor = this.themeService.getColorTheme().getColor(EXTENSION_BADGE_BACKGROUND);546const fgColor = this.themeService.getColorTheme().getColor(EXTENSION_BADGE_FOREGROUND);547this.element.style.backgroundColor = bgColor ? bgColor.toString() : '';548this.element.style.color = fgColor ? fgColor.toString() : '';549};550applyBadgeStyle();551this._register(this.themeService.onDidColorThemeChange(() => applyBadgeStyle()));552553if (this.tooltip) {554const updateTitle = () => {555if (this.element) {556this.elementHover.update(this.tooltip);557}558};559this._register(this.labelService.onDidChangeFormatters(() => updateTitle()));560updateTitle();561}562}563}564565export class ExtensionPackCountWidget extends ExtensionWidget {566567private element: HTMLElement | undefined;568private countBadge: CountBadge | undefined;569570constructor(571private readonly parent: HTMLElement,572) {573super();574this.render();575this._register(toDisposable(() => this.clear()));576}577578private clear(): void {579this.element?.remove();580this.countBadge?.dispose();581this.countBadge = undefined;582}583584render(): void {585this.clear();586if (!this.extension || !(this.extension.categories?.some(category => category.toLowerCase() === 'extension packs')) || !this.extension.extensionPack.length) {587return;588}589this.element = append(this.parent, $('.extension-badge.extension-pack-badge'));590this.countBadge = new CountBadge(this.element, {}, defaultCountBadgeStyles);591this.countBadge.setCount(this.extension.extensionPack.length);592}593}594595export class ExtensionKindIndicatorWidget extends ExtensionWidget {596597private element: HTMLElement | undefined;598private extensionGalleryManifest: IExtensionGalleryManifest | null = null;599600private readonly disposables = this._register(new DisposableStore());601602constructor(603readonly container: HTMLElement,604private small: boolean,605@IHoverService private readonly hoverService: IHoverService,606@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,607@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,608@IExplorerService private readonly explorerService: IExplorerService,609@IViewsService private readonly viewsService: IViewsService,610@IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService,611) {612super();613this.render();614this._register(toDisposable(() => this.clear()));615extensionGalleryManifestService.getExtensionGalleryManifest().then(manifest => {616if (this._store.isDisposed) {617return;618}619this.extensionGalleryManifest = manifest;620this.render();621});622}623624private clear(): void {625this.element?.remove();626this.disposables.clear();627}628629render(): void {630this.clear();631632if (!this.extension) {633return;634}635636if (this.extension?.private) {637this.element = append(this.container, $('.extension-kind-indicator'));638if (!this.small || (this.extensionGalleryManifest?.capabilities.extensions?.includePublicExtensions && this.extensionGalleryManifest?.capabilities.extensions?.includePrivateExtensions)) {639append(this.element, $('span' + ThemeIcon.asCSSSelector(privateExtensionIcon)));640}641if (!this.small) {642append(this.element, $('span.private-extension-label', undefined, localize('privateExtension', "Private Extension")));643}644return;645}646647if (!this.small) {648return;649}650651const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined);652if (!location) {653return;654}655656this.element = append(this.container, $('.extension-kind-indicator'));657const workspaceFolder = this.contextService.getWorkspaceFolder(location);658if (workspaceFolder && this.extension.isWorkspaceScoped) {659this.element.textContent = localize('workspace extension', "Workspace Extension");660this.element.classList.add('clickable');661this.element.setAttribute('role', 'button');662this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location)));663this.disposables.add(onClick(this.element, () => {664this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true));665}));666} else {667this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, location.path));668this.element.textContent = localize('local extension', "Local Extension");669}670}671}672673export class SyncIgnoredWidget extends ExtensionWidget {674675private readonly disposables = this._register(new DisposableStore());676677constructor(678private readonly container: HTMLElement,679@IConfigurationService private readonly configurationService: IConfigurationService,680@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,681@IHoverService private readonly hoverService: IHoverService,682@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,683) {684super();685this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.ignoredExtensions'))(() => this.render()));686this._register(userDataSyncEnablementService.onDidChangeEnablement(() => this.update()));687this.render();688}689690render(): void {691this.disposables.clear();692this.container.innerText = '';693694if (this.extension && this.extension.state === ExtensionState.Installed && this.userDataSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) {695const element = append(this.container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon)));696this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, localize('syncingore.label', "This extension is ignored during sync.")));697element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon));698}699}700}701702export class ExtensionRuntimeStatusWidget extends ExtensionWidget {703704constructor(705private readonly extensionViewState: IExtensionsViewState,706private readonly container: HTMLElement,707@IExtensionService extensionService: IExtensionService,708@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,709@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,710) {711super();712this._register(extensionService.onDidChangeExtensionsStatus(extensions => {713if (this.extension && extensions.some(e => areSameExtensions({ id: e.value }, this.extension!.identifier))) {714this.update();715}716}));717this._register(extensionFeaturesManagementService.onDidChangeAccessData(e => {718if (this.extension && ExtensionIdentifier.equals(this.extension.identifier.id, e.extension)) {719this.update();720}721}));722}723724render(): void {725this.container.innerText = '';726727if (!this.extension) {728return;729}730731if (this.extensionViewState.filters.featureId && this.extension.state === ExtensionState.Installed) {732const accessData = this.extensionFeaturesManagementService.getAllAccessDataForExtension(new ExtensionIdentifier(this.extension.identifier.id)).get(this.extensionViewState.filters.featureId);733const feature = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(this.extensionViewState.filters.featureId);734if (feature?.icon && accessData) {735const featureAccessTimeElement = append(this.container, $('span.activationTime'));736featureAccessTimeElement.textContent = localize('feature access label', "{0} reqs", accessData.accessTimes.length);737const iconElement = append(this.container, $('span' + ThemeIcon.asCSSSelector(feature.icon)));738iconElement.style.paddingLeft = '4px';739return;740}741}742743const extensionStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension);744if (extensionStatus?.activationTimes) {745const activationTime = extensionStatus.activationTimes.codeLoadingTime + extensionStatus.activationTimes.activateCallTime;746append(this.container, $('span' + ThemeIcon.asCSSSelector(activationTimeIcon)));747const activationTimeElement = append(this.container, $('span.activationTime'));748activationTimeElement.textContent = `${activationTime}ms`;749}750}751752}753754export type ExtensionHoverOptions = {755position: () => HoverPosition;756readonly target: HTMLElement;757};758759export class ExtensionHoverWidget extends ExtensionWidget {760761private readonly hover = this._register(new MutableDisposable<IDisposable>());762763constructor(764private readonly options: ExtensionHoverOptions,765private readonly extensionStatusAction: ExtensionStatusAction,766@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,767@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,768@IHoverService private readonly hoverService: IHoverService,769@IConfigurationService private readonly configurationService: IConfigurationService,770@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,771@IThemeService private readonly themeService: IThemeService,772@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,773) {774super();775}776777render(): void {778this.hover.value = undefined;779if (this.extension) {780this.hover.value = this.hoverService.setupManagedHover({781delay: this.configurationService.getValue<number>('workbench.hover.delay'),782showHover: (options, focus) => {783return this.hoverService.showInstantHover({784...options,785additionalClasses: ['extension-hover'],786position: {787hoverPosition: this.options.position(),788forcePosition: true,789},790persistence: {791hideOnKeyDown: true,792}793}, focus);794},795placement: 'element'796},797this.options.target,798{799markdown: () => Promise.resolve(this.getHoverMarkdown()),800markdownNotSupportedFallback: undefined801},802{803appearance: {804showHoverHint: true805}806}807);808}809}810811private getHoverMarkdown(): MarkdownString | undefined {812if (!this.extension) {813return undefined;814}815const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });816817markdown.appendMarkdown(`**${this.extension.displayName}**`);818if (semver.valid(this.extension.version)) {819markdown.appendMarkdown(` <span style="background-color:#8080802B;">** _v${this.extension.version}${(this.extension.isPreReleaseVersion ? ' (pre-release)' : '')}_** </span>`);820}821markdown.appendText(`\n`);822823let addSeparator = false;824if (this.extension.private) {825markdown.appendMarkdown(`$(${privateExtensionIcon.id}) ${localize('privateExtension', "Private Extension")}`);826addSeparator = true;827}828if (this.extension.state === ExtensionState.Installed) {829const installLabel = InstallCountWidget.getInstallLabel(this.extension, true);830if (installLabel) {831if (addSeparator) {832markdown.appendText(` | `);833}834markdown.appendMarkdown(`$(${installCountIcon.id}) ${installLabel}`);835addSeparator = true;836}837if (this.extension.rating) {838if (addSeparator) {839markdown.appendText(` | `);840}841const rating = Math.round(this.extension.rating * 2) / 2;842markdown.appendMarkdown(`$(${starFullIcon.id}) [${rating}](${this.extension.url}&ssr=false#review-details)`);843addSeparator = true;844}845if (this.extension.publisherSponsorLink) {846if (addSeparator) {847markdown.appendText(` | `);848}849markdown.appendMarkdown(`$(${sponsorIcon.id}) [${localize('sponsor', "Sponsor")}](${this.extension.publisherSponsorLink})`);850addSeparator = true;851}852}853if (addSeparator) {854markdown.appendText(`\n`);855}856857const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined);858if (location) {859if (this.extension.isWorkspaceScoped && this.contextService.isInsideWorkspace(location)) {860markdown.appendMarkdown(localize('workspace extension', "Workspace Extension"));861} else {862markdown.appendMarkdown(localize('local extension', "Local Extension"));863}864markdown.appendText(`\n`);865}866867if (this.extension.description) {868markdown.appendMarkdown(`${this.extension.description}`);869markdown.appendText(`\n`);870}871872if (this.extension.publisherDomain?.verified) {873const bgColor = this.themeService.getColorTheme().getColor(extensionVerifiedPublisherIconColor);874const publisherVerifiedTooltip = localize('publisher verified tooltip', "This publisher has verified ownership of {0}", `[${URI.parse(this.extension.publisherDomain.link).authority}](${this.extension.publisherDomain.link})`);875markdown.appendMarkdown(`<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${verifiedPublisherIcon.id})</span> ${publisherVerifiedTooltip}`);876markdown.appendText(`\n`);877}878879if (this.extension.outdated) {880markdown.appendMarkdown(localize('updateRequired', "Latest version:"));881markdown.appendMarkdown(` <span style="background-color:#8080802B;">** _v${this.extension.latestVersion}_** </span>`);882markdown.appendText(`\n`);883}884885const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension);886const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension);887const extensionFeaturesAccessData = this.extensionFeaturesManagementService.getAllAccessDataForExtension(new ExtensionIdentifier(this.extension.identifier.id));888const extensionStatus = this.extensionStatusAction.status;889const runtimeState = this.extension.runtimeState;890const recommendationMessage = this.getRecommendationMessage(this.extension);891892if (extensionRuntimeStatus || extensionFeaturesAccessData.size || extensionStatus.length || runtimeState || recommendationMessage || preReleaseMessage) {893894markdown.appendMarkdown(`---`);895markdown.appendText(`\n`);896897if (extensionRuntimeStatus) {898if (extensionRuntimeStatus.activationTimes) {899const activationTime = extensionRuntimeStatus.activationTimes.codeLoadingTime + extensionRuntimeStatus.activationTimes.activateCallTime;900markdown.appendMarkdown(`${localize('activation', "Activation time")}${extensionRuntimeStatus.activationTimes.activationReason.startup ? ` (${localize('startup', "Startup")})` : ''}: \`${activationTime}ms\``);901markdown.appendText(`\n`);902}903if (extensionRuntimeStatus.runtimeErrors.length || extensionRuntimeStatus.messages.length) {904const hasErrors = extensionRuntimeStatus.runtimeErrors.length || extensionRuntimeStatus.messages.some(message => message.type === Severity.Error);905const hasWarnings = extensionRuntimeStatus.messages.some(message => message.type === Severity.Warning);906const errorsLink = extensionRuntimeStatus.runtimeErrors.length ? `[${extensionRuntimeStatus.runtimeErrors.length === 1 ? localize('uncaught error', '1 uncaught error') : localize('uncaught errors', '{0} uncaught errors', extensionRuntimeStatus.runtimeErrors.length)}](${createCommandUri('extension.open', this.extension.identifier.id, ExtensionEditorTab.Features)})` : undefined;907const messageLink = extensionRuntimeStatus.messages.length ? `[${extensionRuntimeStatus.messages.length === 1 ? localize('message', '1 message') : localize('messages', '{0} messages', extensionRuntimeStatus.messages.length)}](${createCommandUri('extension.open', this.extension.identifier.id, ExtensionEditorTab.Features)})` : undefined;908markdown.appendMarkdown(`$(${hasErrors ? errorIcon.id : hasWarnings ? warningIcon.id : infoIcon.id}) This extension has reported `);909if (errorsLink && messageLink) {910markdown.appendMarkdown(`${errorsLink} and ${messageLink}`);911} else {912markdown.appendMarkdown(`${errorsLink || messageLink}`);913}914markdown.appendText(`\n`);915}916}917918if (extensionFeaturesAccessData.size) {919const registry = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry);920for (const [featureId, accessData] of extensionFeaturesAccessData) {921if (accessData?.accessTimes.length) {922const feature = registry.getExtensionFeature(featureId);923if (feature) {924markdown.appendMarkdown(localize('feature usage label', "{0} usage", feature.label));925markdown.appendMarkdown(`: [${localize('total', "{0} {1} requests in last 30 days", accessData.accessTimes.length, feature.accessDataLabel ?? feature.label)}](${createCommandUri('extension.open', this.extension.identifier.id, ExtensionEditorTab.Features)})`);926markdown.appendText(`\n`);927}928}929}930}931932for (const status of extensionStatus) {933if (status.icon) {934markdown.appendMarkdown(`$(${status.icon.id}) `);935}936markdown.appendMarkdown(status.message.value);937markdown.appendText(`\n`);938}939940if (runtimeState) {941markdown.appendMarkdown(`$(${infoIcon.id}) `);942markdown.appendMarkdown(`${runtimeState.reason}`);943markdown.appendText(`\n`);944}945946if (preReleaseMessage) {947const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor);948markdown.appendMarkdown(`<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span> ${preReleaseMessage}`);949markdown.appendText(`\n`);950}951952if (recommendationMessage) {953markdown.appendMarkdown(recommendationMessage);954markdown.appendText(`\n`);955}956}957958return markdown;959}960961private getRecommendationMessage(extension: IExtension): string | undefined {962if (extension.state === ExtensionState.Installed) {963return undefined;964}965if (extension.deprecationInfo) {966return undefined;967}968const recommendation = this.extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()];969if (!recommendation?.reasonText) {970return undefined;971}972const bgColor = this.themeService.getColorTheme().getColor(extensionButtonProminentBackground);973return `<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${starEmptyIcon.id})</span> ${recommendation.reasonText}`;974}975976static getPreReleaseMessage(extension: IExtension): string | undefined {977if (!extension.hasPreReleaseVersion) {978return undefined;979}980if (extension.isBuiltin) {981return undefined;982}983if (extension.isPreReleaseVersion) {984return undefined;985}986if (extension.preRelease) {987return undefined;988}989const preReleaseVersionLink = `[${localize('Show prerelease version', "Pre-Release version")}](${createCommandUri('workbench.extensions.action.showPreReleaseVersion', extension.identifier.id)})`;990return localize('has prerelease', "This extension has a {0} available", preReleaseVersionLink);991}992993}994995export class ExtensionStatusWidget extends ExtensionWidget {996997private readonly renderDisposables = this._register(new MutableDisposable());998999private readonly _onDidRender = this._register(new Emitter<void>());1000readonly onDidRender: Event<void> = this._onDidRender.event;10011002constructor(1003private readonly container: HTMLElement,1004private readonly extensionStatusAction: ExtensionStatusAction,1005@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,1006) {1007super();1008this.render();1009this._register(extensionStatusAction.onDidChangeStatus(() => this.render()));1010}10111012render(): void {1013reset(this.container);1014this.renderDisposables.value = undefined;1015const disposables = new DisposableStore();1016this.renderDisposables.value = disposables;1017const extensionStatus = this.extensionStatusAction.status;1018if (extensionStatus.length) {1019const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });1020for (let i = 0; i < extensionStatus.length; i++) {1021const status = extensionStatus[i];1022if (status.icon) {1023markdown.appendMarkdown(`$(${status.icon.id}) `);1024}1025markdown.appendMarkdown(status.message.value);1026if (i < extensionStatus.length - 1) {1027markdown.appendText(`\n`);1028}1029}1030const rendered = disposables.add(this.markdownRendererService.render(markdown));1031append(this.container, rendered.element);1032}1033this._onDidRender.fire();1034}1035}10361037export class ExtensionRecommendationWidget extends ExtensionWidget {10381039private readonly _onDidRender = this._register(new Emitter<void>());1040readonly onDidRender: Event<void> = this._onDidRender.event;10411042constructor(1043private readonly container: HTMLElement,1044@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,1045@IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService,1046) {1047super();1048this.render();1049this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.render()));1050}10511052render(): void {1053reset(this.container);1054const recommendationStatus = this.getRecommendationStatus();1055if (recommendationStatus) {1056if (recommendationStatus.icon) {1057append(this.container, $(`div${ThemeIcon.asCSSSelector(recommendationStatus.icon)}`));1058}1059append(this.container, $(`div.recommendation-text`, undefined, recommendationStatus.message));1060}1061this._onDidRender.fire();1062}10631064private getRecommendationStatus(): { icon: ThemeIcon | undefined; message: string } | undefined {1065if (!this.extension1066|| this.extension.deprecationInfo1067|| this.extension.state === ExtensionState.Installed1068) {1069return undefined;1070}1071const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();1072if (extRecommendations[this.extension.identifier.id.toLowerCase()]) {1073const reasonText = extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText;1074if (reasonText) {1075return { icon: starEmptyIcon, message: reasonText };1076}1077} else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(this.extension.identifier.id.toLowerCase()) !== -1) {1078return { icon: undefined, message: localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.") };1079}1080return undefined;1081}1082}10831084export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), false);1085export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), false);1086export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), false);1087export const extensionPrivateBadgeBackground = registerColor('extensionIcon.privateForeground', { dark: '#ffffff60', light: '#00000060', hcDark: '#ffffff60', hcLight: '#00000060' }, localize('extensionIcon.private', "The icon color for private extensions."));10881089registerThemingParticipant((theme, collector) => {1090const extensionRatingIcon = theme.getColor(extensionRatingIconColor);1091if (extensionRatingIcon) {1092collector.addRule(`.extension-ratings .codicon-extensions-star-full, .extension-ratings .codicon-extensions-star-half { color: ${extensionRatingIcon}; }`);1093collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(starFullIcon)} { color: ${extensionRatingIcon}; }`);1094}10951096const extensionVerifiedPublisherIcon = theme.getColor(extensionVerifiedPublisherIconColor);1097if (extensionVerifiedPublisherIcon) {1098collector.addRule(`${ThemeIcon.asCSSSelector(verifiedPublisherIcon)} { color: ${extensionVerifiedPublisherIcon}; }`);1099}11001101collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`);1102collector.addRule(`.extension-editor > .header > .details > .subtitle .sponsor ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`);11031104const privateBadgeBackground = theme.getColor(extensionPrivateBadgeBackground);1105if (privateBadgeBackground) {1106collector.addRule(`.extension-private-badge { color: ${privateBadgeBackground}; }`);1107}1108});110911101111