Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.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 './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 { 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 { renderMarkdown } from '../../../../base/browser/markdownRenderer.js';35import { IOpenerService } from '../../../../platform/opener/common/opener.js';36import { onUnexpectedError } from '../../../../base/common/errors.js';37import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';38import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';39import { KeyCode } from '../../../../base/common/keyCodes.js';40import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js';41import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';42import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';43import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';44import { Registry } from '../../../../platform/registry/common/platform.js';45import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js';46import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';47import { extensionDefaultIcon, extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js';48import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';49import { IExplorerService } from '../../files/browser/files.js';50import { IViewsService } from '../../../services/views/common/viewsService.js';51import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js';52import { IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';5354export abstract class ExtensionWidget extends Disposable implements IExtensionContainer {55private _extension: IExtension | null = null;56get extension(): IExtension | null { return this._extension; }57set extension(extension: IExtension | null) { this._extension = extension; this.update(); }58update(): void { this.render(); }59abstract render(): void;60}6162export function onClick(element: HTMLElement, callback: () => void): IDisposable {63const disposables: DisposableStore = new DisposableStore();64disposables.add(addDisposableListener(element, EventType.CLICK, finalHandler(callback)));65disposables.add(addDisposableListener(element, EventType.KEY_UP, e => {66const keyboardEvent = new StandardKeyboardEvent(e);67if (keyboardEvent.equals(KeyCode.Space) || keyboardEvent.equals(KeyCode.Enter)) {68e.preventDefault();69e.stopPropagation();70callback();71}72}));73return disposables;74}7576export class ExtensionIconWidget extends ExtensionWidget {7778private readonly disposables = this._register(new DisposableStore());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.disposables.clear();107}108109render(): void {110if (!this.extension) {111this.clear();112return;113}114115if (this.extension.iconUrl) {116this.iconElement.style.display = 'inherit';117this.defaultIconElement.style.display = 'none';118if (this.iconUrl !== this.extension.iconUrl) {119this.iconUrl = this.extension.iconUrl;120this.disposables.add(addDisposableListener(this.iconElement, 'error', () => {121if (this.extension?.iconUrlFallback) {122this.iconElement.src = this.extension.iconUrlFallback;123} else {124this.iconElement.style.display = 'none';125this.defaultIconElement.style.display = 'inherit';126}127}, { once: true }));128this.iconElement.src = this.iconUrl;129if (!this.iconElement.complete) {130this.iconElement.style.visibility = 'hidden';131this.iconElement.onload = () => this.iconElement.style.visibility = 'inherit';132} else {133this.iconElement.style.visibility = 'inherit';134}135}136} else {137this.iconUrl = undefined;138this.iconElement.style.display = 'none';139this.iconElement.src = '';140this.defaultIconElement.style.display = 'inherit';141}142}143}144145export class InstallCountWidget extends ExtensionWidget {146147private readonly disposables = this._register(new DisposableStore());148149constructor(150readonly container: HTMLElement,151private small: boolean,152@IHoverService private readonly hoverService: IHoverService,153) {154super();155this.render();156157this._register(toDisposable(() => this.clear()));158}159160private clear(): void {161this.container.innerText = '';162this.disposables.clear();163}164165render(): void {166this.clear();167168if (!this.extension) {169return;170}171172if (this.small && this.extension.state !== ExtensionState.Uninstalled) {173return;174}175176const installLabel = InstallCountWidget.getInstallLabel(this.extension, this.small);177if (!installLabel) {178return;179}180181const parent = this.small ? this.container : append(this.container, $('span.install', { tabIndex: 0 }));182append(parent, $('span' + ThemeIcon.asCSSSelector(installCountIcon)));183const count = append(parent, $('span.count'));184count.textContent = installLabel;185186if (!this.small) {187this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.container, localize('install count', "Install count")));188}189}190191static getInstallLabel(extension: IExtension, small: boolean): string | undefined {192const installCount = extension.installCount;193194if (!installCount) {195return undefined;196}197198let installLabel: string;199200if (small) {201if (installCount > 1000000) {202installLabel = `${Math.floor(installCount / 100000) / 10}M`;203} else if (installCount > 1000) {204installLabel = `${Math.floor(installCount / 1000)}K`;205} else {206installLabel = String(installCount);207}208}209else {210installLabel = installCount.toLocaleString(platform.language);211}212213return installLabel;214}215}216217export class RatingsWidget extends ExtensionWidget {218219private containerHover: IManagedHover | undefined;220private readonly disposables = this._register(new DisposableStore());221222constructor(223readonly container: HTMLElement,224private small: boolean,225@IHoverService private readonly hoverService: IHoverService,226@IOpenerService private readonly openerService: IOpenerService,227) {228super();229container.classList.add('extension-ratings');230231if (this.small) {232container.classList.add('small');233}234235this.render();236this._register(toDisposable(() => this.clear()));237}238239private clear(): void {240this.container.innerText = '';241this.disposables.clear();242}243244render(): void {245this.clear();246247if (!this.extension) {248return;249}250251if (this.small && this.extension.state !== ExtensionState.Uninstalled) {252return;253}254255if (this.extension.rating === undefined) {256return;257}258259if (this.small && !this.extension.ratingCount) {260return;261}262263if (!this.extension.url) {264return;265}266267const rating = Math.round(this.extension.rating * 2) / 2;268if (this.small) {269append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon)));270271const count = append(this.container, $('span.count'));272count.textContent = String(rating);273} else {274const element = append(this.container, $('span.rating.clickable', { tabIndex: 0 }));275for (let i = 1; i <= 5; i++) {276if (rating >= i) {277append(element, $('span' + ThemeIcon.asCSSSelector(starFullIcon)));278} else if (rating >= i - 0.5) {279append(element, $('span' + ThemeIcon.asCSSSelector(starHalfIcon)));280} else {281append(element, $('span' + ThemeIcon.asCSSSelector(starEmptyIcon)));282}283}284if (this.extension.ratingCount) {285const ratingCountElemet = append(element, $('span', undefined, ` (${this.extension.ratingCount})`));286ratingCountElemet.style.paddingLeft = '1px';287}288289this.containerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, ''));290this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating));291element.setAttribute('role', 'link');292if (this.extension.ratingUrl) {293this.disposables.add(onClick(element, () => this.openerService.open(URI.parse(this.extension!.ratingUrl!))));294}295}296}297298}299300export class PublisherWidget extends ExtensionWidget {301302private element: HTMLElement | undefined;303private containerHover: IManagedHover | undefined;304305private readonly disposables = this._register(new DisposableStore());306307constructor(308readonly container: HTMLElement,309private small: boolean,310@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,311@IHoverService private readonly hoverService: IHoverService,312@IOpenerService private readonly openerService: IOpenerService,313) {314super();315316this.render();317this._register(toDisposable(() => this.clear()));318}319320private clear(): void {321this.element?.remove();322this.disposables.clear();323}324325render(): void {326this.clear();327if (!this.extension) {328return;329}330331if (this.extension.resourceExtension) {332return;333}334335if (this.extension.local?.source === 'resource') {336return;337}338339this.element = append(this.container, $('.publisher'));340const publisherDisplayName = $('.publisher-name.ellipsis');341publisherDisplayName.textContent = this.extension.publisherDisplayName;342343const verifiedPublisher = $('.verified-publisher');344append(verifiedPublisher, $('span.extension-verified-publisher.clickable'), renderIcon(verifiedPublisherIcon));345346if (this.small) {347if (this.extension.publisherDomain?.verified) {348append(this.element, verifiedPublisher);349}350append(this.element, publisherDisplayName);351} else {352this.element.classList.toggle('clickable', !!this.extension.url);353this.element.setAttribute('role', 'button');354this.element.tabIndex = 0;355356this.containerHover = this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('publisher', "Publisher ({0})", this.extension.publisherDisplayName)));357append(this.element, publisherDisplayName);358359if (this.extension.publisherDomain?.verified) {360append(this.element, verifiedPublisher);361const publisherDomainLink = URI.parse(this.extension.publisherDomain.link);362verifiedPublisher.tabIndex = 0;363verifiedPublisher.setAttribute('role', 'button');364this.containerHover.update(localize('verified publisher', "This publisher has verified ownership of {0}", this.extension.publisherDomain.link));365verifiedPublisher.setAttribute('role', 'link');366367append(verifiedPublisher, $('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority));368this.disposables.add(onClick(verifiedPublisher, () => this.openerService.open(publisherDomainLink)));369}370371if (this.extension.url) {372this.disposables.add(onClick(this.element, () => this.extensionsWorkbenchService.openSearch(`publisher:"${this.extension?.publisherDisplayName}"`)));373}374}375376}377378}379380export class SponsorWidget extends ExtensionWidget {381382private readonly disposables = this._register(new DisposableStore());383384constructor(385readonly container: HTMLElement,386@IHoverService private readonly hoverService: IHoverService,387@IOpenerService private readonly openerService: IOpenerService,388) {389super();390this.render();391}392393render(): void {394reset(this.container);395this.disposables.clear();396if (!this.extension?.publisherSponsorLink) {397return;398}399400const sponsor = append(this.container, $('span.sponsor.clickable', { tabIndex: 0 }));401this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), sponsor, this.extension?.publisherSponsorLink.toString() ?? ''));402sponsor.setAttribute('role', 'link'); // #132645403const sponsorIconElement = renderIcon(sponsorIcon);404const label = $('span', undefined, localize('sponsor', "Sponsor"));405append(sponsor, sponsorIconElement, label);406this.disposables.add(onClick(sponsor, () => {407this.openerService.open(this.extension!.publisherSponsorLink!);408}));409}410}411412export class RecommendationWidget extends ExtensionWidget {413414private element?: HTMLElement;415private readonly disposables = this._register(new DisposableStore());416417constructor(418private parent: HTMLElement,419@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService420) {421super();422this.render();423this._register(toDisposable(() => this.clear()));424this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.render()));425}426427private clear(): void {428this.element?.remove();429this.element = undefined;430this.disposables.clear();431}432433render(): void {434this.clear();435if (!this.extension || this.extension.state === ExtensionState.Installed || this.extension.deprecationInfo) {436return;437}438const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();439if (extRecommendations[this.extension.identifier.id.toLowerCase()]) {440this.element = append(this.parent, $('div.extension-bookmark'));441const recommendation = append(this.element, $('.recommendation'));442append(recommendation, $('span' + ThemeIcon.asCSSSelector(ratingIcon)));443}444}445446}447448export class PreReleaseBookmarkWidget extends ExtensionWidget {449450private element?: HTMLElement;451private readonly disposables = this._register(new DisposableStore());452453constructor(454private parent: HTMLElement,455) {456super();457this.render();458this._register(toDisposable(() => this.clear()));459}460461private clear(): void {462this.element?.remove();463this.element = undefined;464this.disposables.clear();465}466467render(): void {468this.clear();469if (this.extension?.state === ExtensionState.Installed ? this.extension.preRelease : this.extension?.hasPreReleaseVersion) {470this.element = append(this.parent, $('div.extension-bookmark'));471const preRelease = append(this.element, $('.pre-release'));472append(preRelease, $('span' + ThemeIcon.asCSSSelector(preReleaseIcon)));473}474}475476}477478export class RemoteBadgeWidget extends ExtensionWidget {479480private readonly remoteBadge = this._register(new MutableDisposable<ExtensionIconBadge>());481482private element: HTMLElement;483484constructor(485parent: HTMLElement,486private readonly tooltip: boolean,487@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,488@IInstantiationService private readonly instantiationService: IInstantiationService489) {490super();491this.element = append(parent, $(''));492this.render();493this._register(toDisposable(() => this.clear()));494}495496private clear(): void {497this.remoteBadge.value?.element.remove();498this.remoteBadge.clear();499}500501render(): void {502this.clear();503if (!this.extension || !this.extension.local || !this.extension.server || !(this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) || this.extension.server !== this.extensionManagementServerService.remoteExtensionManagementServer) {504return;505}506let tooltip: string | undefined;507if (this.tooltip && this.extensionManagementServerService.remoteExtensionManagementServer) {508tooltip = localize('remote extension title', "Extension in {0}", this.extensionManagementServerService.remoteExtensionManagementServer.label);509}510this.remoteBadge.value = this.instantiationService.createInstance(ExtensionIconBadge, remoteIcon, tooltip);511append(this.element, this.remoteBadge.value.element);512}513}514515export class ExtensionIconBadge extends Disposable {516517readonly element: HTMLElement;518readonly elementHover: IManagedHover;519520constructor(521private readonly icon: ThemeIcon,522private readonly tooltip: string | undefined,523@IHoverService hoverService: IHoverService,524@ILabelService private readonly labelService: ILabelService,525@IThemeService private readonly themeService: IThemeService,526) {527super();528this.element = $('div.extension-badge.extension-icon-badge');529this.elementHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, ''));530this.render();531}532533private render(): void {534append(this.element, $('span' + ThemeIcon.asCSSSelector(this.icon)));535536const applyBadgeStyle = () => {537if (!this.element) {538return;539}540const bgColor = this.themeService.getColorTheme().getColor(EXTENSION_BADGE_BACKGROUND);541const fgColor = this.themeService.getColorTheme().getColor(EXTENSION_BADGE_FOREGROUND);542this.element.style.backgroundColor = bgColor ? bgColor.toString() : '';543this.element.style.color = fgColor ? fgColor.toString() : '';544};545applyBadgeStyle();546this._register(this.themeService.onDidColorThemeChange(() => applyBadgeStyle()));547548if (this.tooltip) {549const updateTitle = () => {550if (this.element) {551this.elementHover.update(this.tooltip);552}553};554this._register(this.labelService.onDidChangeFormatters(() => updateTitle()));555updateTitle();556}557}558}559560export class ExtensionPackCountWidget extends ExtensionWidget {561562private element: HTMLElement | undefined;563private countBadge: CountBadge | undefined;564565constructor(566private readonly parent: HTMLElement,567) {568super();569this.render();570this._register(toDisposable(() => this.clear()));571}572573private clear(): void {574this.element?.remove();575this.countBadge?.dispose();576this.countBadge = undefined;577}578579render(): void {580this.clear();581if (!this.extension || !(this.extension.categories?.some(category => category.toLowerCase() === 'extension packs')) || !this.extension.extensionPack.length) {582return;583}584this.element = append(this.parent, $('.extension-badge.extension-pack-badge'));585this.countBadge = new CountBadge(this.element, {}, defaultCountBadgeStyles);586this.countBadge.setCount(this.extension.extensionPack.length);587}588}589590export class ExtensionKindIndicatorWidget extends ExtensionWidget {591592private element: HTMLElement | undefined;593private extensionGalleryManifest: IExtensionGalleryManifest | null = null;594595private readonly disposables = this._register(new DisposableStore());596597constructor(598readonly container: HTMLElement,599private small: boolean,600@IHoverService private readonly hoverService: IHoverService,601@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,602@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,603@IExplorerService private readonly explorerService: IExplorerService,604@IViewsService private readonly viewsService: IViewsService,605@IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService,606) {607super();608this.render();609this._register(toDisposable(() => this.clear()));610extensionGalleryManifestService.getExtensionGalleryManifest().then(manifest => {611if (this._store.isDisposed) {612return;613}614this.extensionGalleryManifest = manifest;615this.render();616});617}618619private clear(): void {620this.element?.remove();621this.disposables.clear();622}623624render(): void {625this.clear();626627if (!this.extension) {628return;629}630631if (this.extension?.private) {632this.element = append(this.container, $('.extension-kind-indicator'));633if (!this.small || (this.extensionGalleryManifest?.capabilities.extensions?.includePublicExtensions && this.extensionGalleryManifest?.capabilities.extensions?.includePrivateExtensions)) {634append(this.element, $('span' + ThemeIcon.asCSSSelector(privateExtensionIcon)));635}636if (!this.small) {637append(this.element, $('span.private-extension-label', undefined, localize('privateExtension', "Private Extension")));638}639return;640}641642if (!this.small) {643return;644}645646const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined);647if (!location) {648return;649}650651this.element = append(this.container, $('.extension-kind-indicator'));652const workspaceFolder = this.contextService.getWorkspaceFolder(location);653if (workspaceFolder && this.extension.isWorkspaceScoped) {654this.element.textContent = localize('workspace extension', "Workspace Extension");655this.element.classList.add('clickable');656this.element.setAttribute('role', 'button');657this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location)));658this.disposables.add(onClick(this.element, () => {659this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true));660}));661} else {662this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, location.path));663this.element.textContent = localize('local extension', "Local Extension");664}665}666}667668export class SyncIgnoredWidget extends ExtensionWidget {669670private readonly disposables = this._register(new DisposableStore());671672constructor(673private readonly container: HTMLElement,674@IConfigurationService private readonly configurationService: IConfigurationService,675@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,676@IHoverService private readonly hoverService: IHoverService,677@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,678) {679super();680this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.ignoredExtensions'))(() => this.render()));681this._register(userDataSyncEnablementService.onDidChangeEnablement(() => this.update()));682this.render();683}684685render(): void {686this.disposables.clear();687this.container.innerText = '';688689if (this.extension && this.extension.state === ExtensionState.Installed && this.userDataSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) {690const element = append(this.container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon)));691this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, localize('syncingore.label', "This extension is ignored during sync.")));692element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon));693}694}695}696697export class ExtensionRuntimeStatusWidget extends ExtensionWidget {698699constructor(700private readonly extensionViewState: IExtensionsViewState,701private readonly container: HTMLElement,702@IExtensionService extensionService: IExtensionService,703@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,704@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,705) {706super();707this._register(extensionService.onDidChangeExtensionsStatus(extensions => {708if (this.extension && extensions.some(e => areSameExtensions({ id: e.value }, this.extension!.identifier))) {709this.update();710}711}));712this._register(extensionFeaturesManagementService.onDidChangeAccessData(e => {713if (this.extension && ExtensionIdentifier.equals(this.extension.identifier.id, e.extension)) {714this.update();715}716}));717}718719render(): void {720this.container.innerText = '';721722if (!this.extension) {723return;724}725726if (this.extensionViewState.filters.featureId && this.extension.state === ExtensionState.Installed) {727const accessData = this.extensionFeaturesManagementService.getAllAccessDataForExtension(new ExtensionIdentifier(this.extension.identifier.id)).get(this.extensionViewState.filters.featureId);728const feature = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(this.extensionViewState.filters.featureId);729if (feature?.icon && accessData) {730const featureAccessTimeElement = append(this.container, $('span.activationTime'));731featureAccessTimeElement.textContent = localize('feature access label', "{0} reqs", accessData.accessTimes.length);732const iconElement = append(this.container, $('span' + ThemeIcon.asCSSSelector(feature.icon)));733iconElement.style.paddingLeft = '4px';734return;735}736}737738const extensionStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension);739if (extensionStatus?.activationTimes) {740const activationTime = extensionStatus.activationTimes.codeLoadingTime + extensionStatus.activationTimes.activateCallTime;741append(this.container, $('span' + ThemeIcon.asCSSSelector(activationTimeIcon)));742const activationTimeElement = append(this.container, $('span.activationTime'));743activationTimeElement.textContent = `${activationTime}ms`;744}745}746747}748749export type ExtensionHoverOptions = {750position: () => HoverPosition;751readonly target: HTMLElement;752};753754export class ExtensionHoverWidget extends ExtensionWidget {755756private readonly hover = this._register(new MutableDisposable<IDisposable>());757758constructor(759private readonly options: ExtensionHoverOptions,760private readonly extensionStatusAction: ExtensionStatusAction,761@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,762@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,763@IHoverService private readonly hoverService: IHoverService,764@IConfigurationService private readonly configurationService: IConfigurationService,765@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,766@IThemeService private readonly themeService: IThemeService,767@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,768) {769super();770}771772render(): void {773this.hover.value = undefined;774if (this.extension) {775this.hover.value = this.hoverService.setupManagedHover({776delay: this.configurationService.getValue<number>('workbench.hover.delay'),777showHover: (options, focus) => {778return this.hoverService.showInstantHover({779...options,780additionalClasses: ['extension-hover'],781position: {782hoverPosition: this.options.position(),783forcePosition: true,784},785persistence: {786hideOnKeyDown: true,787}788}, focus);789},790placement: 'element'791},792this.options.target,793{794markdown: () => Promise.resolve(this.getHoverMarkdown()),795markdownNotSupportedFallback: undefined796},797{798appearance: {799showHoverHint: true800}801}802);803}804}805806private getHoverMarkdown(): MarkdownString | undefined {807if (!this.extension) {808return undefined;809}810const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });811812markdown.appendMarkdown(`**${this.extension.displayName}**`);813if (semver.valid(this.extension.version)) {814markdown.appendMarkdown(` <span style="background-color:#8080802B;">** _v${this.extension.version}${(this.extension.isPreReleaseVersion ? ' (pre-release)' : '')}_** </span>`);815}816markdown.appendText(`\n`);817818let addSeparator = false;819if (this.extension.private) {820markdown.appendMarkdown(`$(${privateExtensionIcon.id}) ${localize('privateExtension', "Private Extension")}`);821addSeparator = true;822}823if (this.extension.state === ExtensionState.Installed) {824const installLabel = InstallCountWidget.getInstallLabel(this.extension, true);825if (installLabel) {826if (addSeparator) {827markdown.appendText(` | `);828}829markdown.appendMarkdown(`$(${installCountIcon.id}) ${installLabel}`);830addSeparator = true;831}832if (this.extension.rating) {833if (addSeparator) {834markdown.appendText(` | `);835}836const rating = Math.round(this.extension.rating * 2) / 2;837markdown.appendMarkdown(`$(${starFullIcon.id}) [${rating}](${this.extension.url}&ssr=false#review-details)`);838addSeparator = true;839}840if (this.extension.publisherSponsorLink) {841if (addSeparator) {842markdown.appendText(` | `);843}844markdown.appendMarkdown(`$(${sponsorIcon.id}) [${localize('sponsor', "Sponsor")}](${this.extension.publisherSponsorLink})`);845addSeparator = true;846}847}848if (addSeparator) {849markdown.appendText(`\n`);850}851852const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined);853if (location) {854if (this.extension.isWorkspaceScoped && this.contextService.isInsideWorkspace(location)) {855markdown.appendMarkdown(localize('workspace extension', "Workspace Extension"));856} else {857markdown.appendMarkdown(localize('local extension', "Local Extension"));858}859markdown.appendText(`\n`);860}861862if (this.extension.description) {863markdown.appendMarkdown(`${this.extension.description}`);864markdown.appendText(`\n`);865}866867if (this.extension.publisherDomain?.verified) {868const bgColor = this.themeService.getColorTheme().getColor(extensionVerifiedPublisherIconColor);869const publisherVerifiedTooltip = localize('publisher verified tooltip', "This publisher has verified ownership of {0}", `[${URI.parse(this.extension.publisherDomain.link).authority}](${this.extension.publisherDomain.link})`);870markdown.appendMarkdown(`<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${verifiedPublisherIcon.id})</span> ${publisherVerifiedTooltip}`);871markdown.appendText(`\n`);872}873874if (this.extension.outdated) {875markdown.appendMarkdown(localize('updateRequired', "Latest version:"));876markdown.appendMarkdown(` <span style="background-color:#8080802B;">** _v${this.extension.latestVersion}_** </span>`);877markdown.appendText(`\n`);878}879880const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension);881const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension);882const extensionFeaturesAccessData = this.extensionFeaturesManagementService.getAllAccessDataForExtension(new ExtensionIdentifier(this.extension.identifier.id));883const extensionStatus = this.extensionStatusAction.status;884const runtimeState = this.extension.runtimeState;885const recommendationMessage = this.getRecommendationMessage(this.extension);886887if (extensionRuntimeStatus || extensionFeaturesAccessData.size || extensionStatus.length || runtimeState || recommendationMessage || preReleaseMessage) {888889markdown.appendMarkdown(`---`);890markdown.appendText(`\n`);891892if (extensionRuntimeStatus) {893if (extensionRuntimeStatus.activationTimes) {894const activationTime = extensionRuntimeStatus.activationTimes.codeLoadingTime + extensionRuntimeStatus.activationTimes.activateCallTime;895markdown.appendMarkdown(`${localize('activation', "Activation time")}${extensionRuntimeStatus.activationTimes.activationReason.startup ? ` (${localize('startup', "Startup")})` : ''}: \`${activationTime}ms\``);896markdown.appendText(`\n`);897}898if (extensionRuntimeStatus.runtimeErrors.length || extensionRuntimeStatus.messages.length) {899const hasErrors = extensionRuntimeStatus.runtimeErrors.length || extensionRuntimeStatus.messages.some(message => message.type === Severity.Error);900const hasWarnings = extensionRuntimeStatus.messages.some(message => message.type === Severity.Warning);901const errorsLink = extensionRuntimeStatus.runtimeErrors.length ? `[${extensionRuntimeStatus.runtimeErrors.length === 1 ? localize('uncaught error', '1 uncaught error') : localize('uncaught errors', '{0} uncaught errors', extensionRuntimeStatus.runtimeErrors.length)}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Features]))}`)})` : undefined;902const messageLink = extensionRuntimeStatus.messages.length ? `[${extensionRuntimeStatus.messages.length === 1 ? localize('message', '1 message') : localize('messages', '{0} messages', extensionRuntimeStatus.messages.length)}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Features]))}`)})` : undefined;903markdown.appendMarkdown(`$(${hasErrors ? errorIcon.id : hasWarnings ? warningIcon.id : infoIcon.id}) This extension has reported `);904if (errorsLink && messageLink) {905markdown.appendMarkdown(`${errorsLink} and ${messageLink}`);906} else {907markdown.appendMarkdown(`${errorsLink || messageLink}`);908}909markdown.appendText(`\n`);910}911}912913if (extensionFeaturesAccessData.size) {914const registry = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry);915for (const [featureId, accessData] of extensionFeaturesAccessData) {916if (accessData?.accessTimes.length) {917const feature = registry.getExtensionFeature(featureId);918if (feature) {919markdown.appendMarkdown(localize('feature usage label', "{0} usage", feature.label));920markdown.appendMarkdown(`: [${localize('total', "{0} {1} requests in last 30 days", accessData.accessTimes.length, feature.accessDataLabel ?? feature.label)}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Features]))}`)})`);921markdown.appendText(`\n`);922}923}924}925}926927for (const status of extensionStatus) {928if (status.icon) {929markdown.appendMarkdown(`$(${status.icon.id}) `);930}931markdown.appendMarkdown(status.message.value);932markdown.appendText(`\n`);933}934935if (runtimeState) {936markdown.appendMarkdown(`$(${infoIcon.id}) `);937markdown.appendMarkdown(`${runtimeState.reason}`);938markdown.appendText(`\n`);939}940941if (preReleaseMessage) {942const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor);943markdown.appendMarkdown(`<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span> ${preReleaseMessage}`);944markdown.appendText(`\n`);945}946947if (recommendationMessage) {948markdown.appendMarkdown(recommendationMessage);949markdown.appendText(`\n`);950}951}952953return markdown;954}955956private getRecommendationMessage(extension: IExtension): string | undefined {957if (extension.state === ExtensionState.Installed) {958return undefined;959}960if (extension.deprecationInfo) {961return undefined;962}963const recommendation = this.extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()];964if (!recommendation?.reasonText) {965return undefined;966}967const bgColor = this.themeService.getColorTheme().getColor(extensionButtonProminentBackground);968return `<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${starEmptyIcon.id})</span> ${recommendation.reasonText}`;969}970971static getPreReleaseMessage(extension: IExtension): string | undefined {972if (!extension.hasPreReleaseVersion) {973return undefined;974}975if (extension.isBuiltin) {976return undefined;977}978if (extension.isPreReleaseVersion) {979return undefined;980}981if (extension.preRelease) {982return undefined;983}984const preReleaseVersionLink = `[${localize('Show prerelease version', "Pre-Release version")}](${URI.parse(`command:workbench.extensions.action.showPreReleaseVersion?${encodeURIComponent(JSON.stringify([extension.identifier.id]))}`)})`;985return localize('has prerelease', "This extension has a {0} available", preReleaseVersionLink);986}987988}989990export class ExtensionStatusWidget extends ExtensionWidget {991992private readonly renderDisposables = this._register(new MutableDisposable());993994private readonly _onDidRender = this._register(new Emitter<void>());995readonly onDidRender: Event<void> = this._onDidRender.event;996997constructor(998private readonly container: HTMLElement,999private readonly extensionStatusAction: ExtensionStatusAction,1000@IOpenerService private readonly openerService: IOpenerService,1001) {1002super();1003this.render();1004this._register(extensionStatusAction.onDidChangeStatus(() => this.render()));1005}10061007render(): void {1008reset(this.container);1009this.renderDisposables.value = undefined;1010const disposables = new DisposableStore();1011this.renderDisposables.value = disposables;1012const extensionStatus = this.extensionStatusAction.status;1013if (extensionStatus.length) {1014const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });1015for (let i = 0; i < extensionStatus.length; i++) {1016const status = extensionStatus[i];1017if (status.icon) {1018markdown.appendMarkdown(`$(${status.icon.id}) `);1019}1020markdown.appendMarkdown(status.message.value);1021if (i < extensionStatus.length - 1) {1022markdown.appendText(`\n`);1023}1024}1025const rendered = disposables.add(renderMarkdown(markdown, {1026actionHandler: (content) => {1027this.openerService.open(content, { allowCommands: true }).catch(onUnexpectedError);1028},1029}));1030append(this.container, rendered.element);1031}1032this._onDidRender.fire();1033}1034}10351036export class ExtensionRecommendationWidget extends ExtensionWidget {10371038private readonly _onDidRender = this._register(new Emitter<void>());1039readonly onDidRender: Event<void> = this._onDidRender.event;10401041constructor(1042private readonly container: HTMLElement,1043@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,1044@IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService,1045) {1046super();1047this.render();1048this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.render()));1049}10501051render(): void {1052reset(this.container);1053const recommendationStatus = this.getRecommendationStatus();1054if (recommendationStatus) {1055if (recommendationStatus.icon) {1056append(this.container, $(`div${ThemeIcon.asCSSSelector(recommendationStatus.icon)}`));1057}1058append(this.container, $(`div.recommendation-text`, undefined, recommendationStatus.message));1059}1060this._onDidRender.fire();1061}10621063private getRecommendationStatus(): { icon: ThemeIcon | undefined; message: string } | undefined {1064if (!this.extension1065|| this.extension.deprecationInfo1066|| this.extension.state === ExtensionState.Installed1067) {1068return undefined;1069}1070const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();1071if (extRecommendations[this.extension.identifier.id.toLowerCase()]) {1072const reasonText = extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText;1073if (reasonText) {1074return { icon: starEmptyIcon, message: reasonText };1075}1076} else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(this.extension.identifier.id.toLowerCase()) !== -1) {1077return { icon: undefined, message: localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.") };1078}1079return undefined;1080}1081}10821083export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), false);1084export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), false);1085export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), false);1086export const extensionPrivateBadgeBackground = registerColor('extensionIcon.privateForeground', { dark: '#ffffff60', light: '#00000060', hcDark: '#ffffff60', hcLight: '#00000060' }, localize('extensionIcon.private', "The icon color for private extensions."));10871088registerThemingParticipant((theme, collector) => {1089const extensionRatingIcon = theme.getColor(extensionRatingIconColor);1090if (extensionRatingIcon) {1091collector.addRule(`.extension-ratings .codicon-extensions-star-full, .extension-ratings .codicon-extensions-star-half { color: ${extensionRatingIcon}; }`);1092collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(starFullIcon)} { color: ${extensionRatingIcon}; }`);1093}10941095const extensionVerifiedPublisherIcon = theme.getColor(extensionVerifiedPublisherIconColor);1096if (extensionVerifiedPublisherIcon) {1097collector.addRule(`${ThemeIcon.asCSSSelector(verifiedPublisherIcon)} { color: ${extensionVerifiedPublisherIcon}; }`);1098}10991100collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`);1101collector.addRule(`.extension-editor > .header > .details > .subtitle .sponsor ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`);11021103const privateBadgeBackground = theme.getColor(extensionPrivateBadgeBackground);1104if (privateBadgeBackground) {1105collector.addRule(`.extension-private-badge { color: ${privateBadgeBackground}; }`);1106}1107});110811091110