From d09fd2b5d78e10927937d2b24ee588a6c27b34eb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 14 Mar 2025 00:00:08 +0100 Subject: [PATCH] render private extension (#243503) * render private extension * fix all repsitory signed flag --- .../common/extensionGalleryService.ts | 25 +- .../common/extensionManagement.ts | 3 + .../node/extensionManagementService.ts | 2 + .../extensions/browser/extensionEditor.ts | 97 +++----- .../extensions/browser/extensionsIcons.ts | 1 + .../extensions/browser/extensionsList.ts | 21 +- .../extensions/browser/extensionsWidgets.ts | 228 +++++++++++++++--- .../browser/extensionsWorkbenchService.ts | 4 + .../extensions/browser/media/extension.css | 23 +- .../browser/media/extensionEditor.css | 15 +- .../browser/media/extensionsWidgets.css | 4 +- .../contrib/extensions/common/extensions.ts | 1 + .../common/extensionManagementService.ts | 1 + .../common/webExtensionManagementService.ts | 2 + 14 files changed, 275 insertions(+), 152 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 17f5e9ede92..c436d227016 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -48,8 +48,6 @@ const enum ExtensionGalleryResourceType { PublisherViewUri = 'PublisherViewUriTemplate', ExtensionDetailsViewUri = 'ExtensionDetailsViewUriTemplate', ExtensionRatingViewUri = 'ExtensionRatingViewUriTemplate', - WebLanguagePackService = 'WebLanguagePackService', - ExtensionUriTemplate = 'ExtensionUriTemplate', } const enum Flag { @@ -187,6 +185,7 @@ const PropertyType = { SponsorLink: 'Microsoft.VisualStudio.Code.SponsorLink', SupportLink: 'Microsoft.VisualStudio.Services.Links.Support', ExecutesCode: 'Microsoft.VisualStudio.Code.ExecutesCode', + Private: 'PrivateMarketplace', }; interface ICriterium { @@ -402,6 +401,11 @@ function isPreReleaseVersion(version: IRawGalleryExtensionVersion): boolean { return values.length > 0 && values[0].value === 'true'; } +function isPrivateExtension(version: IRawGalleryExtensionVersion): boolean { + const values = version.properties ? version.properties.filter(p => p.key === PropertyType.Private) : []; + return values.length > 0 && values[0].value === 'true'; +} + function executesCode(version: IRawGalleryExtensionVersion): boolean | undefined { const values = version.properties ? version.properties.filter(p => p.key === PropertyType.ExecutesCode) : []; return values.length > 0 ? values[0].value === 'true' : undefined; @@ -557,6 +561,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller }, hasPreReleaseVersion: isPreReleaseVersion(latestVersion), hasReleaseVersion: true, + private: isPrivateExtension(latestVersion), preview: getIsPreview(galleryExtension.flags), isSigned: !!assets.signature, queryContext, @@ -646,7 +651,7 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle sortBy, filters }, - allRepositorySigned: !extensionGalleryManifest.capabilities.signing?.allRepositorySigned, + allRepositorySigned: !!extensionGalleryManifest.capabilities.signing?.allRepositorySigned, }; } @@ -1940,20 +1945,6 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle }); } - if (extensionsGallery.resourceUrlTemplate) { - resources.push({ - id: extensionsGallery.resourceUrlTemplate, - type: ExtensionGalleryResourceType.ExtensionUriTemplate - }); - } - - if (extensionsGallery.nlsBaseUrl) { - resources.push({ - id: extensionsGallery.nlsBaseUrl, - type: ExtensionGalleryResourceType.WebLanguagePackService - }); - } - const filtering = [ { name: FilterType.Tag, diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 1dd20692b3a..2a55c89dc5b 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -229,6 +229,7 @@ export interface IGalleryExtension { releaseDate: number; lastUpdated: number; preview: boolean; + private: boolean; hasPreReleaseVersion: boolean; hasReleaseVersion: boolean; isSigned: boolean; @@ -247,6 +248,7 @@ export type InstallSource = 'gallery' | 'vsix' | 'resource'; export interface IGalleryMetadata { id: string; publisherId: string; + private: boolean; publisherDisplayName: string; isPreReleaseVersion: boolean; targetPlatform?: TargetPlatform; @@ -274,6 +276,7 @@ export interface ILocalExtension extends IExtension { installedTimestamp?: number; isPreReleaseVersion: boolean; hasPreReleaseVersion: boolean; + private: boolean; preRelease: boolean; updated: boolean; pinned: boolean; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 28b058ee782..1ed45d18af9 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -869,6 +869,7 @@ export class ExtensionsScanner extends Disposable { installedTimestamp: extension.metadata?.installedTimestamp, updated: !!extension.metadata?.updated, pinned: !!extension.metadata?.pinned, + private: !!extension.metadata?.private, isWorkspaceScoped: false, source: extension.metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'vsix'), size: extension.metadata?.size ?? 0, @@ -1041,6 +1042,7 @@ class InstallExtensionInProfileTask extends AbstractExtensionTask 0) { + lastNonEmptySubtitleEntryContainer = subTitleEntryElement; + } + } + if (lastNonEmptySubtitleEntryContainer) { + lastNonEmptySubtitleEntryContainer.classList.add('last-non-empty'); + } }, set gallery(gallery: IGalleryExtension | null) { versionWidget.gallery = gallery; @@ -559,41 +555,8 @@ export class ExtensionEditor extends EditorPane { template.description.textContent = extension.description; - // subtitle - template.publisher.classList.toggle('clickable', !!extension.url); - template.publisherDisplayName.textContent = extension.publisherDisplayName; - template.publisher.parentElement?.classList.toggle('hide', !!extension.resourceExtension || extension.local?.source === 'resource'); - this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.publisher, localize('publisher', "Publisher ({0})", extension.publisher))); - - const location = extension.resourceExtension?.location ?? (extension.local?.source === 'resource' ? extension.local?.location : undefined); - template.resource.parentElement?.classList.toggle('hide', !location); - if (location) { - const workspaceFolder = this.contextService.getWorkspaceFolder(location); - if (workspaceFolder && extension.isWorkspaceScoped) { - template.resource.parentElement?.classList.add('clickable'); - this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.resource, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); - template.resource.textContent = localize('workspace extension', "Workspace Extension"); - this.transientDisposables.add(onClick(template.resource, () => { - this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true)); - })); - } else { - template.resource.parentElement?.classList.remove('clickable'); - this.transientDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.resource, location.path)); - template.resource.textContent = localize('local extension', "Local Extension"); - } - } - - template.installCount.parentElement?.classList.toggle('hide', !extension.url); - template.rating.parentElement?.classList.toggle('hide', !extension.url); - template.rating.classList.toggle('clickable', !!extension.url); - if (extension.url) { this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(extension.url!)))); - this.transientDisposables.add(onClick(template.publisher, () => this.extensionsWorkbenchService.openSearch(`publisher:"${extension.publisherDisplayName}"`))); - } - - if (extension.ratingUrl) { - this.transientDisposables.add(onClick(template.rating, () => this.openerService.open(URI.parse(extension.ratingUrl!)))); } const manifest = await this.extensionManifest.get().promise; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts index f7871ef4e2b..ea1bc04ede8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsIcons.ts @@ -23,6 +23,7 @@ export const syncEnabledIcon = registerIcon('extensions-sync-enabled', Codicon.s export const syncIgnoredIcon = registerIcon('extensions-sync-ignored', Codicon.syncIgnored, localize('syncIgnoredIcon', 'Icon to indicate that an extension is ignored when syncing.')); export const remoteIcon = registerIcon('extensions-remote', Codicon.remote, localize('remoteIcon', 'Icon to indicate that an extension is remote in the extensions view and editor.')); export const installCountIcon = registerIcon('extensions-install-count', Codicon.cloudDownload, localize('installCountIcon', 'Icon shown along with the install count in the extensions view and editor.')); +export const privateExtensionIcon = registerIcon('extensions-private', Codicon.lock, localize('lockIcon', 'Icon shown for private extensions in the extensions view and editor.')); export const ratingIcon = registerIcon('extensions-rating', Codicon.star, localize('ratingIcon', 'Icon shown along with the rating in the extensions view and editor.')); export const preReleaseIcon = registerIcon('extensions-pre-release', Codicon.versions, localize('preReleaseIcon', 'Icon shown for extensions having pre-release versions in extensions view and editor.')); export const sponsorIcon = registerIcon('extensions-sponsor', Codicon.heartFilled, localize('sponsorIcon', 'Icon used for sponsoring extensions in the extensions view and editor.')); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 8e2e5ab9691..97be5462cf6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -11,11 +11,10 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IPagedRenderer } from '../../../../base/browser/ui/list/listPaging.js'; -import { Event } from '../../../../base/common/event.js'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService, IExtensionsViewState } from '../common/extensions.js'; import { ManageExtensionAction, ExtensionRuntimeStateAction, ExtensionStatusLabelAction, RemoteInstallAction, ExtensionStatusAction, LocalInstallAction, ButtonWithDropDownExtensionAction, InstallDropdownAction, InstallingLabelAction, ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction, WebInstallAction, MigrateDeprecatedExtensionAction, SetLanguageAction, ClearLanguageAction, UpdateAction } from './extensionsActions.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; -import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, VerifiedPublisherWidget } from './extensionsWidgets.js'; +import { RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget, ExtensionHoverWidget, ExtensionRuntimeStatusWidget, PreReleaseBookmarkWidget, PublisherWidget, ExtensionKindIndicatorWidget } from './extensionsWidgets.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; @@ -34,7 +33,6 @@ export interface ITemplateData { element: HTMLElement; icon: HTMLImageElement; name: HTMLElement; - publisherDisplayName: HTMLElement; description: HTMLElement; installCount: HTMLElement; ratings: HTMLElement; @@ -85,13 +83,12 @@ export class Renderer implements IPagedRenderer { const installCount = append(header, $('span.install-count')); const ratings = append(header, $('span.ratings')); const syncIgnore = append(header, $('span.sync-ignored')); + const extensionKindIndicator = append(header, $('span')); const activationStatus = append(header, $('span.activation-status')); const headerRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, header, false); const description = append(details, $('.description.ellipsis')); const footer = append(details, $('.footer')); - const publisher = append(footer, $('.author.ellipsis')); - const verifiedPublisherWidget = this.instantiationService.createInstance(VerifiedPublisherWidget, append(publisher, $(`.verified-publisher`)), true); - const publisherDisplayName = append(publisher, $('.publisher-name.ellipsis')); + const publisherWidget = this.instantiationService.createInstance(PublisherWidget, append(footer, $('.publisher-container')), true); const actionbar = new ActionBar(footer, { actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { if (action instanceof ButtonWithDropDownExtensionAction) { @@ -140,12 +137,13 @@ export class Renderer implements IPagedRenderer { iconRemoteBadgeWidget, extensionPackBadgeWidget, headerRemoteBadgeWidget, - verifiedPublisherWidget, + publisherWidget, extensionHoverWidget, this.instantiationService.createInstance(SyncIgnoredWidget, syncIgnore), this.instantiationService.createInstance(ExtensionRuntimeStatusWidget, this.extensionViewState, activationStatus), this.instantiationService.createInstance(InstallCountWidget, installCount, true), this.instantiationService.createInstance(RatingsWidget, ratings, true), + this.instantiationService.createInstance(ExtensionKindIndicatorWidget, extensionKindIndicator, true), ]; const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets]); @@ -153,7 +151,7 @@ export class Renderer implements IPagedRenderer { const disposable = combinedDisposable(...actions, ...widgets, actionbar, actionBarListener, extensionContainers); return { - root, element, icon, name, installCount, ratings, description, publisherDisplayName, disposables: [disposable], actionbar, + root, element, icon, name, installCount, ratings, description, disposables: [disposable], actionbar, extensionDisposables: [], set extension(extension: IExtension) { extensionContainers.extension = extension; @@ -170,7 +168,6 @@ export class Renderer implements IPagedRenderer { data.icon.src = ''; data.name.textContent = ''; data.description.textContent = ''; - data.publisherDisplayName.textContent = ''; data.installCount.style.display = 'none'; data.ratings.style.display = 'none'; data.extension = null; @@ -209,12 +206,6 @@ export class Renderer implements IPagedRenderer { data.name.textContent = extension.displayName; data.description.textContent = extension.description; - const updatePublisher = () => { - data.publisherDisplayName.textContent = !extension.resourceExtension && extension.local?.source !== 'resource' ? extension.publisherDisplayName : ''; - }; - updatePublisher(); - Event.filter(this.extensionsWorkbenchService.onChange, e => !!e && areSameExtensions(e.identifier, extension.identifier))(() => updatePublisher(), this, data.extensionDisposables); - data.installCount.style.display = ''; data.ratings.style.display = ''; data.extension = extension; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 6fc27676d0e..20325ef00d3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IUserDataSyncEnablementService } from '../../../../platform/userDataSync/common/userDataSync.js'; -import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, warningIcon } from './extensionsIcons.js'; +import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, privateExtensionIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, warningIcon } from './extensionsIcons.js'; import { registerColor, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; @@ -46,6 +46,10 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IExplorerService } from '../../files/browser/files.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js'; export abstract class ExtensionWidget extends Disposable implements IExtensionContainer { private _extension: IExtension | null = null; @@ -71,17 +75,26 @@ export function onClick(element: HTMLElement, callback: () => void): IDisposable export class InstallCountWidget extends ExtensionWidget { + private readonly disposables = this._register(new DisposableStore()); + constructor( - private container: HTMLElement, + readonly container: HTMLElement, private small: boolean, + @IHoverService private readonly hoverService: IHoverService, ) { super(); - container.classList.add('extension-install-count'); this.render(); + + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.container.innerText = ''; + this.disposables.clear(); } render(): void { - this.container.innerText = ''; + this.clear(); if (!this.extension) { return; @@ -96,9 +109,18 @@ export class InstallCountWidget extends ExtensionWidget { return; } - append(this.container, $('span' + ThemeIcon.asCSSSelector(installCountIcon))); - const count = append(this.container, $('span.count')); + if (!this.small && !this.extension.url) { + return; + } + + const parent = this.small ? this.container : append(this.container, $('span.install', { tabIndex: 0 })); + append(parent, $('span' + ThemeIcon.asCSSSelector(installCountIcon))); + const count = append(parent, $('span.count')); count.textContent = installLabel; + + if (!this.small) { + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.container, localize('install count', "Install count"))); + } } static getInstallLabel(extension: IExtension, small: boolean): string | undefined { @@ -129,12 +151,14 @@ export class InstallCountWidget extends ExtensionWidget { export class RatingsWidget extends ExtensionWidget { - private readonly containerHover: IManagedHover; + private containerHover: IManagedHover | undefined; + private readonly disposables = this._register(new DisposableStore()); constructor( - private container: HTMLElement, + readonly container: HTMLElement, private small: boolean, - @IHoverService hoverService: IHoverService + @IHoverService private readonly hoverService: IHoverService, + @IOpenerService private readonly openerService: IOpenerService, ) { super(); container.classList.add('extension-ratings'); @@ -143,13 +167,17 @@ export class RatingsWidget extends ExtensionWidget { container.classList.add('small'); } - this.containerHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, '')); - this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.container.innerText = ''; + this.disposables.clear(); } render(): void { - this.container.innerText = ''; + this.clear(); if (!this.extension) { return; @@ -167,51 +195,71 @@ export class RatingsWidget extends ExtensionWidget { return; } + if (!this.extension.url) { + return; + } + const rating = Math.round(this.extension.rating * 2) / 2; - this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating)); if (this.small) { append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon))); const count = append(this.container, $('span.count')); count.textContent = String(rating); } else { + const element = append(this.container, $('span.rating.clickable', { tabIndex: 0 })); for (let i = 1; i <= 5; i++) { if (rating >= i) { - append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon))); + append(element, $('span' + ThemeIcon.asCSSSelector(starFullIcon))); } else if (rating >= i - 0.5) { - append(this.container, $('span' + ThemeIcon.asCSSSelector(starHalfIcon))); + append(element, $('span' + ThemeIcon.asCSSSelector(starHalfIcon))); } else { - append(this.container, $('span' + ThemeIcon.asCSSSelector(starEmptyIcon))); + append(element, $('span' + ThemeIcon.asCSSSelector(starEmptyIcon))); } } if (this.extension.ratingCount) { - const ratingCountElemet = append(this.container, $('span', undefined, ` (${this.extension.ratingCount})`)); + const ratingCountElemet = append(element, $('span', undefined, ` (${this.extension.ratingCount})`)); ratingCountElemet.style.paddingLeft = '1px'; } + + this.containerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, '')); + this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating)); + element.setAttribute('role', 'link'); + if (this.extension.ratingUrl) { + this.disposables.add(onClick(element, () => this.openerService.open(URI.parse(this.extension!.ratingUrl!)))); + } } } + } -export class VerifiedPublisherWidget extends ExtensionWidget { +export class PublisherWidget extends ExtensionWidget { + + private element: HTMLElement | undefined; + private containerHover: IManagedHover | undefined; private readonly disposables = this._register(new DisposableStore()); - private readonly containerHover: IManagedHover; constructor( - private container: HTMLElement, + readonly container: HTMLElement, private small: boolean, - @IHoverService hoverService: IHoverService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IHoverService private readonly hoverService: IHoverService, @IOpenerService private readonly openerService: IOpenerService, ) { super(); - this.containerHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, '')); + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.element?.remove(); + this.disposables.clear(); } render(): void { - reset(this.container); - this.disposables.clear(); - if (!this.extension?.publisherDomain?.verified) { + this.clear(); + if (!this.extension) { return; } @@ -223,20 +271,45 @@ export class VerifiedPublisherWidget extends ExtensionWidget { return; } - const publisherDomainLink = URI.parse(this.extension.publisherDomain.link); - const verifiedPublisher = append(this.container, $('span.extension-verified-publisher.clickable')); - append(verifiedPublisher, renderIcon(verifiedPublisherIcon)); + this.element = append(this.container, $('.publisher')); + const publisherDisplayName = $('.publisher-name.ellipsis'); + publisherDisplayName.textContent = this.extension.publisherDisplayName; - if (!this.small) { - verifiedPublisher.tabIndex = 0; - this.containerHover.update(`Verified Domain: ${this.extension.publisherDomain.link}`); - verifiedPublisher.setAttribute('role', 'link'); + const verifiedPublisher = $('.verified-publisher'); + append(verifiedPublisher, $('span.extension-verified-publisher.clickable'), renderIcon(verifiedPublisherIcon)); - append(verifiedPublisher, $('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority)); - this.disposables.add(onClick(verifiedPublisher, () => this.openerService.open(publisherDomainLink))); + if (this.small) { + if (this.extension.publisherDomain) { + append(this.element, verifiedPublisher); + } + append(this.element, publisherDisplayName); + } else { + this.element.classList.toggle('clickable', !!this.extension.url); + this.element.setAttribute('role', 'button'); + this.element.tabIndex = 0; + + this.containerHover = this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('publisher', "Publisher ({0})", this.extension.publisherDisplayName))); + append(this.element, publisherDisplayName); + + if (this.extension.publisherDomain) { + append(this.element, verifiedPublisher); + const publisherDomainLink = URI.parse(this.extension.publisherDomain.link); + verifiedPublisher.tabIndex = 0; + verifiedPublisher.setAttribute('role', 'button'); + this.containerHover.update(localize('verified publisher', "This publisher has verified ownership of {0}", this.extension.publisherDomain.link)); + verifiedPublisher.setAttribute('role', 'link'); + + append(verifiedPublisher, $('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority)); + this.disposables.add(onClick(verifiedPublisher, () => this.openerService.open(publisherDomainLink))); + } + + if (this.extension.url) { + this.disposables.add(onClick(this.element, () => this.extensionsWorkbenchService.openSearch(`publisher:"${this.extension?.publisherDisplayName}"`))); + } } } + } export class SponsorWidget extends ExtensionWidget { @@ -244,7 +317,7 @@ export class SponsorWidget extends ExtensionWidget { private readonly disposables = this._register(new DisposableStore()); constructor( - private container: HTMLElement, + readonly container: HTMLElement, @IHoverService private readonly hoverService: IHoverService, @IOpenerService private readonly openerService: IOpenerService, ) { @@ -445,6 +518,73 @@ export class ExtensionPackCountWidget extends ExtensionWidget { } } +export class ExtensionKindIndicatorWidget extends ExtensionWidget { + + private element: HTMLElement | undefined; + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + readonly container: HTMLElement, + private small: boolean, + @IHoverService private readonly hoverService: IHoverService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IExplorerService private readonly explorerService: IExplorerService, + @IViewsService private readonly viewsService: IViewsService, + ) { + super(); + this.render(); + this._register(toDisposable(() => this.clear())); + } + + private clear(): void { + this.element?.remove(); + this.disposables.clear(); + } + + render(): void { + this.clear(); + + if (this.small) { + return; + } + + if (!this.extension) { + return; + } + + if (this.extension?.private) { + this.element = append(this.container, $('.extension-kind-indicator')); + append(this.element, $('span' + ThemeIcon.asCSSSelector(privateExtensionIcon))); + if (!this.small) { + append(this.element, $('span.private-extension-label', undefined, localize('privateExtension', "Private Extension"))); + } + return; + } + + const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined); + if (!location) { + return; + } + + this.element = append(this.container, $('.extension-kind-indicator')); + const workspaceFolder = this.contextService.getWorkspaceFolder(location); + if (workspaceFolder && this.extension.isWorkspaceScoped) { + this.element.textContent = localize('workspace extension', "Workspace Extension"); + this.element.classList.add('clickable'); + this.element.setAttribute('role', 'button'); + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location))); + this.disposables.add(onClick(this.element, () => { + this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true)); + })); + } else { + this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, location.path)); + this.element.textContent = localize('local extension', "Local Extension"); + } + } +} + export class SyncIgnoredWidget extends ExtensionWidget { private readonly disposables = this._register(new DisposableStore()); @@ -595,8 +735,12 @@ export class ExtensionHoverWidget extends ExtensionWidget { } markdown.appendText(`\n`); + let addSeparator = false; + if (this.extension.private) { + markdown.appendMarkdown(`$(${privateExtensionIcon.id}) ${localize('privateExtension', "Private Extension")}`); + addSeparator = true; + } if (this.extension.state === ExtensionState.Installed) { - let addSeparator = false; const installLabel = InstallCountWidget.getInstallLabel(this.extension, true); if (installLabel) { if (addSeparator) { @@ -620,9 +764,9 @@ export class ExtensionHoverWidget extends ExtensionWidget { markdown.appendMarkdown(`$(${sponsorIcon.id}) [${localize('sponsor', "Sponsor")}](${this.extension.publisherSponsorLink})`); addSeparator = true; } - if (addSeparator) { - markdown.appendText(`\n`); - } + } + if (addSeparator) { + markdown.appendText(`\n`); } const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined); @@ -862,6 +1006,7 @@ export class ExtensionRecommendationWidget extends ExtensionWidget { export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), false); export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), false); export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), false); +export const extensionPrivateBadgeBackground = registerColor('extensionIcon.privateForeground', { dark: '#ffffff60', light: '#00000060', hcDark: '#ffffff60', hcLight: '#00000060' }, localize('extensionIcon.private', "The icon color for private extensions.")); registerThemingParticipant((theme, collector) => { const extensionRatingIcon = theme.getColor(extensionRatingIconColor); @@ -877,4 +1022,9 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`); collector.addRule(`.extension-editor > .header > .details > .subtitle .sponsor ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`); + + const privateBadgeBackground = theme.getColor(extensionPrivateBadgeBackground); + if (privateBadgeBackground) { + collector.addRule(`.extension-private-badge { color: ${privateBadgeBackground}; }`); + } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index b5a1450cca2..90b805560e1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -211,6 +211,10 @@ export class Extension implements IExtension { return this.local ? this.local.manifest.version : this.latestVersion; } + get private(): boolean { + return this.local ? this.local.private : this.gallery ? this.gallery.private : false; + } + get pinned(): boolean { return !!this.local?.pinned; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 30bbba041ad..df7aaffb5e9 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -92,11 +92,17 @@ .extension-list-item > .details > .header-container > .header > .activation-status, .extension-list-item > .details > .header-container > .header > .install-count, +.extension-list-item > .details > .header-container > .header .extension-kind-indicator, .extension-list-item > .details > .header-container > .header > .ratings { display: flex; align-items: center; } +.extension-list-item > .details > .header-container > .header .extension-kind-indicator { + font-size: 80%; + margin-left: 2px; +} + .extension-list-item > .details > .header-container > .header > .install-count:not(:empty) { font-size: 80%; margin: 0 6px; @@ -179,24 +185,27 @@ align-items: center; } -.extension-list-item > .details > .footer > .author { +.extension-list-item > .details > .footer > .publisher-container { flex: 1; - display: flex; - align-items: center; line-height: 24px; } -.extension-list-item > .details > .footer > .author > .publisher-name { +.extension-list-item > .details > .footer .publisher { + display: flex; + align-items: center; +} + +.extension-list-item > .details > .footer .publisher > .publisher-name { font-size: 90%; color: var(--vscode-descriptionForeground); font-weight: 600; } -.monaco-list-row.selected .extension-list-item > .details > .footer > .author > .publisher-name{ +.monaco-list-row.selected .extension-list-item > .details > .footer .publisher > .publisher-name{ color: unset; } -.extension-list-item > .details > .footer > .author > .publisher-name:not(:first-child) { +.extension-list-item > .details > .footer .publisher > .publisher-name:not(:first-child) { padding-left: 1px; } @@ -227,7 +236,7 @@ min-width: 0; } -.monaco-list-row.disabled:not(.selected) .extension-list-item > .details > .footer > .author > .publisher-name { +.monaco-list-row.disabled:not(.selected) .extension-list-item > .details > .footer .publisher > .publisher-name { color: var(--vscode-disabledForeground); } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 03023ddf5fe..62be7a0b84a 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -148,6 +148,7 @@ } .extension-editor > .header > .details > .subtitle, +.extension-editor > .header > .details > .subtitle .extension-kind-indicator, .extension-editor > .header > .details > .subtitle .install, .extension-editor > .header > .details > .subtitle .rating, .extension-editor > .header > .details > .subtitle .sponsor { @@ -163,10 +164,14 @@ margin-left: 6px; } -.extension-editor > .header > .details > .subtitle > div:not(:first-child):not(:empty):not(.resource) { - border-left: 1px solid rgba(128, 128, 128, 0.7); - margin-left: 14px; - padding-left: 14px; +.extension-editor > .header > .details > .subtitle .extension-kind-indicator > .codicon { + margin-right: 6px; +} + +.extension-editor > .header > .details > .subtitle > .subtitle-entry:not(:empty):not(.last-non-empty) { + border-right: 1px solid rgba(128, 128, 128, 0.7); + margin-right: 14px; + padding-right: 14px; } .extension-editor > .header > .details > .description { @@ -734,7 +739,7 @@ padding-top: 5px; } -.extension-editor .subcontent .monaco-list-row .extension > .details > .footer > .author { +.extension-editor .subcontent .monaco-list-row .extension > .details > .footer .publisher { font-size: 90%; font-weight: 600; opacity: 0.6; diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css index 6a7ab84a23a..c5d81e6c952 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css @@ -31,12 +31,12 @@ opacity: .75; } -.extension-verified-publisher { +.verified-publisher { display: flex; align-items: center; } -.extension-verified-publisher > .extension-verified-publisher-domain { +.verified-publisher > .extension-verified-publisher-domain { padding-left: 2px; color: var(--vscode-extensionIcon-verifiedForeground); text-decoration: var(--text-link-decoration); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index ad36a535de6..4a232d2d560 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -68,6 +68,7 @@ export interface IExtension { readonly publisherSponsorLink?: URI; readonly pinned: boolean; readonly version: string; + readonly private: boolean; readonly latestVersion: string; readonly preRelease: boolean; readonly isPreReleaseVersion: boolean; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 2cb589f849f..e77726ba5cc 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -1409,6 +1409,7 @@ class WorkspaceExtensionsManagementService extends Disposable { updated: !!extension.metadata?.updated, pinned: !!extension.metadata?.pinned, isWorkspaceScoped: true, + private: false, source: 'resource', size: extension.metadata?.size ?? 0, }; diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index c3e21ded56a..1cde4cdfc68 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -237,6 +237,7 @@ function toLocalExtension(extension: IExtension): ILocalExtension { targetPlatform: TargetPlatform.WEB, updated: !!metadata.updated, pinned: !!metadata?.pinned, + private: !!metadata.private, isWorkspaceScoped: false, source: metadata?.source ?? (extension.identifier.uuid ? 'gallery' : 'resource'), size: metadata.size ?? 0, @@ -291,6 +292,7 @@ class InstallExtensionTask extends AbstractExtensionTask implem metadata.isSystem = existingExtension?.type === ExtensionType.System ? true : undefined; metadata.updated = !!existingExtension; metadata.isApplicationScoped = this.options.isApplicationScoped || metadata.isApplicationScoped; + metadata.private = this.extension.private; metadata.preRelease = isBoolean(this.options.preRelease) ? this.options.preRelease : this.options.installPreReleaseVersion || this.extension.properties.isPreReleaseVersion || metadata.preRelease;