Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts
5262 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 } 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';90import { ThemeIcon } from '../../../../base/common/themables.js';91import { Codicon } from '../../../../base/common/codicons.js';92import { fromNow } from '../../../../base/common/date.js';9394class NavBar extends Disposable {9596private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>());97get onChange(): Event<{ id: string | null; focus: boolean }> { return this._onChange.event; }9899private _currentId: string | null = null;100get currentId(): string | null { return this._currentId; }101102private actions: Action[];103private actionbar: ActionBar;104105constructor(container: HTMLElement) {106super();107const element = append(container, $('.navbar'));108this.actions = [];109this.actionbar = this._register(new ActionBar(element));110}111112push(id: string, label: string, tooltip: string): void {113const action = new Action(id, label, undefined, true, () => this.update(id, true));114115action.tooltip = tooltip;116117this.actions.push(action);118this.actionbar.push(action);119120if (this.actions.length === 1) {121this.update(id);122}123}124125clear(): void {126this.actions = dispose(this.actions);127this.actionbar.clear();128}129130switch(id: string): boolean {131const action = this.actions.find(action => action.id === id);132if (action) {133action.run();134return true;135}136return false;137}138139private update(id: string, focus?: boolean): void {140this._currentId = id;141this._onChange.fire({ id, focus: !!focus });142this.actions.forEach(a => a.checked = a.id === id);143}144}145146interface ILayoutParticipant {147layout(): void;148}149150interface IActiveElement {151focus(): void;152}153154interface IExtensionEditorTemplate {155name: HTMLElement;156preview: HTMLElement;157builtin: HTMLElement;158description: HTMLElement;159actionsAndStatusContainer: HTMLElement;160extensionActionBar: ActionBar;161navbar: NavBar;162content: HTMLElement;163header: HTMLElement;164extension: IExtension;165gallery: IGalleryExtension | null;166manifest: IExtensionManifest | null;167}168169const enum WebviewIndex {170Readme,171Changelog172}173174const CONTEXT_SHOW_PRE_RELEASE_VERSION = new RawContextKey<boolean>('showPreReleaseVersion', false);175176abstract class ExtensionWithDifferentGalleryVersionWidget extends ExtensionWidget {177private _gallery: IGalleryExtension | null = null;178get gallery(): IGalleryExtension | null { return this._gallery; }179set gallery(gallery: IGalleryExtension | null) {180if (this.extension && gallery && !areSameExtensions(this.extension.identifier, gallery.identifier)) {181return;182}183this._gallery = gallery;184this.update();185}186}187188class VersionWidget extends ExtensionWithDifferentGalleryVersionWidget {189private readonly element: HTMLElement;190constructor(191container: HTMLElement,192hoverService: IHoverService193) {194super();195this.element = append(container, $('code.version', undefined, 'pre-release'));196this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('extension version', "Extension Version")));197this.render();198}199render(): void {200if (this.extension?.preRelease) {201show(this.element);202} else {203hide(this.element);204}205}206}207208export class ExtensionEditor extends EditorPane {209210static readonly ID: string = 'workbench.editor.extension';211212private readonly _scopedContextKeyService = this._register(new MutableDisposable<IScopedContextKeyService>());213private template: IExtensionEditorTemplate | undefined;214215private extensionReadme: Cache<string> | null;216private extensionChangelog: Cache<string> | null;217private extensionManifest: Cache<IExtensionManifest | null> | null;218219// Some action bar items use a webview whose vertical scroll position we track in this map220private initialScrollProgress: Map<WebviewIndex, number> = new Map();221222// Spot when an ExtensionEditor instance gets reused for a different extension, in which case the vertical scroll positions must be zeroed223private currentIdentifier: string = '';224225private layoutParticipants: ILayoutParticipant[] = [];226private readonly contentDisposables = this._register(new DisposableStore());227private readonly transientDisposables = this._register(new DisposableStore());228private activeElement: IActiveElement | null = null;229private dimension: Dimension | undefined;230231private showPreReleaseVersionContextKey: IContextKey<boolean> | undefined;232233constructor(234group: IEditorGroup,235@ITelemetryService telemetryService: ITelemetryService,236@IInstantiationService private readonly instantiationService: IInstantiationService,237@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,238@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,239@IThemeService themeService: IThemeService,240@INotificationService private readonly notificationService: INotificationService,241@IOpenerService private readonly openerService: IOpenerService,242@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,243@IStorageService storageService: IStorageService,244@IExtensionService private readonly extensionService: IExtensionService,245@IWebviewService private readonly webviewService: IWebviewService,246@ILanguageService private readonly languageService: ILanguageService,247@IContextMenuService private readonly contextMenuService: IContextMenuService,248@IContextKeyService private readonly contextKeyService: IContextKeyService,249@IHoverService private readonly hoverService: IHoverService,250) {251super(ExtensionEditor.ID, group, telemetryService, themeService, storageService);252this.extensionReadme = null;253this.extensionChangelog = null;254this.extensionManifest = null;255}256257override get scopedContextKeyService(): IContextKeyService | undefined {258return this._scopedContextKeyService.value;259}260261protected createEditor(parent: HTMLElement): void {262const root = append(parent, $('.extension-editor'));263this._scopedContextKeyService.value = this.contextKeyService.createScoped(root);264this._scopedContextKeyService.value.createKey('inExtensionEditor', true);265this.showPreReleaseVersionContextKey = CONTEXT_SHOW_PRE_RELEASE_VERSION.bindTo(this._scopedContextKeyService.value);266267root.tabIndex = 0; // this is required for the focus tracker on the editor268root.style.outline = 'none';269root.setAttribute('role', 'document');270const header = append(root, $('.header'));271272const iconContainer = append(header, $('.icon-container'));273const iconWidget = this.instantiationService.createInstance(ExtensionIconWidget, iconContainer);274const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, iconContainer, true);275276const details = append(header, $('.details'));277const title = append(details, $('.title'));278const name = append(title, $('span.name.clickable', { role: 'heading', tabIndex: 0 }));279this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name")));280const versionWidget = new VersionWidget(title, this.hoverService);281282const preview = append(title, $('span.preview'));283this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), preview, localize('preview', "Preview")));284preview.textContent = localize('preview', "Preview");285286const builtin = append(title, $('span.builtin'));287builtin.textContent = localize('builtin', "Built-in");288289const subtitle = append(details, $('.subtitle'));290const subTitleEntryContainers: HTMLElement[] = [];291292const publisherContainer = append(subtitle, $('.subtitle-entry'));293subTitleEntryContainers.push(publisherContainer);294const publisherWidget = this.instantiationService.createInstance(PublisherWidget, publisherContainer, false);295296const extensionKindContainer = append(subtitle, $('.subtitle-entry'));297subTitleEntryContainers.push(extensionKindContainer);298const extensionKindWidget = this.instantiationService.createInstance(ExtensionKindIndicatorWidget, extensionKindContainer, false);299300const installCountContainer = append(subtitle, $('.subtitle-entry'));301subTitleEntryContainers.push(installCountContainer);302const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCountContainer, false);303304const ratingsContainer = append(subtitle, $('.subtitle-entry'));305subTitleEntryContainers.push(ratingsContainer);306const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, ratingsContainer, false);307308const sponsorContainer = append(subtitle, $('.subtitle-entry'));309subTitleEntryContainers.push(sponsorContainer);310const sponsorWidget = this.instantiationService.createInstance(SponsorWidget, sponsorContainer);311312const widgets: ExtensionWidget[] = [313iconWidget,314remoteBadge,315versionWidget,316publisherWidget,317extensionKindWidget,318installCountWidget,319ratingsWidget,320sponsorWidget,321];322323const description = append(details, $('.description'));324325const installAction = this.instantiationService.createInstance(InstallDropdownAction);326const actions = [327this.instantiationService.createInstance(ExtensionRuntimeStateAction),328this.instantiationService.createInstance(ExtensionStatusLabelAction),329this.instantiationService.createInstance(UpdateAction, true),330this.instantiationService.createInstance(SetColorThemeAction),331this.instantiationService.createInstance(SetFileIconThemeAction),332this.instantiationService.createInstance(SetProductIconThemeAction),333this.instantiationService.createInstance(SetLanguageAction),334this.instantiationService.createInstance(ClearLanguageAction),335336this.instantiationService.createInstance(EnableDropDownAction),337this.instantiationService.createInstance(TogglePreReleaseExtensionAction),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(ToggleAutoUpdateForExtensionAction),352new ExtensionEditorManageExtensionAction(this.scopedContextKeyService || this.contextKeyService, this.instantiationService),353];354355const actionsAndStatusContainer = append(details, $('.actions-status-container'));356const extensionActionBar = this._register(new ActionBar(actionsAndStatusContainer, {357actionViewItemProvider: (action: IAction, options) => {358if (action instanceof DropDownExtensionAction) {359return action.createActionViewItem(options);360}361if (action instanceof ButtonWithDropDownExtensionAction) {362return new ButtonWithDropdownExtensionActionViewItem(363action,364{365...options,366icon: true,367label: true,368menuActionsOrProvider: { getActions: () => action.menuActions },369menuActionClassNames: action.menuActionClassNames370},371this.contextMenuService);372}373if (action instanceof ToggleAutoUpdateForExtensionAction) {374return new CheckboxActionViewItem(undefined, action, { ...options, icon: true, label: true, checkboxStyles: defaultCheckboxStyles });375}376return undefined;377},378focusOnlyEnabledItems: true379}));380381extensionActionBar.push(actions, { icon: true, label: true });382extensionActionBar.setFocusable(true);383// update focusable elements when the enablement of an action changes384this._register(Event.any(...actions.map(a => Event.filter(a.onDidChange, e => e.enabled !== undefined)))(() => {385extensionActionBar.setFocusable(false);386extensionActionBar.setFocusable(true);387}));388389const otherExtensionContainers: IExtensionContainer[] = [];390const extensionStatusAction = this.instantiationService.createInstance(ExtensionStatusAction);391const extensionStatusWidget = this._register(this.instantiationService.createInstance(ExtensionStatusWidget, append(actionsAndStatusContainer, $('.status')), extensionStatusAction));392393otherExtensionContainers.push(extensionStatusAction, new class extends ExtensionWidget {394render() {395actionsAndStatusContainer.classList.toggle('list-layout', this.extension?.state === ExtensionState.Installed);396}397}());398399const recommendationWidget = this.instantiationService.createInstance(ExtensionRecommendationWidget, append(details, $('.recommendation')));400widgets.push(recommendationWidget);401402this._register(Event.any(extensionStatusWidget.onDidRender, recommendationWidget.onDidRender)(() => {403if (this.dimension) {404this.layout(this.dimension);405}406}));407408const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, ...otherExtensionContainers]);409for (const disposable of [...actions, ...widgets, ...otherExtensionContainers, extensionContainers]) {410this._register(disposable);411}412413const onError = Event.chain(extensionActionBar.onDidRun, $ =>414$.map(({ error }) => error)415.filter(error => !!error)416);417418this._register(onError(this.onError, this));419420const body = append(root, $('.body'));421const navbar = this._register(new NavBar(body));422423const content = append(body, $('.content'));424content.id = generateUuid(); // An id is needed for the webview parent flow to425426this.template = {427builtin,428content,429description,430header,431name,432navbar,433preview,434actionsAndStatusContainer,435extensionActionBar,436set extension(extension: IExtension) {437extensionContainers.extension = extension;438let lastNonEmptySubtitleEntryContainer;439for (const subTitleEntryElement of subTitleEntryContainers) {440subTitleEntryElement.classList.remove('last-non-empty');441if (subTitleEntryElement.children.length > 0) {442lastNonEmptySubtitleEntryContainer = subTitleEntryElement;443}444}445if (lastNonEmptySubtitleEntryContainer) {446lastNonEmptySubtitleEntryContainer.classList.add('last-non-empty');447}448},449set gallery(gallery: IGalleryExtension | null) {450versionWidget.gallery = gallery;451},452set manifest(manifest: IExtensionManifest | null) {453installAction.manifest = manifest;454}455};456}457458override async setInput(input: ExtensionsInput, options: IExtensionEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {459await super.setInput(input, options, context, token);460this.updatePreReleaseVersionContext();461if (this.template) {462await this.render(input.extension, this.template, !!options?.preserveFocus);463}464}465466override setOptions(options: IExtensionEditorOptions | undefined): void {467const currentOptions: IExtensionEditorOptions | undefined = this.options;468super.setOptions(options);469this.updatePreReleaseVersionContext();470471if (this.input && this.template && currentOptions?.showPreReleaseVersion !== options?.showPreReleaseVersion) {472this.render((this.input as ExtensionsInput).extension, this.template, !!options?.preserveFocus);473return;474}475476if (options?.tab) {477this.template?.navbar.switch(options.tab);478}479480}481482private updatePreReleaseVersionContext(): void {483let showPreReleaseVersion = (<IExtensionEditorOptions | undefined>this.options)?.showPreReleaseVersion;484if (isUndefined(showPreReleaseVersion)) {485showPreReleaseVersion = !!(<ExtensionsInput>this.input).extension.gallery?.properties.isPreReleaseVersion;486}487this.showPreReleaseVersionContextKey?.set(showPreReleaseVersion);488}489490async openTab(tab: ExtensionEditorTab): Promise<void> {491if (!this.input || !this.template) {492return;493}494if (this.template.navbar.switch(tab)) {495return;496}497// Fallback to Readme tab if ExtensionPack tab does not exist498if (tab === ExtensionEditorTab.ExtensionPack) {499this.template.navbar.switch(ExtensionEditorTab.Readme);500}501}502503private async getGalleryVersionToShow(extension: IExtension, preRelease?: boolean): Promise<IGalleryExtension | null> {504if (extension.resourceExtension) {505return null;506}507if (extension.local?.source === 'resource') {508return null;509}510if (isUndefined(preRelease)) {511return null;512}513if (preRelease === extension.gallery?.properties.isPreReleaseVersion) {514return null;515}516if (preRelease && !extension.hasPreReleaseVersion) {517return null;518}519if (!preRelease && !extension.hasReleaseVersion) {520return null;521}522return (await this.extensionGalleryService.getExtensions([{ ...extension.identifier, preRelease, hasPreRelease: extension.hasPreReleaseVersion }], CancellationToken.None))[0] || null;523}524525private async render(extension: IExtension, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {526this.activeElement = null;527this.transientDisposables.clear();528529const token = this.transientDisposables.add(new CancellationTokenSource()).token;530531const gallery = await this.getGalleryVersionToShow(extension, (this.options as IExtensionEditorOptions)?.showPreReleaseVersion);532if (token.isCancellationRequested) {533return;534}535536this.extensionReadme = new Cache(() => gallery ? this.extensionGalleryService.getReadme(gallery, token) : extension.getReadme(token));537this.extensionChangelog = new Cache(() => gallery ? this.extensionGalleryService.getChangelog(gallery, token) : extension.getChangelog(token));538this.extensionManifest = new Cache(() => gallery ? this.extensionGalleryService.getManifest(gallery, token) : extension.getManifest(token));539540template.extension = extension;541template.gallery = gallery;542template.manifest = null;543544template.name.textContent = extension.displayName;545template.name.classList.toggle('clickable', !!extension.url);546template.name.classList.toggle('deprecated', !!extension.deprecationInfo);547template.preview.style.display = extension.preview ? 'inherit' : 'none';548template.builtin.style.display = extension.isBuiltin ? 'inherit' : 'none';549550template.description.textContent = extension.description;551552if (extension.url) {553this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(extension.url!))));554}555556const manifest = await this.extensionManifest.get().promise;557if (token.isCancellationRequested) {558return;559}560561if (manifest) {562template.manifest = manifest;563}564565this.renderNavbar(extension, manifest, template, preserveFocus);566567// report telemetry568const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();569let recommendationsData = {};570if (extRecommendations[extension.identifier.id.toLowerCase()]) {571recommendationsData = { recommendationReason: extRecommendations[extension.identifier.id.toLowerCase()].reasonId };572}573/* __GDPR__574"extensionGallery:openExtension" : {575"owner": "sandy081",576"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },577"${include}": [578"${GalleryExtensionTelemetryData}"579]580}581*/582this.telemetryService.publicLog('extensionGallery:openExtension', { ...extension.telemetryData, ...recommendationsData });583584}585586private renderNavbar(extension: IExtension, manifest: IExtensionManifest | null, template: IExtensionEditorTemplate, preserveFocus: boolean): void {587template.content.innerText = '';588template.navbar.clear();589590if (this.currentIdentifier !== extension.identifier.id) {591this.initialScrollProgress.clear();592this.currentIdentifier = extension.identifier.id;593}594595template.navbar.push(ExtensionEditorTab.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file"));596if (manifest) {597template.navbar.push(ExtensionEditorTab.Features, localize('features', "Features"), localize('featurestooltip', "Lists features contributed by this extension"));598}599if (extension.hasChangelog()) {600template.navbar.push(ExtensionEditorTab.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file"));601}602if (extension.dependencies.length) {603template.navbar.push(ExtensionEditorTab.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on"));604}605if (manifest && manifest.extensionPack?.length && !this.shallRenderAsExtensionPack(manifest)) {606template.navbar.push(ExtensionEditorTab.ExtensionPack, localize('extensionpack', "Extension Pack"), localize('extensionpacktooltip', "Lists extensions those will be installed together with this extension"));607}608609if ((<IExtensionEditorOptions | undefined>this.options)?.tab) {610template.navbar.switch((<IExtensionEditorOptions>this.options).tab!);611}612if (template.navbar.currentId) {613this.onNavbarChange(extension, { id: template.navbar.currentId, focus: !preserveFocus }, template);614}615template.navbar.onChange(e => this.onNavbarChange(extension, e, template), this, this.transientDisposables);616}617618override clearInput(): void {619this.contentDisposables.clear();620this.transientDisposables.clear();621622super.clearInput();623}624625override focus(): void {626super.focus();627this.activeElement?.focus();628}629630showFind(): void {631this.activeWebview?.showFind();632}633634runFindAction(previous: boolean): void {635this.activeWebview?.runFindAction(previous);636}637638public get activeWebview(): IWebview | undefined {639if (!this.activeElement || !(this.activeElement as IWebview).runFindAction) {640return undefined;641}642return this.activeElement as IWebview;643}644645private onNavbarChange(extension: IExtension, { id, focus }: { id: string | null; focus: boolean }, template: IExtensionEditorTemplate): void {646this.contentDisposables.clear();647template.content.innerText = '';648this.activeElement = null;649if (id) {650const cts = new CancellationTokenSource();651this.contentDisposables.add(toDisposable(() => cts.dispose(true)));652this.open(id, extension, template, cts.token)653.then(activeElement => {654if (cts.token.isCancellationRequested) {655return;656}657this.activeElement = activeElement;658if (focus) {659this.focus();660}661});662}663}664665private open(id: string, extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {666// Setup common container structure for all tabs667const details = append(template.content, $('.details'));668const contentContainer = append(details, $('.content-container'));669const additionalDetailsContainer = append(details, $('.additional-details-container'));670671const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500);672layout();673this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));674675// Render additional details synchronously to avoid flicker676this.renderAdditionalDetails(additionalDetailsContainer, extension);677678switch (id) {679case ExtensionEditorTab.Readme: return this.openDetails(extension, contentContainer, token);680case ExtensionEditorTab.Features: return this.openFeatures(extension, contentContainer, token);681case ExtensionEditorTab.Changelog: return this.openChangelog(extension, contentContainer, token);682case ExtensionEditorTab.Dependencies: return this.openExtensionDependencies(extension, contentContainer, token);683case ExtensionEditorTab.ExtensionPack: return this.openExtensionPack(extension, contentContainer, token);684}685return Promise.resolve(null);686}687688private async openMarkdown(extension: IExtension, cacheResult: CacheResult<string>, noContentCopy: string, container: HTMLElement, webviewIndex: WebviewIndex, title: string, token: CancellationToken): Promise<IActiveElement | null> {689try {690const body = await this.renderMarkdown(extension, cacheResult, container, token);691if (token.isCancellationRequested) {692return Promise.resolve(null);693}694695const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay({696title,697options: {698enableFindWidget: true,699tryRestoreScrollPosition: true,700disableServiceWorker: true,701},702contentOptions: {},703extension: undefined,704}));705706webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0;707708webview.claim(this, this.window, this.scopedContextKeyService);709setParentFlowTo(webview.container, container);710webview.layoutWebviewOverElement(container);711712webview.setHtml(body);713webview.claim(this, this.window, undefined);714715this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire()));716717this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress)));718719const removeLayoutParticipant = arrays.insert(this.layoutParticipants, {720layout: () => {721webview.layoutWebviewOverElement(container);722}723});724this.contentDisposables.add(toDisposable(removeLayoutParticipant));725726let isDisposed = false;727this.contentDisposables.add(toDisposable(() => { isDisposed = true; }));728729this.contentDisposables.add(this.themeService.onDidColorThemeChange(async () => {730// Render again since syntax highlighting of code blocks may have changed731const body = await this.renderMarkdown(extension, cacheResult, container);732if (!isDisposed) { // Make sure we weren't disposed of in the meantime733webview.setHtml(body);734}735}));736737this.contentDisposables.add(webview.onDidClickLink(link => {738if (!link) {739return;740}741// Only allow links with specific schemes742if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto)) {743this.openerService.open(link);744} else if (matchesScheme(link, Schemas.command) && extension.type === ExtensionType.System) {745this.openerService.open(link, {746allowCommands: [747ShowCurrentReleaseNotesActionId748]749});750}751}));752753return webview;754} catch (e) {755const p = append(container, $('p.nocontent'));756p.textContent = noContentCopy;757return p;758}759}760761private async renderMarkdown(extension: IExtension, cacheResult: CacheResult<string>, container: HTMLElement, token?: CancellationToken): Promise<string> {762const contents = await this.loadContents(() => cacheResult, container);763if (token?.isCancellationRequested) {764return '';765}766767const allowedLinkProtocols = [Schemas.http, Schemas.https, Schemas.mailto];768const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, {769sanitizerConfig: {770allowedLinkProtocols: {771override: extension.type === ExtensionType.System772? [...allowedLinkProtocols, Schemas.command]773: allowedLinkProtocols774}775}776}, token);777if (token?.isCancellationRequested) {778return '';779}780781return this.renderBody(content);782}783784private renderBody(body: TrustedHTML): string {785const nonce = generateUuid();786const colorMap = TokenizationRegistry.getColorMap();787const css = colorMap ? generateTokensCSSForColorMap(colorMap) : '';788return `<!DOCTYPE html>789<html>790<head>791<meta http-equiv="Content-type" content="text/html;charset=UTF-8">792<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src 'nonce-${nonce}';">793<style nonce="${nonce}">794${DEFAULT_MARKDOWN_STYLES}795796/* prevent scroll-to-top button from blocking the body text */797body {798padding-bottom: 75px;799}800801#scroll-to-top {802position: fixed;803width: 32px;804height: 32px;805right: 25px;806bottom: 25px;807background-color: var(--vscode-button-secondaryBackground);808border-color: var(--vscode-button-border);809border-radius: 50%;810cursor: pointer;811box-shadow: 1px 1px 1px rgba(0,0,0,.25);812outline: none;813display: flex;814justify-content: center;815align-items: center;816}817818#scroll-to-top:hover {819background-color: var(--vscode-button-secondaryHoverBackground);820box-shadow: 2px 2px 2px rgba(0,0,0,.25);821}822823body.vscode-high-contrast #scroll-to-top {824border-width: 2px;825border-style: solid;826box-shadow: none;827}828829#scroll-to-top span.icon::before {830content: "";831display: block;832background: var(--vscode-button-secondaryForeground);833/* Chevron up icon */834webkit-mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=');835-webkit-mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=');836width: 16px;837height: 16px;838}839${css}840</style>841</head>842<body>843<a id="scroll-to-top" role="button" aria-label="scroll to top" href="#"><span class="icon"></span></a>844${body}845</body>846</html>`;847}848849private async openDetails(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {850let activeElement: IActiveElement | null = null;851const manifest = await this.extensionManifest!.get().promise;852if (manifest && manifest.extensionPack?.length && this.shallRenderAsExtensionPack(manifest)) {853activeElement = await this.openExtensionPackReadme(extension, manifest, contentContainer, token);854} else {855activeElement = await this.openMarkdown(extension, this.extensionReadme!.get(), localize('noReadme', "No README available."), contentContainer, WebviewIndex.Readme, localize('Readme title', "Readme"), token);856}857858return activeElement;859}860861private shallRenderAsExtensionPack(manifest: IExtensionManifest): boolean {862return !!(manifest.categories?.some(category => category.toLowerCase() === 'extension packs'));863}864865private async openExtensionPackReadme(extension: IExtension, manifest: IExtensionManifest, container: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {866if (token.isCancellationRequested) {867return Promise.resolve(null);868}869870const extensionPackReadme = append(container, $('div', { class: 'extension-pack-readme' }));871extensionPackReadme.style.margin = '0 auto';872extensionPackReadme.style.maxWidth = '882px';873874const extensionPack = append(extensionPackReadme, $('div', { class: 'extension-pack' }));875876const packCount = manifest.extensionPack!.length;877const headerHeight = 37; // navbar height878const contentMinHeight = 200; // minimum height for readme content879880const layout = () => {881extensionPackReadme.classList.remove('one-row', 'two-rows', 'three-rows', 'more-rows');882const availableHeight = container.clientHeight;883const availableForPack = Math.max(availableHeight - headerHeight - contentMinHeight, 0);884let rowClass = 'one-row';885if (availableForPack >= 302 && packCount > 6) {886rowClass = 'more-rows';887} else if (availableForPack >= 282 && packCount > 4) {888rowClass = 'three-rows';889} else if (availableForPack >= 200 && packCount > 2) {890rowClass = 'two-rows';891} else {892rowClass = 'one-row';893}894extensionPackReadme.classList.add(rowClass);895};896897layout();898this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));899900const extensionPackHeader = append(extensionPack, $('div.header'));901extensionPackHeader.textContent = localize('extension pack', "Extension Pack ({0})", manifest.extensionPack!.length);902const extensionPackContent = append(extensionPack, $('div', { class: 'extension-pack-content' }));903extensionPackContent.setAttribute('tabindex', '0');904const readmeContent = append(extensionPackReadme, $('div.readme-content'));905906await Promise.all([907this.renderExtensionPack(manifest, extensionPackContent, token),908this.openMarkdown(extension, this.extensionReadme!.get(), localize('noReadme', "No README available."), readmeContent, WebviewIndex.Readme, localize('Readme title', "Readme"), token),909]);910911return { focus: () => extensionPackContent.focus() };912}913914private renderAdditionalDetails(container: HTMLElement, extension: IExtension): void {915const content = $('div', { class: 'additional-details-content', tabindex: '0' });916const scrollableContent = new DomScrollableElement(content, {});917const layout = () => scrollableContent.scanDomNode();918const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });919this.contentDisposables.add(toDisposable(removeLayoutParticipant));920this.contentDisposables.add(scrollableContent);921922this.contentDisposables.add(this.instantiationService.createInstance(AdditionalDetailsWidget, content, extension));923924append(container, scrollableContent.getDomNode());925scrollableContent.scanDomNode();926}927928private async openChangelog(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {929const activeElement = await this.openMarkdown(extension, this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), contentContainer, WebviewIndex.Changelog, localize('Changelog title', "Changelog"), token);930931return activeElement;932}933934private async openFeatures(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {935const manifest = await this.loadContents(() => this.extensionManifest!.get(), contentContainer);936if (token.isCancellationRequested) {937return null;938}939if (!manifest) {940return null;941}942943const extensionFeaturesTab = this.contentDisposables.add(this.instantiationService.createInstance(ExtensionFeaturesTab, manifest, (<IExtensionEditorOptions | undefined>this.options)?.feature));944const featureLayout = () => extensionFeaturesTab.layout(contentContainer.clientHeight, contentContainer.clientWidth);945const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout: featureLayout });946this.contentDisposables.add(toDisposable(removeLayoutParticipant));947append(contentContainer, extensionFeaturesTab.domNode);948featureLayout();949950return extensionFeaturesTab.domNode;951}952953private openExtensionDependencies(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {954if (token.isCancellationRequested) {955return Promise.resolve(null);956}957958if (arrays.isFalsyOrEmpty(extension.dependencies)) {959append(contentContainer, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies");960return Promise.resolve(contentContainer);961}962963const content = $('div', { class: 'subcontent' });964const scrollableContent = new DomScrollableElement(content, {});965append(contentContainer, scrollableContent.getDomNode());966this.contentDisposables.add(scrollableContent);967968const dependenciesTree = this.instantiationService.createInstance(ExtensionsTree,969new ExtensionData(extension, null, extension => extension.dependencies || [], this.extensionsWorkbenchService), content,970{971listBackground: editorBackground972});973const depLayout = () => {974scrollableContent.scanDomNode();975const scrollDimensions = scrollableContent.getScrollDimensions();976dependenciesTree.layout(scrollDimensions.height);977};978const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout: depLayout });979this.contentDisposables.add(toDisposable(removeLayoutParticipant));980981this.contentDisposables.add(dependenciesTree);982scrollableContent.scanDomNode();983984return Promise.resolve({ focus() { dependenciesTree.domFocus(); } });985}986987private async openExtensionPack(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {988if (token.isCancellationRequested) {989return Promise.resolve(null);990}991992const manifest = await this.loadContents(() => this.extensionManifest!.get(), contentContainer);993if (token.isCancellationRequested) {994return null;995}996if (!manifest) {997return null;998}9991000return this.renderExtensionPack(manifest, contentContainer, token);1001}10021003private async renderExtensionPack(manifest: IExtensionManifest, parent: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {1004if (token.isCancellationRequested) {1005return null;1006}10071008const content = $('div', { class: 'subcontent' });1009const scrollableContent = new DomScrollableElement(content, { useShadows: false });1010append(parent, scrollableContent.getDomNode());10111012const extensionsGridView = this.instantiationService.createInstance(ExtensionsGridView, content, new Delegate());1013const extensions: IExtension[] = await getExtensions(manifest.extensionPack!, this.extensionsWorkbenchService);1014extensionsGridView.setExtensions(extensions);1015scrollableContent.scanDomNode();10161017this.contentDisposables.add(scrollableContent);1018this.contentDisposables.add(extensionsGridView);1019this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout: () => scrollableContent.scanDomNode() })));10201021return content;1022}10231024private loadContents<T>(loadingTask: () => CacheResult<T>, container: HTMLElement): Promise<T> {1025container.classList.add('loading');10261027const result = this.contentDisposables.add(loadingTask());1028const onDone = () => container.classList.remove('loading');1029result.promise.then(onDone, onDone);10301031return result.promise;1032}10331034layout(dimension: Dimension): void {1035this.dimension = dimension;1036this.layoutParticipants.forEach(p => p.layout());1037}10381039private onError(err: any): void {1040if (isCancellationError(err)) {1041return;1042}10431044this.notificationService.error(err);1045}1046}10471048class AdditionalDetailsWidget extends Disposable {10491050private readonly disposables = this._register(new DisposableStore());10511052constructor(1053private readonly container: HTMLElement,1054extension: IExtension,1055@IHoverService private readonly hoverService: IHoverService,1056@IOpenerService private readonly openerService: IOpenerService,1057@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,1058@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,1059@IFileService private readonly fileService: IFileService,1060@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,1061@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,1062@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService,1063) {1064super();1065this.render(extension);1066this._register(this.extensionsWorkbenchService.onChange(e => {1067if (e && areSameExtensions(e.identifier, extension.identifier) && e.server === extension.server) {1068this.render(e);1069}1070}));1071}10721073private render(extension: IExtension): void {1074this.container.innerText = '';1075this.disposables.clear();10761077if (extension.local) {1078this.renderInstallInfo(this.container, extension.local);1079}1080if (extension.gallery) {1081this.renderMarketplaceInfo(this.container, extension);1082}1083this.renderCategories(this.container, extension);1084this.renderExtensionResources(this.container, extension);1085}10861087private renderCategories(container: HTMLElement, extension: IExtension): void {1088if (extension.categories.length) {1089const categoriesContainer = append(container, $('.categories-container.additional-details-element'));1090append(categoriesContainer, $('.additional-details-title', undefined, localize('categories', "Categories")));1091const categoriesElement = append(categoriesContainer, $('.categories'));1092this.extensionGalleryManifestService.getExtensionGalleryManifest()1093.then(manifest => {1094const hasCategoryFilter = manifest?.capabilities.extensionQuery.filtering?.some(({ name }) => name === FilterType.Category);1095for (const category of extension.categories) {1096const categoryElement = append(categoriesElement, $('span.category', { tabindex: '0' }, category));1097if (hasCategoryFilter) {1098categoryElement.classList.add('clickable');1099this.disposables.add(onClick(categoryElement, () => this.extensionsWorkbenchService.openSearch(`@category:"${category}"`)));1100}1101}1102});1103}1104}11051106private renderExtensionResources(container: HTMLElement, extension: IExtension): void {1107const resources: [string, ThemeIcon, URI][] = [];1108if (extension.repository) {1109try {1110resources.push([localize('repository', "Repository"), ThemeIcon.fromId(Codicon.repo.id), URI.parse(extension.repository)]);1111} catch (error) {/* Ignore */ }1112}1113if (extension.supportUrl) {1114try {1115resources.push([localize('issues', "Issues"), ThemeIcon.fromId(Codicon.issues.id), URI.parse(extension.supportUrl)]);1116} catch (error) {/* Ignore */ }1117}1118if (extension.licenseUrl) {1119try {1120resources.push([localize('license', "License"), ThemeIcon.fromId(Codicon.linkExternal.id), URI.parse(extension.licenseUrl)]);1121} catch (error) {/* Ignore */ }1122}1123if (extension.publisherUrl) {1124resources.push([extension.publisherDisplayName, ThemeIcon.fromId(Codicon.linkExternal.id), extension.publisherUrl]);1125}1126if (extension.url) {1127resources.push([localize('Marketplace', "Marketplace"), ThemeIcon.fromId(Codicon.linkExternal.id), URI.parse(extension.url)]);1128}1129if (resources.length || extension.publisherSponsorLink) {1130const extensionResourcesContainer = append(container, $('.resources-container.additional-details-element'));1131append(extensionResourcesContainer, $('.additional-details-title', undefined, localize('resources', "Resources")));1132const resourcesElement = append(extensionResourcesContainer, $('.resources'));1133for (const [label, icon, uri] of resources) {1134const resourceElement = append(resourcesElement, $('.resource'));1135append(resourceElement, $(ThemeIcon.asCSSSelector(icon)));1136append(resourceElement, $('a', { tabindex: '0' }, label));1137this.disposables.add(onClick(resourceElement, () => this.openerService.open(uri)));1138this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), resourceElement, uri.toString()));1139}1140}1141}11421143private renderInstallInfo(container: HTMLElement, extension: ILocalExtension): void {1144const installInfoContainer = append(container, $('.more-info-container.additional-details-element'));1145append(installInfoContainer, $('.additional-details-title', undefined, localize('Install Info', "Installation")));1146const installInfo = append(installInfoContainer, $('.more-info'));1147append(installInfo,1148$('.more-info-entry', undefined,1149$('div.more-info-entry-name', undefined, localize('id', "Identifier")),1150$('code', undefined, extension.identifier.id)1151));1152if (extension.type !== ExtensionType.System) {1153append(installInfo,1154$('.more-info-entry', undefined,1155$('div.more-info-entry-name', undefined, localize('Version', "Version")),1156$('code', undefined, extension.manifest.version)1157)1158);1159}1160if (extension.installedTimestamp) {1161append(installInfo,1162$('.more-info-entry', undefined,1163$('div.more-info-entry-name', undefined, localize('last updated', "Last Updated")),1164$('div', {1165'title': new Date(extension.installedTimestamp).toString()1166}, fromNow(extension.installedTimestamp, true, true, true))1167)1168);1169}1170if (!extension.isBuiltin && extension.source !== 'gallery') {1171const element = $('div', undefined, extension.source === 'vsix' ? localize('vsix', "VSIX") : localize('other', "Local"));1172append(installInfo,1173$('.more-info-entry', undefined,1174$('div.more-info-entry-name', undefined, localize('source', "Source")),1175element1176)1177);1178if (isNative && extension.source === 'resource' && extension.location.scheme === Schemas.file) {1179element.classList.add('link');1180element.tabIndex = 0;1181element.setAttribute('role', 'link');1182element.title = extension.location.fsPath;1183this.disposables.add(onClick(element, () => this.openerService.open(extension.location, { openExternal: true })));1184}1185}1186if (extension.size) {1187const element = $('div', undefined, ByteSize.formatSize(extension.size));1188append(installInfo,1189$('.more-info-entry', undefined,1190$('div.more-info-entry-name', { title: localize('size when installed', "Size when installed") }, localize('size', "Size")),1191element1192)1193);1194if (isNative && extension.location.scheme === Schemas.file) {1195element.classList.add('link');1196element.tabIndex = 0;1197element.setAttribute('role', 'link');1198element.title = extension.location.fsPath;1199this.disposables.add(onClick(element, () => this.openerService.open(extension.location, { openExternal: true })));1200}1201}1202this.getCacheLocation(extension).then(cacheLocation => {1203if (!cacheLocation) {1204return;1205}1206computeSize(cacheLocation, this.fileService).then(cacheSize => {1207if (!cacheSize) {1208return;1209}1210const element = $('div', undefined, ByteSize.formatSize(cacheSize));1211append(installInfo,1212$('.more-info-entry', undefined,1213$('div.more-info-entry-name', { title: localize('disk space used', "Cache size") }, localize('cache size', "Cache")),1214element)1215);1216if (isNative && extension.location.scheme === Schemas.file) {1217element.classList.add('link');1218element.tabIndex = 0;1219element.setAttribute('role', 'link');1220element.title = cacheLocation.fsPath;1221this.disposables.add(onClick(element, () => this.openerService.open(cacheLocation.with({ scheme: Schemas.file }), { openExternal: true })));1222}1223});1224});1225}12261227private async getCacheLocation(extension: ILocalExtension): Promise<URI | undefined> {1228let extensionCacheLocation = this.uriIdentityService.extUri.joinPath(this.userDataProfilesService.defaultProfile.globalStorageHome, extension.identifier.id.toLowerCase());1229if (extension.location.scheme === Schemas.vscodeRemote) {1230const environment = await this.remoteAgentService.getEnvironment();1231if (!environment) {1232return undefined;1233}1234extensionCacheLocation = this.uriIdentityService.extUri.joinPath(environment.globalStorageHome, extension.identifier.id.toLowerCase());1235}1236return extensionCacheLocation;1237}12381239private renderMarketplaceInfo(container: HTMLElement, extension: IExtension): void {1240const gallery = extension.gallery;1241const moreInfoContainer = append(container, $('.more-info-container.additional-details-element'));1242append(moreInfoContainer, $('.additional-details-title', undefined, localize('Marketplace Info', "Marketplace")));1243const moreInfo = append(moreInfoContainer, $('.more-info'));1244if (gallery) {1245if (!extension.local) {1246append(moreInfo,1247$('.more-info-entry', undefined,1248$('div.more-info-entry-name', undefined, localize('id', "Identifier")),1249$('code', undefined, extension.identifier.id)1250));1251append(moreInfo,1252$('.more-info-entry', undefined,1253$('div.more-info-entry-name', undefined, localize('Version', "Version")),1254$('code', undefined, gallery.version)1255)1256);1257}1258append(moreInfo,1259$('.more-info-entry', undefined,1260$('div.more-info-entry-name', undefined, localize('published', "Published")),1261$('div', {1262'title': new Date(gallery.releaseDate).toString()1263}, fromNow(gallery.releaseDate, true, true, true))1264),1265$('.more-info-entry', undefined,1266$('div.more-info-entry-name', undefined, localize('last released', "Last Released")),1267$('div', {1268'title': new Date(gallery.lastUpdated).toString()1269}, fromNow(gallery.lastUpdated, true, true, true))1270)1271);1272}1273}1274}12751276const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', ExtensionEditor.ID), EditorContextKeys.focus.toNegated());1277registerAction2(class ShowExtensionEditorFindAction extends Action2 {1278constructor() {1279super({1280id: 'editor.action.extensioneditor.showfind',1281title: localize('find', "Find"),1282keybinding: {1283when: contextKeyExpr,1284weight: KeybindingWeight.EditorContrib,1285primary: KeyMod.CtrlCmd | KeyCode.KeyF,1286}1287});1288}1289run(accessor: ServicesAccessor): void {1290const extensionEditor = getExtensionEditor(accessor);1291extensionEditor?.showFind();1292}1293});12941295registerAction2(class StartExtensionEditorFindNextAction extends Action2 {1296constructor() {1297super({1298id: 'editor.action.extensioneditor.findNext',1299title: localize('find next', "Find Next"),1300keybinding: {1301when: ContextKeyExpr.and(1302contextKeyExpr,1303KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED),1304primary: KeyCode.Enter,1305weight: KeybindingWeight.EditorContrib1306}1307});1308}1309run(accessor: ServicesAccessor): void {1310const extensionEditor = getExtensionEditor(accessor);1311extensionEditor?.runFindAction(false);1312}1313});13141315registerAction2(class StartExtensionEditorFindPreviousAction extends Action2 {1316constructor() {1317super({1318id: 'editor.action.extensioneditor.findPrevious',1319title: localize('find previous', "Find Previous"),1320keybinding: {1321when: ContextKeyExpr.and(1322contextKeyExpr,1323KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED),1324primary: KeyMod.Shift | KeyCode.Enter,1325weight: KeybindingWeight.EditorContrib1326}1327});1328}1329run(accessor: ServicesAccessor): void {1330const extensionEditor = getExtensionEditor(accessor);1331extensionEditor?.runFindAction(true);1332}1333});13341335registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {13361337const link = theme.getColor(textLinkForeground);1338if (link) {1339collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource { color: ${link}; }`);1340collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a { color: ${link}; }`);1341}13421343const activeLink = theme.getColor(textLinkActiveForeground);1344if (activeLink) {1345collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource:hover,1346.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource:active { color: ${activeLink}; }`);1347collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a:hover,1348.monaco-workbench .extension-editor .content .feature-contributions a:active { color: ${activeLink}; }`);1349}13501351const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground);1352if (buttonHoverBackgroundColor) {1353collector.addRule(`.monaco-workbench .extension-editor .content > .details > .additional-details-container .categories-container > .categories > .category.clickable:hover { background-color: ${buttonHoverBackgroundColor}; border-color: ${buttonHoverBackgroundColor}; }`);1354}13551356const buttonForegroundColor = theme.getColor(buttonForeground);1357if (buttonForegroundColor) {1358collector.addRule(`.monaco-workbench .extension-editor .content > .details > .additional-details-container .categories-container > .categories > .category.clickable:hover { color: ${buttonForegroundColor}; }`);1359}13601361});13621363function getExtensionEditor(accessor: ServicesAccessor): ExtensionEditor | null {1364const activeEditorPane = accessor.get(IEditorService).activeEditorPane;1365if (activeEditorPane instanceof ExtensionEditor) {1366return activeEditorPane;1367}1368return null;1369}137013711372