Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionEditor.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 { $, Dimension, append, hide, setParentFlowTo, show } from '../../../../base/browser/dom.js';6import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';7import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';8import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';9import { CheckboxActionViewItem } from '../../../../base/browser/ui/toggle/toggle.js';10import { Action, IAction } from '../../../../base/common/actions.js';11import * as arrays from '../../../../base/common/arrays.js';12import { Cache, CacheResult } from '../../../../base/common/cache.js';13import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';14import { isCancellationError } from '../../../../base/common/errors.js';15import { Emitter, Event } from '../../../../base/common/event.js';16import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';17import { Disposable, DisposableStore, MutableDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js';18import { Schemas, matchesScheme } from '../../../../base/common/network.js';19import { isNative, language } from '../../../../base/common/platform.js';20import { isUndefined } from '../../../../base/common/types.js';21import { URI } from '../../../../base/common/uri.js';22import { generateUuid } from '../../../../base/common/uuid.js';23import './media/extensionEditor.css';24import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';25import { TokenizationRegistry } from '../../../../editor/common/languages.js';26import { ILanguageService } from '../../../../editor/common/languages/language.js';27import { generateTokensCSSForColorMap } from '../../../../editor/common/languages/supports/tokenization.js';28import { localize } from '../../../../nls.js';29import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';30import { ContextKeyExpr, IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';31import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';32import { computeSize, FilterType, IExtensionGalleryService, IGalleryExtension, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';33import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';34import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';35import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';36import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';37import { INotificationService } from '../../../../platform/notification/common/notification.js';38import { IOpenerService } from '../../../../platform/opener/common/opener.js';39import { IStorageService } from '../../../../platform/storage/common/storage.js';40import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';41import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js';42import { buttonForeground, buttonHoverBackground, editorBackground, textLinkActiveForeground, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js';43import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';44import { EditorPane } from '../../../browser/parts/editor/editorPane.js';45import { IEditorOpenContext } from '../../../common/editor.js';46import { ExtensionFeaturesTab } from './extensionFeaturesTab.js';47import {48ButtonWithDropDownExtensionAction,49ClearLanguageAction,50DisableDropDownAction,51EnableDropDownAction,52ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction,53ExtensionEditorManageExtensionAction,54ExtensionStatusAction,55ExtensionStatusLabelAction,56InstallAnotherVersionAction,57InstallDropdownAction, InstallingLabelAction,58LocalInstallAction,59MigrateDeprecatedExtensionAction,60ExtensionRuntimeStateAction,61RemoteInstallAction,62SetColorThemeAction,63SetFileIconThemeAction,64SetLanguageAction,65SetProductIconThemeAction,66ToggleAutoUpdateForExtensionAction,67UninstallAction,68UpdateAction,69WebInstallAction,70TogglePreReleaseExtensionAction,71} from './extensionsActions.js';72import { Delegate } from './extensionsList.js';73import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from './extensionsViewer.js';74import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, PublisherWidget, onClick, ExtensionKindIndicatorWidget, ExtensionIconWidget } from './extensionsWidgets.js';75import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsWorkbenchService } from '../common/extensions.js';76import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js';77import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js';78import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from '../../webview/browser/webview.js';79import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';80import { IEditorService } from '../../../services/editor/common/editorService.js';81import { IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';82import { IExtensionService } from '../../../services/extensions/common/extensions.js';83import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';84import { IHoverService } from '../../../../platform/hover/browser/hover.js';85import { ByteSize, IFileService } from '../../../../platform/files/common/files.js';86import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';87import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';88import { IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';89import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js';9091function toDateString(date: Date) {92return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}, ${date.toLocaleTimeString(language, { hourCycle: 'h23' })}`;93}9495class NavBar extends Disposable {9697private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>());98get onChange(): Event<{ id: string | null; focus: boolean }> { return this._onChange.event; }99100private _currentId: string | null = null;101get currentId(): string | null { return this._currentId; }102103private actions: Action[];104private actionbar: ActionBar;105106constructor(container: HTMLElement) {107super();108const element = append(container, $('.navbar'));109this.actions = [];110this.actionbar = this._register(new ActionBar(element));111}112113push(id: string, label: string, tooltip: string): void {114const action = new Action(id, label, undefined, true, () => this.update(id, true));115116action.tooltip = tooltip;117118this.actions.push(action);119this.actionbar.push(action);120121if (this.actions.length === 1) {122this.update(id);123}124}125126clear(): void {127this.actions = dispose(this.actions);128this.actionbar.clear();129}130131switch(id: string): boolean {132const action = this.actions.find(action => action.id === id);133if (action) {134action.run();135return true;136}137return false;138}139140private update(id: string, focus?: boolean): void {141this._currentId = id;142this._onChange.fire({ id, focus: !!focus });143this.actions.forEach(a => a.checked = a.id === id);144}145}146147interface ILayoutParticipant {148layout(): void;149}150151interface IActiveElement {152focus(): void;153}154155interface IExtensionEditorTemplate {156name: HTMLElement;157preview: HTMLElement;158builtin: HTMLElement;159description: HTMLElement;160actionsAndStatusContainer: HTMLElement;161extensionActionBar: ActionBar;162navbar: NavBar;163content: HTMLElement;164header: HTMLElement;165extension: IExtension;166gallery: IGalleryExtension | null;167manifest: IExtensionManifest | null;168}169170const enum WebviewIndex {171Readme,172Changelog173}174175const CONTEXT_SHOW_PRE_RELEASE_VERSION = new RawContextKey<boolean>('showPreReleaseVersion', false);176177abstract class ExtensionWithDifferentGalleryVersionWidget extends ExtensionWidget {178private _gallery: IGalleryExtension | null = null;179get gallery(): IGalleryExtension | null { return this._gallery; }180set gallery(gallery: IGalleryExtension | null) {181if (this.extension && gallery && !areSameExtensions(this.extension.identifier, gallery.identifier)) {182return;183}184this._gallery = gallery;185this.update();186}187}188189class VersionWidget extends ExtensionWithDifferentGalleryVersionWidget {190private readonly element: HTMLElement;191constructor(192container: HTMLElement,193hoverService: IHoverService194) {195super();196this.element = append(container, $('code.version', undefined, 'pre-release'));197this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('extension version', "Extension Version")));198this.render();199}200render(): void {201if (this.extension?.preRelease) {202show(this.element);203} else {204hide(this.element);205}206}207}208209export class ExtensionEditor extends EditorPane {210211static readonly ID: string = 'workbench.editor.extension';212213private readonly _scopedContextKeyService = this._register(new MutableDisposable<IScopedContextKeyService>());214private template: IExtensionEditorTemplate | undefined;215216private extensionReadme: Cache<string> | null;217private extensionChangelog: Cache<string> | null;218private extensionManifest: Cache<IExtensionManifest | null> | null;219220// Some action bar items use a webview whose vertical scroll position we track in this map221private initialScrollProgress: Map<WebviewIndex, number> = new Map();222223// Spot when an ExtensionEditor instance gets reused for a different extension, in which case the vertical scroll positions must be zeroed224private currentIdentifier: string = '';225226private layoutParticipants: ILayoutParticipant[] = [];227private readonly contentDisposables = this._register(new DisposableStore());228private readonly transientDisposables = this._register(new DisposableStore());229private activeElement: IActiveElement | null = null;230private dimension: Dimension | undefined;231232private showPreReleaseVersionContextKey: IContextKey<boolean> | undefined;233234constructor(235group: IEditorGroup,236@ITelemetryService telemetryService: ITelemetryService,237@IInstantiationService private readonly instantiationService: IInstantiationService,238@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,239@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,240@IThemeService themeService: IThemeService,241@INotificationService private readonly notificationService: INotificationService,242@IOpenerService private readonly openerService: IOpenerService,243@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,244@IStorageService storageService: IStorageService,245@IExtensionService private readonly extensionService: IExtensionService,246@IWebviewService private readonly webviewService: IWebviewService,247@ILanguageService private readonly languageService: ILanguageService,248@IContextMenuService private readonly contextMenuService: IContextMenuService,249@IContextKeyService private readonly contextKeyService: IContextKeyService,250@IHoverService private readonly hoverService: IHoverService,251) {252super(ExtensionEditor.ID, group, telemetryService, themeService, storageService);253this.extensionReadme = null;254this.extensionChangelog = null;255this.extensionManifest = null;256}257258override get scopedContextKeyService(): IContextKeyService | undefined {259return this._scopedContextKeyService.value;260}261262protected createEditor(parent: HTMLElement): void {263const root = append(parent, $('.extension-editor'));264this._scopedContextKeyService.value = this.contextKeyService.createScoped(root);265this._scopedContextKeyService.value.createKey('inExtensionEditor', true);266this.showPreReleaseVersionContextKey = CONTEXT_SHOW_PRE_RELEASE_VERSION.bindTo(this._scopedContextKeyService.value);267268root.tabIndex = 0; // this is required for the focus tracker on the editor269root.style.outline = 'none';270root.setAttribute('role', 'document');271const header = append(root, $('.header'));272273const iconContainer = append(header, $('.icon-container'));274const iconWidget = this.instantiationService.createInstance(ExtensionIconWidget, iconContainer);275const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, iconContainer, true);276277const details = append(header, $('.details'));278const title = append(details, $('.title'));279const name = append(title, $('span.name.clickable', { role: 'heading', tabIndex: 0 }));280this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name")));281const versionWidget = new VersionWidget(title, this.hoverService);282283const preview = append(title, $('span.preview'));284this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), preview, localize('preview', "Preview")));285preview.textContent = localize('preview', "Preview");286287const builtin = append(title, $('span.builtin'));288builtin.textContent = localize('builtin', "Built-in");289290const subtitle = append(details, $('.subtitle'));291const subTitleEntryContainers: HTMLElement[] = [];292293const publisherContainer = append(subtitle, $('.subtitle-entry'));294subTitleEntryContainers.push(publisherContainer);295const publisherWidget = this.instantiationService.createInstance(PublisherWidget, publisherContainer, false);296297const extensionKindContainer = append(subtitle, $('.subtitle-entry'));298subTitleEntryContainers.push(extensionKindContainer);299const extensionKindWidget = this.instantiationService.createInstance(ExtensionKindIndicatorWidget, extensionKindContainer, false);300301const installCountContainer = append(subtitle, $('.subtitle-entry'));302subTitleEntryContainers.push(installCountContainer);303const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCountContainer, false);304305const ratingsContainer = append(subtitle, $('.subtitle-entry'));306subTitleEntryContainers.push(ratingsContainer);307const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, ratingsContainer, false);308309const sponsorContainer = append(subtitle, $('.subtitle-entry'));310subTitleEntryContainers.push(sponsorContainer);311const sponsorWidget = this.instantiationService.createInstance(SponsorWidget, sponsorContainer);312313const widgets: ExtensionWidget[] = [314iconWidget,315remoteBadge,316versionWidget,317publisherWidget,318extensionKindWidget,319installCountWidget,320ratingsWidget,321sponsorWidget,322];323324const description = append(details, $('.description'));325326const installAction = this.instantiationService.createInstance(InstallDropdownAction);327const actions = [328this.instantiationService.createInstance(ExtensionRuntimeStateAction),329this.instantiationService.createInstance(ExtensionStatusLabelAction),330this.instantiationService.createInstance(UpdateAction, true),331this.instantiationService.createInstance(SetColorThemeAction),332this.instantiationService.createInstance(SetFileIconThemeAction),333this.instantiationService.createInstance(SetProductIconThemeAction),334this.instantiationService.createInstance(SetLanguageAction),335this.instantiationService.createInstance(ClearLanguageAction),336337this.instantiationService.createInstance(EnableDropDownAction),338this.instantiationService.createInstance(DisableDropDownAction),339this.instantiationService.createInstance(RemoteInstallAction, false),340this.instantiationService.createInstance(LocalInstallAction),341this.instantiationService.createInstance(WebInstallAction),342installAction,343this.instantiationService.createInstance(InstallingLabelAction),344this.instantiationService.createInstance(ButtonWithDropDownExtensionAction, 'extensions.uninstall', UninstallAction.UninstallClass, [345[346this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, false),347this.instantiationService.createInstance(UninstallAction),348this.instantiationService.createInstance(InstallAnotherVersionAction, null, true),349]350]),351this.instantiationService.createInstance(TogglePreReleaseExtensionAction),352this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction),353new ExtensionEditorManageExtensionAction(this.scopedContextKeyService || this.contextKeyService, this.instantiationService),354];355356const actionsAndStatusContainer = append(details, $('.actions-status-container'));357const extensionActionBar = this._register(new ActionBar(actionsAndStatusContainer, {358actionViewItemProvider: (action: IAction, options) => {359if (action instanceof DropDownExtensionAction) {360return action.createActionViewItem(options);361}362if (action instanceof ButtonWithDropDownExtensionAction) {363return new ButtonWithDropdownExtensionActionViewItem(364action,365{366...options,367icon: true,368label: true,369menuActionsOrProvider: { getActions: () => action.menuActions },370menuActionClassNames: action.menuActionClassNames371},372this.contextMenuService);373}374if (action instanceof ToggleAutoUpdateForExtensionAction) {375return new CheckboxActionViewItem(undefined, action, { ...options, icon: true, label: true, checkboxStyles: defaultCheckboxStyles });376}377return undefined;378},379focusOnlyEnabledItems: true380}));381382extensionActionBar.push(actions, { icon: true, label: true });383extensionActionBar.setFocusable(true);384// update focusable elements when the enablement of an action changes385this._register(Event.any(...actions.map(a => Event.filter(a.onDidChange, e => e.enabled !== undefined)))(() => {386extensionActionBar.setFocusable(false);387extensionActionBar.setFocusable(true);388}));389390const otherExtensionContainers: IExtensionContainer[] = [];391const extensionStatusAction = this.instantiationService.createInstance(ExtensionStatusAction);392const extensionStatusWidget = this._register(this.instantiationService.createInstance(ExtensionStatusWidget, append(actionsAndStatusContainer, $('.status')), extensionStatusAction));393394otherExtensionContainers.push(extensionStatusAction, new class extends ExtensionWidget {395render() {396actionsAndStatusContainer.classList.toggle('list-layout', this.extension?.state === ExtensionState.Installed);397}398}());399400const recommendationWidget = this.instantiationService.createInstance(ExtensionRecommendationWidget, append(details, $('.recommendation')));401widgets.push(recommendationWidget);402403this._register(Event.any(extensionStatusWidget.onDidRender, recommendationWidget.onDidRender)(() => {404if (this.dimension) {405this.layout(this.dimension);406}407}));408409const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, ...otherExtensionContainers]);410for (const disposable of [...actions, ...widgets, ...otherExtensionContainers, extensionContainers]) {411this._register(disposable);412}413414const onError = Event.chain(extensionActionBar.onDidRun, $ =>415$.map(({ error }) => error)416.filter(error => !!error)417);418419this._register(onError(this.onError, this));420421const body = append(root, $('.body'));422const navbar = new NavBar(body);423424const content = append(body, $('.content'));425content.id = generateUuid(); // An id is needed for the webview parent flow to426427this.template = {428builtin,429content,430description,431header,432name,433navbar,434preview,435actionsAndStatusContainer,436extensionActionBar,437set extension(extension: IExtension) {438extensionContainers.extension = extension;439let lastNonEmptySubtitleEntryContainer;440for (const subTitleEntryElement of subTitleEntryContainers) {441subTitleEntryElement.classList.remove('last-non-empty');442if (subTitleEntryElement.children.length > 0) {443lastNonEmptySubtitleEntryContainer = subTitleEntryElement;444}445}446if (lastNonEmptySubtitleEntryContainer) {447lastNonEmptySubtitleEntryContainer.classList.add('last-non-empty');448}449},450set gallery(gallery: IGalleryExtension | null) {451versionWidget.gallery = gallery;452},453set manifest(manifest: IExtensionManifest | null) {454installAction.manifest = manifest;455}456};457}458459override async setInput(input: ExtensionsInput, options: IExtensionEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {460await super.setInput(input, options, context, token);461this.updatePreReleaseVersionContext();462if (this.template) {463await this.render(input.extension, this.template, !!options?.preserveFocus);464}465}466467override setOptions(options: IExtensionEditorOptions | undefined): void {468const currentOptions: IExtensionEditorOptions | undefined = this.options;469super.setOptions(options);470this.updatePreReleaseVersionContext();471472if (this.input && this.template && currentOptions?.showPreReleaseVersion !== options?.showPreReleaseVersion) {473this.render((this.input as ExtensionsInput).extension, this.template, !!options?.preserveFocus);474return;475}476477if (options?.tab) {478this.template?.navbar.switch(options.tab);479}480481}482483private updatePreReleaseVersionContext(): void {484let showPreReleaseVersion = (<IExtensionEditorOptions | undefined>this.options)?.showPreReleaseVersion;485if (isUndefined(showPreReleaseVersion)) {486showPreReleaseVersion = !!(<ExtensionsInput>this.input).extension.gallery?.properties.isPreReleaseVersion;487}488this.showPreReleaseVersionContextKey?.set(showPreReleaseVersion);489}490491async openTab(tab: ExtensionEditorTab): Promise<void> {492if (!this.input || !this.template) {493return;494}495if (this.template.navbar.switch(tab)) {496return;497}498// Fallback to Readme tab if ExtensionPack tab does not exist499if (tab === ExtensionEditorTab.ExtensionPack) {500this.template.navbar.switch(ExtensionEditorTab.Readme);501}502}503504private async getGalleryVersionToShow(extension: IExtension, preRelease?: boolean): Promise<IGalleryExtension | null> {505if (extension.resourceExtension) {506return null;507}508if (extension.local?.source === 'resource') {509return null;510}511if (isUndefined(preRelease)) {512return null;513}514if (preRelease === extension.gallery?.properties.isPreReleaseVersion) {515return null;516}517if (preRelease && !extension.hasPreReleaseVersion) {518return null;519}520if (!preRelease && !extension.hasReleaseVersion) {521return null;522}523return (await this.extensionGalleryService.getExtensions([{ ...extension.identifier, preRelease, hasPreRelease: extension.hasPreReleaseVersion }], CancellationToken.None))[0] || null;524}525526private async render(extension: IExtension, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {527this.activeElement = null;528this.transientDisposables.clear();529530const token = this.transientDisposables.add(new CancellationTokenSource()).token;531532const gallery = await this.getGalleryVersionToShow(extension, (this.options as IExtensionEditorOptions)?.showPreReleaseVersion);533if (token.isCancellationRequested) {534return;535}536537this.extensionReadme = new Cache(() => gallery ? this.extensionGalleryService.getReadme(gallery, token) : extension.getReadme(token));538this.extensionChangelog = new Cache(() => gallery ? this.extensionGalleryService.getChangelog(gallery, token) : extension.getChangelog(token));539this.extensionManifest = new Cache(() => gallery ? this.extensionGalleryService.getManifest(gallery, token) : extension.getManifest(token));540541template.extension = extension;542template.gallery = gallery;543template.manifest = null;544545template.name.textContent = extension.displayName;546template.name.classList.toggle('clickable', !!extension.url);547template.name.classList.toggle('deprecated', !!extension.deprecationInfo);548template.preview.style.display = extension.preview ? 'inherit' : 'none';549template.builtin.style.display = extension.isBuiltin ? 'inherit' : 'none';550551template.description.textContent = extension.description;552553if (extension.url) {554this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(extension.url!))));555}556557const manifest = await this.extensionManifest.get().promise;558if (token.isCancellationRequested) {559return;560}561562if (manifest) {563template.manifest = manifest;564}565566this.renderNavbar(extension, manifest, template, preserveFocus);567568// report telemetry569const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();570let recommendationsData = {};571if (extRecommendations[extension.identifier.id.toLowerCase()]) {572recommendationsData = { recommendationReason: extRecommendations[extension.identifier.id.toLowerCase()].reasonId };573}574/* __GDPR__575"extensionGallery:openExtension" : {576"owner": "sandy081",577"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },578"${include}": [579"${GalleryExtensionTelemetryData}"580]581}582*/583this.telemetryService.publicLog('extensionGallery:openExtension', { ...extension.telemetryData, ...recommendationsData });584585}586587private renderNavbar(extension: IExtension, manifest: IExtensionManifest | null, template: IExtensionEditorTemplate, preserveFocus: boolean): void {588template.content.innerText = '';589template.navbar.clear();590591if (this.currentIdentifier !== extension.identifier.id) {592this.initialScrollProgress.clear();593this.currentIdentifier = extension.identifier.id;594}595596template.navbar.push(ExtensionEditorTab.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file"));597if (manifest) {598template.navbar.push(ExtensionEditorTab.Features, localize('features', "Features"), localize('featurestooltip', "Lists features contributed by this extension"));599}600if (extension.hasChangelog()) {601template.navbar.push(ExtensionEditorTab.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file"));602}603if (extension.dependencies.length) {604template.navbar.push(ExtensionEditorTab.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on"));605}606if (manifest && manifest.extensionPack?.length && !this.shallRenderAsExtensionPack(manifest)) {607template.navbar.push(ExtensionEditorTab.ExtensionPack, localize('extensionpack', "Extension Pack"), localize('extensionpacktooltip', "Lists extensions those will be installed together with this extension"));608}609610if ((<IExtensionEditorOptions | undefined>this.options)?.tab) {611template.navbar.switch((<IExtensionEditorOptions>this.options).tab!);612}613if (template.navbar.currentId) {614this.onNavbarChange(extension, { id: template.navbar.currentId, focus: !preserveFocus }, template);615}616template.navbar.onChange(e => this.onNavbarChange(extension, e, template), this, this.transientDisposables);617}618619override clearInput(): void {620this.contentDisposables.clear();621this.transientDisposables.clear();622623super.clearInput();624}625626override focus(): void {627super.focus();628this.activeElement?.focus();629}630631showFind(): void {632this.activeWebview?.showFind();633}634635runFindAction(previous: boolean): void {636this.activeWebview?.runFindAction(previous);637}638639public get activeWebview(): IWebview | undefined {640if (!this.activeElement || !(this.activeElement as IWebview).runFindAction) {641return undefined;642}643return this.activeElement as IWebview;644}645646private onNavbarChange(extension: IExtension, { id, focus }: { id: string | null; focus: boolean }, template: IExtensionEditorTemplate): void {647this.contentDisposables.clear();648template.content.innerText = '';649this.activeElement = null;650if (id) {651const cts = new CancellationTokenSource();652this.contentDisposables.add(toDisposable(() => cts.dispose(true)));653this.open(id, extension, template, cts.token)654.then(activeElement => {655if (cts.token.isCancellationRequested) {656return;657}658this.activeElement = activeElement;659if (focus) {660this.focus();661}662});663}664}665666private open(id: string, extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {667switch (id) {668case ExtensionEditorTab.Readme: return this.openDetails(extension, template, token);669case ExtensionEditorTab.Features: return this.openFeatures(template, token);670case ExtensionEditorTab.Changelog: return this.openChangelog(extension, template, token);671case ExtensionEditorTab.Dependencies: return this.openExtensionDependencies(extension, template, token);672case ExtensionEditorTab.ExtensionPack: return this.openExtensionPack(extension, template, token);673}674return Promise.resolve(null);675}676677private async openMarkdown(extension: IExtension, cacheResult: CacheResult<string>, noContentCopy: string, container: HTMLElement, webviewIndex: WebviewIndex, title: string, token: CancellationToken): Promise<IActiveElement | null> {678try {679const body = await this.renderMarkdown(extension, cacheResult, container, token);680if (token.isCancellationRequested) {681return Promise.resolve(null);682}683684const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay({685title,686options: {687enableFindWidget: true,688tryRestoreScrollPosition: true,689disableServiceWorker: true,690},691contentOptions: {},692extension: undefined,693}));694695webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0;696697webview.claim(this, this.window, this.scopedContextKeyService);698setParentFlowTo(webview.container, container);699webview.layoutWebviewOverElement(container);700701webview.setHtml(body);702webview.claim(this, this.window, undefined);703704this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire()));705706this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress)));707708const removeLayoutParticipant = arrays.insert(this.layoutParticipants, {709layout: () => {710webview.layoutWebviewOverElement(container);711}712});713this.contentDisposables.add(toDisposable(removeLayoutParticipant));714715let isDisposed = false;716this.contentDisposables.add(toDisposable(() => { isDisposed = true; }));717718this.contentDisposables.add(this.themeService.onDidColorThemeChange(async () => {719// Render again since syntax highlighting of code blocks may have changed720const body = await this.renderMarkdown(extension, cacheResult, container);721if (!isDisposed) { // Make sure we weren't disposed of in the meantime722webview.setHtml(body);723}724}));725726this.contentDisposables.add(webview.onDidClickLink(link => {727if (!link) {728return;729}730// Only allow links with specific schemes731if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto)) {732this.openerService.open(link);733} else if (matchesScheme(link, Schemas.command) && extension.type === ExtensionType.System) {734this.openerService.open(link, {735allowCommands: [736ShowCurrentReleaseNotesActionId737]738});739}740}));741742return webview;743} catch (e) {744const p = append(container, $('p.nocontent'));745p.textContent = noContentCopy;746return p;747}748}749750private async renderMarkdown(extension: IExtension, cacheResult: CacheResult<string>, container: HTMLElement, token?: CancellationToken): Promise<string> {751const contents = await this.loadContents(() => cacheResult, container);752if (token?.isCancellationRequested) {753return '';754}755756const allowedLinkProtocols = [Schemas.http, Schemas.https, Schemas.mailto];757const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, {758sanitizerConfig: {759allowedLinkProtocols: {760override: extension.type === ExtensionType.System761? [...allowedLinkProtocols, Schemas.command]762: allowedLinkProtocols763}764}765}, token);766if (token?.isCancellationRequested) {767return '';768}769770return this.renderBody(content);771}772773private renderBody(body: TrustedHTML): string {774const nonce = generateUuid();775const colorMap = TokenizationRegistry.getColorMap();776const css = colorMap ? generateTokensCSSForColorMap(colorMap) : '';777return `<!DOCTYPE html>778<html>779<head>780<meta http-equiv="Content-type" content="text/html;charset=UTF-8">781<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src 'nonce-${nonce}';">782<style nonce="${nonce}">783${DEFAULT_MARKDOWN_STYLES}784785/* prevent scroll-to-top button from blocking the body text */786body {787padding-bottom: 75px;788}789790#scroll-to-top {791position: fixed;792width: 32px;793height: 32px;794right: 25px;795bottom: 25px;796background-color: var(--vscode-button-secondaryBackground);797border-color: var(--vscode-button-border);798border-radius: 50%;799cursor: pointer;800box-shadow: 1px 1px 1px rgba(0,0,0,.25);801outline: none;802display: flex;803justify-content: center;804align-items: center;805}806807#scroll-to-top:hover {808background-color: var(--vscode-button-secondaryHoverBackground);809box-shadow: 2px 2px 2px rgba(0,0,0,.25);810}811812body.vscode-high-contrast #scroll-to-top {813border-width: 2px;814border-style: solid;815box-shadow: none;816}817818#scroll-to-top span.icon::before {819content: "";820display: block;821background: var(--vscode-button-secondaryForeground);822/* Chevron up icon */823webkit-mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=');824-webkit-mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=');825width: 16px;826height: 16px;827}828${css}829</style>830</head>831<body>832<a id="scroll-to-top" role="button" aria-label="scroll to top" href="#"><span class="icon"></span></a>833${body}834</body>835</html>`;836}837838private async openDetails(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {839const details = append(template.content, $('.details'));840const readmeContainer = append(details, $('.readme-container'));841const additionalDetailsContainer = append(details, $('.additional-details-container'));842843const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500);844layout();845this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));846847let activeElement: IActiveElement | null = null;848const manifest = await this.extensionManifest!.get().promise;849if (manifest && manifest.extensionPack?.length && this.shallRenderAsExtensionPack(manifest)) {850activeElement = await this.openExtensionPackReadme(extension, manifest, readmeContainer, token);851} else {852activeElement = await this.openMarkdown(extension, this.extensionReadme!.get(), localize('noReadme', "No README available."), readmeContainer, WebviewIndex.Readme, localize('Readme title', "Readme"), token);853}854855this.renderAdditionalDetails(additionalDetailsContainer, extension);856return activeElement;857}858859private shallRenderAsExtensionPack(manifest: IExtensionManifest): boolean {860return !!(manifest.categories?.some(category => category.toLowerCase() === 'extension packs'));861}862863private async openExtensionPackReadme(extension: IExtension, manifest: IExtensionManifest, container: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {864if (token.isCancellationRequested) {865return Promise.resolve(null);866}867868const extensionPackReadme = append(container, $('div', { class: 'extension-pack-readme' }));869extensionPackReadme.style.margin = '0 auto';870extensionPackReadme.style.maxWidth = '882px';871872const extensionPack = append(extensionPackReadme, $('div', { class: 'extension-pack' }));873if (manifest.extensionPack!.length <= 3) {874extensionPackReadme.classList.add('one-row');875} else if (manifest.extensionPack!.length <= 6) {876extensionPackReadme.classList.add('two-rows');877} else if (manifest.extensionPack!.length <= 9) {878extensionPackReadme.classList.add('three-rows');879} else {880extensionPackReadme.classList.add('more-rows');881}882883const extensionPackHeader = append(extensionPack, $('div.header'));884extensionPackHeader.textContent = localize('extension pack', "Extension Pack ({0})", manifest.extensionPack!.length);885const extensionPackContent = append(extensionPack, $('div', { class: 'extension-pack-content' }));886extensionPackContent.setAttribute('tabindex', '0');887append(extensionPack, $('div.footer'));888const readmeContent = append(extensionPackReadme, $('div.readme-content'));889890await Promise.all([891this.renderExtensionPack(manifest, extensionPackContent, token),892this.openMarkdown(extension, this.extensionReadme!.get(), localize('noReadme', "No README available."), readmeContent, WebviewIndex.Readme, localize('Readme title', "Readme"), token),893]);894895return { focus: () => extensionPackContent.focus() };896}897898private renderAdditionalDetails(container: HTMLElement, extension: IExtension): void {899const content = $('div', { class: 'additional-details-content', tabindex: '0' });900const scrollableContent = new DomScrollableElement(content, {});901const layout = () => scrollableContent.scanDomNode();902const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });903this.contentDisposables.add(toDisposable(removeLayoutParticipant));904this.contentDisposables.add(scrollableContent);905906this.contentDisposables.add(this.instantiationService.createInstance(AdditionalDetailsWidget, content, extension));907908append(container, scrollableContent.getDomNode());909scrollableContent.scanDomNode();910}911912private openChangelog(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {913return this.openMarkdown(extension, this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template.content, WebviewIndex.Changelog, localize('Changelog title', "Changelog"), token);914}915916private async openFeatures(template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {917const manifest = await this.loadContents(() => this.extensionManifest!.get(), template.content);918if (token.isCancellationRequested) {919return null;920}921if (!manifest) {922return null;923}924925const extensionFeaturesTab = this.contentDisposables.add(this.instantiationService.createInstance(ExtensionFeaturesTab, manifest, (<IExtensionEditorOptions | undefined>this.options)?.feature));926const layout = () => extensionFeaturesTab.layout(template.content.clientHeight, template.content.clientWidth);927const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });928this.contentDisposables.add(toDisposable(removeLayoutParticipant));929append(template.content, extensionFeaturesTab.domNode);930layout();931return extensionFeaturesTab.domNode;932}933934private openExtensionDependencies(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {935if (token.isCancellationRequested) {936return Promise.resolve(null);937}938939if (arrays.isFalsyOrEmpty(extension.dependencies)) {940append(template.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies");941return Promise.resolve(template.content);942}943944const content = $('div', { class: 'subcontent' });945const scrollableContent = new DomScrollableElement(content, {});946append(template.content, scrollableContent.getDomNode());947this.contentDisposables.add(scrollableContent);948949const dependenciesTree = this.instantiationService.createInstance(ExtensionsTree,950new ExtensionData(extension, null, extension => extension.dependencies || [], this.extensionsWorkbenchService), content,951{952listBackground: editorBackground953});954const layout = () => {955scrollableContent.scanDomNode();956const scrollDimensions = scrollableContent.getScrollDimensions();957dependenciesTree.layout(scrollDimensions.height);958};959const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });960this.contentDisposables.add(toDisposable(removeLayoutParticipant));961962this.contentDisposables.add(dependenciesTree);963scrollableContent.scanDomNode();964return Promise.resolve({ focus() { dependenciesTree.domFocus(); } });965}966967private async openExtensionPack(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {968if (token.isCancellationRequested) {969return Promise.resolve(null);970}971const manifest = await this.loadContents(() => this.extensionManifest!.get(), template.content);972if (token.isCancellationRequested) {973return null;974}975if (!manifest) {976return null;977}978return this.renderExtensionPack(manifest, template.content, token);979}980981private async renderExtensionPack(manifest: IExtensionManifest, parent: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {982if (token.isCancellationRequested) {983return null;984}985986const content = $('div', { class: 'subcontent' });987const scrollableContent = new DomScrollableElement(content, { useShadows: false });988append(parent, scrollableContent.getDomNode());989990const extensionsGridView = this.instantiationService.createInstance(ExtensionsGridView, content, new Delegate());991const extensions: IExtension[] = await getExtensions(manifest.extensionPack!, this.extensionsWorkbenchService);992extensionsGridView.setExtensions(extensions);993scrollableContent.scanDomNode();994995this.contentDisposables.add(scrollableContent);996this.contentDisposables.add(extensionsGridView);997this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout: () => scrollableContent.scanDomNode() })));998999return content;1000}10011002private loadContents<T>(loadingTask: () => CacheResult<T>, container: HTMLElement): Promise<T> {1003container.classList.add('loading');10041005const result = this.contentDisposables.add(loadingTask());1006const onDone = () => container.classList.remove('loading');1007result.promise.then(onDone, onDone);10081009return result.promise;1010}10111012layout(dimension: Dimension): void {1013this.dimension = dimension;1014this.layoutParticipants.forEach(p => p.layout());1015}10161017private onError(err: any): void {1018if (isCancellationError(err)) {1019return;1020}10211022this.notificationService.error(err);1023}1024}10251026class AdditionalDetailsWidget extends Disposable {10271028private readonly disposables = this._register(new DisposableStore());10291030constructor(1031private readonly container: HTMLElement,1032extension: IExtension,1033@IHoverService private readonly hoverService: IHoverService,1034@IOpenerService private readonly openerService: IOpenerService,1035@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,1036@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,1037@IFileService private readonly fileService: IFileService,1038@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,1039@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,1040@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService,1041) {1042super();1043this.render(extension);1044this._register(this.extensionsWorkbenchService.onChange(e => {1045if (e && areSameExtensions(e.identifier, extension.identifier) && e.server === extension.server) {1046this.render(e);1047}1048}));1049}10501051private render(extension: IExtension): void {1052this.container.innerText = '';1053this.disposables.clear();10541055if (extension.local) {1056this.renderInstallInfo(this.container, extension.local);1057}1058if (extension.gallery) {1059this.renderMarketplaceInfo(this.container, extension);1060}1061this.renderCategories(this.container, extension);1062this.renderExtensionResources(this.container, extension);1063}10641065private renderCategories(container: HTMLElement, extension: IExtension): void {1066if (extension.categories.length) {1067const categoriesContainer = append(container, $('.categories-container.additional-details-element'));1068append(categoriesContainer, $('.additional-details-title', undefined, localize('categories', "Categories")));1069const categoriesElement = append(categoriesContainer, $('.categories'));1070this.extensionGalleryManifestService.getExtensionGalleryManifest()1071.then(manifest => {1072const hasCategoryFilter = manifest?.capabilities.extensionQuery.filtering?.some(({ name }) => name === FilterType.Category);1073for (const category of extension.categories) {1074const categoryElement = append(categoriesElement, $('span.category', { tabindex: '0' }, category));1075if (hasCategoryFilter) {1076categoryElement.classList.add('clickable');1077this.disposables.add(onClick(categoryElement, () => this.extensionsWorkbenchService.openSearch(`@category:"${category}"`)));1078}1079}1080});1081}1082}10831084private renderExtensionResources(container: HTMLElement, extension: IExtension): void {1085const resources: [string, URI][] = [];1086if (extension.url) {1087resources.push([localize('Marketplace', "Marketplace"), URI.parse(extension.url)]);1088}1089if (extension.supportUrl) {1090try {1091resources.push([localize('issues', "Issues"), URI.parse(extension.supportUrl)]);1092} catch (error) {/* Ignore */ }1093}1094if (extension.repository) {1095try {1096resources.push([localize('repository', "Repository"), URI.parse(extension.repository)]);1097} catch (error) {/* Ignore */ }1098}1099if (extension.licenseUrl) {1100try {1101resources.push([localize('license', "License"), URI.parse(extension.licenseUrl)]);1102} catch (error) {/* Ignore */ }1103}1104if (extension.publisherUrl) {1105resources.push([extension.publisherDisplayName, extension.publisherUrl]);1106}1107if (resources.length || extension.publisherSponsorLink) {1108const extensionResourcesContainer = append(container, $('.resources-container.additional-details-element'));1109append(extensionResourcesContainer, $('.additional-details-title', undefined, localize('resources', "Resources")));1110const resourcesElement = append(extensionResourcesContainer, $('.resources'));1111for (const [label, uri] of resources) {1112const resource = append(resourcesElement, $('a.resource', { tabindex: '0' }, label));1113this.disposables.add(onClick(resource, () => this.openerService.open(uri)));1114this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), resource, uri.toString()));1115}1116}1117}11181119private renderInstallInfo(container: HTMLElement, extension: ILocalExtension): void {1120const installInfoContainer = append(container, $('.more-info-container.additional-details-element'));1121append(installInfoContainer, $('.additional-details-title', undefined, localize('Install Info', "Installation")));1122const installInfo = append(installInfoContainer, $('.more-info'));1123append(installInfo,1124$('.more-info-entry', undefined,1125$('div.more-info-entry-name', undefined, localize('id', "Identifier")),1126$('code', undefined, extension.identifier.id)1127));1128if (extension.type !== ExtensionType.System) {1129append(installInfo,1130$('.more-info-entry', undefined,1131$('div.more-info-entry-name', undefined, localize('Version', "Version")),1132$('code', undefined, extension.manifest.version)1133)1134);1135}1136if (extension.installedTimestamp) {1137append(installInfo,1138$('.more-info-entry', undefined,1139$('div.more-info-entry-name', undefined, localize('last updated', "Last Updated")),1140$('div', undefined, toDateString(new Date(extension.installedTimestamp)))1141)1142);1143}1144if (!extension.isBuiltin && extension.source !== 'gallery') {1145const element = $('div', undefined, extension.source === 'vsix' ? localize('vsix', "VSIX") : localize('other', "Local"));1146append(installInfo,1147$('.more-info-entry', undefined,1148$('div.more-info-entry-name', undefined, localize('source', "Source")),1149element1150)1151);1152if (isNative && extension.source === 'resource' && extension.location.scheme === Schemas.file) {1153element.classList.add('link');1154element.title = extension.location.fsPath;1155this.disposables.add(onClick(element, () => this.openerService.open(extension.location, { openExternal: true })));1156}1157}1158if (extension.size) {1159const element = $('div', undefined, ByteSize.formatSize(extension.size));1160append(installInfo,1161$('.more-info-entry', undefined,1162$('div.more-info-entry-name', { title: localize('size when installed', "Size when installed") }, localize('size', "Size")),1163element1164)1165);1166if (isNative && extension.location.scheme === Schemas.file) {1167element.classList.add('link');1168element.title = extension.location.fsPath;1169this.disposables.add(onClick(element, () => this.openerService.open(extension.location, { openExternal: true })));1170}1171}1172this.getCacheLocation(extension).then(cacheLocation => {1173if (!cacheLocation) {1174return;1175}1176computeSize(cacheLocation, this.fileService).then(cacheSize => {1177if (!cacheSize) {1178return;1179}1180const element = $('div', undefined, ByteSize.formatSize(cacheSize));1181append(installInfo,1182$('.more-info-entry', undefined,1183$('div.more-info-entry-name', { title: localize('disk space used', "Cache size") }, localize('cache size', "Cache")),1184element)1185);1186if (isNative && extension.location.scheme === Schemas.file) {1187element.classList.add('link');1188element.title = cacheLocation.fsPath;1189this.disposables.add(onClick(element, () => this.openerService.open(cacheLocation.with({ scheme: Schemas.file }), { openExternal: true })));1190}1191});1192});1193}11941195private async getCacheLocation(extension: ILocalExtension): Promise<URI | undefined> {1196let extensionCacheLocation = this.uriIdentityService.extUri.joinPath(this.userDataProfilesService.defaultProfile.globalStorageHome, extension.identifier.id.toLowerCase());1197if (extension.location.scheme === Schemas.vscodeRemote) {1198const environment = await this.remoteAgentService.getEnvironment();1199if (!environment) {1200return undefined;1201}1202extensionCacheLocation = this.uriIdentityService.extUri.joinPath(environment.globalStorageHome, extension.identifier.id.toLowerCase());1203}1204return extensionCacheLocation;1205}12061207private renderMarketplaceInfo(container: HTMLElement, extension: IExtension): void {1208const gallery = extension.gallery;1209const moreInfoContainer = append(container, $('.more-info-container.additional-details-element'));1210append(moreInfoContainer, $('.additional-details-title', undefined, localize('Marketplace Info', "Marketplace")));1211const moreInfo = append(moreInfoContainer, $('.more-info'));1212if (gallery) {1213if (!extension.local) {1214append(moreInfo,1215$('.more-info-entry', undefined,1216$('div.more-info-entry-name', undefined, localize('id', "Identifier")),1217$('code', undefined, extension.identifier.id)1218));1219append(moreInfo,1220$('.more-info-entry', undefined,1221$('div.more-info-entry-name', undefined, localize('Version', "Version")),1222$('code', undefined, gallery.version)1223)1224);1225}1226append(moreInfo,1227$('.more-info-entry', undefined,1228$('div.more-info-entry-name', undefined, localize('published', "Published")),1229$('div', undefined, toDateString(new Date(gallery.releaseDate)))1230),1231$('.more-info-entry', undefined,1232$('div.more-info-entry-name', undefined, localize('last released', "Last Released")),1233$('div', undefined, toDateString(new Date(gallery.lastUpdated)))1234)1235);1236}1237}1238}12391240const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', ExtensionEditor.ID), EditorContextKeys.focus.toNegated());1241registerAction2(class ShowExtensionEditorFindAction extends Action2 {1242constructor() {1243super({1244id: 'editor.action.extensioneditor.showfind',1245title: localize('find', "Find"),1246keybinding: {1247when: contextKeyExpr,1248weight: KeybindingWeight.EditorContrib,1249primary: KeyMod.CtrlCmd | KeyCode.KeyF,1250}1251});1252}1253run(accessor: ServicesAccessor): void {1254const extensionEditor = getExtensionEditor(accessor);1255extensionEditor?.showFind();1256}1257});12581259registerAction2(class StartExtensionEditorFindNextAction extends Action2 {1260constructor() {1261super({1262id: 'editor.action.extensioneditor.findNext',1263title: localize('find next', "Find Next"),1264keybinding: {1265when: ContextKeyExpr.and(1266contextKeyExpr,1267KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED),1268primary: KeyCode.Enter,1269weight: KeybindingWeight.EditorContrib1270}1271});1272}1273run(accessor: ServicesAccessor): void {1274const extensionEditor = getExtensionEditor(accessor);1275extensionEditor?.runFindAction(false);1276}1277});12781279registerAction2(class StartExtensionEditorFindPreviousAction extends Action2 {1280constructor() {1281super({1282id: 'editor.action.extensioneditor.findPrevious',1283title: localize('find previous', "Find Previous"),1284keybinding: {1285when: ContextKeyExpr.and(1286contextKeyExpr,1287KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED),1288primary: KeyMod.Shift | KeyCode.Enter,1289weight: KeybindingWeight.EditorContrib1290}1291});1292}1293run(accessor: ServicesAccessor): void {1294const extensionEditor = getExtensionEditor(accessor);1295extensionEditor?.runFindAction(true);1296}1297});12981299registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {13001301const link = theme.getColor(textLinkForeground);1302if (link) {1303collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource { color: ${link}; }`);1304collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a { color: ${link}; }`);1305}13061307const activeLink = theme.getColor(textLinkActiveForeground);1308if (activeLink) {1309collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource:hover,1310.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource:active { color: ${activeLink}; }`);1311collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a:hover,1312.monaco-workbench .extension-editor .content .feature-contributions a:active { color: ${activeLink}; }`);1313}13141315const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground);1316if (buttonHoverBackgroundColor) {1317collector.addRule(`.monaco-workbench .extension-editor .content > .details > .additional-details-container .categories-container > .categories > .category.clickable:hover { background-color: ${buttonHoverBackgroundColor}; border-color: ${buttonHoverBackgroundColor}; }`);1318}13191320const buttonForegroundColor = theme.getColor(buttonForeground);1321if (buttonForegroundColor) {1322collector.addRule(`.monaco-workbench .extension-editor .content > .details > .additional-details-container .categories-container > .categories > .category.clickable:hover { color: ${buttonForegroundColor}; }`);1323}13241325});13261327function getExtensionEditor(accessor: ServicesAccessor): ExtensionEditor | null {1328const activeEditorPane = accessor.get(IEditorService).activeEditorPane;1329if (activeEditorPane instanceof ExtensionEditor) {1330return activeEditorPane;1331}1332return null;1333}133413351336