Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionsViewer.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 * as dom from '../../../../base/browser/dom.js';6import { localize } from '../../../../nls.js';7import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable } from '../../../../base/common/lifecycle.js';8import { Action, ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';9import { IExtensionsWorkbenchService, IExtension, IExtensionsViewState } from '../common/extensions.js';10import { Event } from '../../../../base/common/event.js';11import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';12import { IListService, IWorkbenchPagedListOptions, WorkbenchAsyncDataTree, WorkbenchPagedList } from '../../../../platform/list/browser/listService.js';13import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';14import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';15import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js';16import { IAsyncDataSource, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';17import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js';18import { CancellationToken } from '../../../../base/common/cancellation.js';19import { isNonEmptyArray } from '../../../../base/common/arrays.js';20import { Delegate, Renderer } from './extensionsList.js';21import { listFocusForeground, listFocusBackground, foreground, editorBackground } from '../../../../platform/theme/common/colorRegistry.js';22import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';23import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';24import { KeyCode } from '../../../../base/common/keyCodes.js';25import { IListStyles } from '../../../../base/browser/ui/list/listWidget.js';26import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';27import { IStyleOverride } from '../../../../platform/theme/browser/defaultStyles.js';28import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';29import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js';30import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';31import { ExtensionAction, getContextMenuActions, ManageExtensionAction } from './extensionsActions.js';32import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';33import { INotificationService } from '../../../../platform/notification/common/notification.js';34import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js';35import { DelayedPagedModel, IPagedModel } from '../../../../base/common/paging.js';36import { ExtensionIconWidget } from './extensionsWidgets.js';3738function getAriaLabelForExtension(extension: IExtension | null): string {39if (!extension) {40return '';41}42const publisher = extension.publisherDomain?.verified ? localize('extension.arialabel.verifiedPublisher', "Verified Publisher {0}", extension.publisherDisplayName) : localize('extension.arialabel.publisher', "Publisher {0}", extension.publisherDisplayName);43const deprecated = extension?.deprecationInfo ? localize('extension.arialabel.deprecated', "Deprecated") : '';44const rating = extension?.rating ? localize('extension.arialabel.rating', "Rated {0} out of 5 stars by {1} users", extension.rating.toFixed(2), extension.ratingCount) : '';45return `${extension.displayName}, ${deprecated ? `${deprecated}, ` : ''}${extension.version}, ${publisher}, ${extension.description} ${rating ? `, ${rating}` : ''}`;46}4748export class ExtensionsList extends Disposable {4950readonly list: WorkbenchPagedList<IExtension>;51private readonly contextMenuActionRunner = this._register(new ActionRunner());5253constructor(54parent: HTMLElement,55viewId: string,56options: Partial<IWorkbenchPagedListOptions<IExtension>>,57extensionsViewState: IExtensionsViewState,58@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,59@IViewDescriptorService viewDescriptorService: IViewDescriptorService,60@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,61@INotificationService notificationService: INotificationService,62@IContextMenuService private readonly contextMenuService: IContextMenuService,63@IContextKeyService private readonly contextKeyService: IContextKeyService,64@IInstantiationService private readonly instantiationService: IInstantiationService,65) {66super();67this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && notificationService.error(error)));68const delegate = new Delegate();69const renderer = instantiationService.createInstance(Renderer, extensionsViewState, {70hoverOptions: {71position: () => {72const viewLocation = viewDescriptorService.getViewLocationById(viewId);73if (viewLocation === ViewContainerLocation.Sidebar) {74return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT;75}76if (viewLocation === ViewContainerLocation.AuxiliaryBar) {77return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT;78}79return HoverPosition.RIGHT;80}81}82});83this.list = instantiationService.createInstance(WorkbenchPagedList, `${viewId}-Extensions`, parent, delegate, [renderer], {84multipleSelectionSupport: false,85setRowLineHeight: false,86horizontalScrolling: false,87accessibilityProvider: {88getAriaLabel(extension: IExtension | null): string {89return getAriaLabelForExtension(extension);90},91getWidgetAriaLabel(): string {92return localize('extensions', "Extensions");93}94},95overrideStyles: getLocationBasedViewColors(viewDescriptorService.getViewLocationById(viewId)).listOverrideStyles,96openOnSingleClick: true,97...options98}) as WorkbenchPagedList<IExtension>;99this._register(this.list.onContextMenu(e => this.onContextMenu(e), this));100this._register(this.list);101102this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => {103this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions });104}));105}106107setModel(model: IPagedModel<IExtension>) {108this.list.model = new DelayedPagedModel(model);109}110111layout(height?: number, width?: number): void {112this.list.layout(height, width);113}114115private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void {116extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension;117this.extensionsWorkbenchService.open(extension, options);118}119120private async onContextMenu(e: IListContextMenuEvent<IExtension>): Promise<void> {121if (e.element) {122const disposables = new DisposableStore();123const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction));124const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element125: e.element;126manageExtensionAction.extension = extension;127let groups: IAction[][] = [];128if (manageExtensionAction.enabled) {129groups = await manageExtensionAction.getActionGroups();130} else if (extension) {131groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService);132groups.forEach(group => group.forEach(extensionAction => {133if (extensionAction instanceof ExtensionAction) {134extensionAction.extension = extension;135}136}));137}138const actions: IAction[] = [];139for (const menuActions of groups) {140for (const menuAction of menuActions) {141actions.push(menuAction);142if (isDisposable(menuAction)) {143disposables.add(menuAction);144}145}146actions.push(new Separator());147}148actions.pop();149this.contextMenuService.showContextMenu({150getAnchor: () => e.anchor,151getActions: () => actions,152actionRunner: this.contextMenuActionRunner,153onHide: () => disposables.dispose()154});155}156}157}158159export class ExtensionsGridView extends Disposable {160161readonly element: HTMLElement;162private readonly renderer: Renderer;163private readonly delegate: Delegate;164private readonly disposableStore: DisposableStore;165166constructor(167parent: HTMLElement,168delegate: Delegate,169@IInstantiationService private readonly instantiationService: IInstantiationService170) {171super();172this.element = dom.append(parent, dom.$('.extensions-grid-view'));173this.renderer = this.instantiationService.createInstance(Renderer, { onFocus: Event.None, onBlur: Event.None, filters: {} }, { hoverOptions: { position() { return HoverPosition.BELOW; } } });174this.delegate = delegate;175this.disposableStore = this._register(new DisposableStore());176}177178setExtensions(extensions: IExtension[]): void {179this.disposableStore.clear();180extensions.forEach((e, index) => this.renderExtension(e, index));181}182183private renderExtension(extension: IExtension, index: number): void {184const extensionContainer = dom.append(this.element, dom.$('.extension-container'));185extensionContainer.style.height = `${this.delegate.getHeight()}px`;186extensionContainer.setAttribute('tabindex', '0');187188const template = this.renderer.renderTemplate(extensionContainer);189this.disposableStore.add(toDisposable(() => this.renderer.disposeTemplate(template)));190191const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction);192openExtensionAction.extension = extension;193template.name.setAttribute('tabindex', '0');194195const handleEvent = (e: StandardMouseEvent | StandardKeyboardEvent) => {196if (e instanceof StandardKeyboardEvent && e.keyCode !== KeyCode.Enter) {197return;198}199openExtensionAction.run(e.ctrlKey || e.metaKey);200e.stopPropagation();201e.preventDefault();202};203204this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.CLICK, (e: MouseEvent) => handleEvent(new StandardMouseEvent(dom.getWindow(template.name), e))));205this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => handleEvent(new StandardKeyboardEvent(e))));206this.disposableStore.add(dom.addDisposableListener(extensionContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => handleEvent(new StandardKeyboardEvent(e))));207208this.renderer.renderElement(extension, index, template);209}210}211212interface IExtensionTemplateData {213name: HTMLElement;214identifier: HTMLElement;215author: HTMLElement;216extensionDisposables: IDisposable[];217extensionData: IExtensionData;218}219220interface IUnknownExtensionTemplateData {221identifier: HTMLElement;222}223224interface IExtensionData {225extension: IExtension;226hasChildren: boolean;227getChildren: () => Promise<IExtensionData[] | null>;228parent: IExtensionData | null;229}230231class AsyncDataSource implements IAsyncDataSource<IExtensionData, any> {232233public hasChildren({ hasChildren }: IExtensionData): boolean {234return hasChildren;235}236237public getChildren(extensionData: IExtensionData): Promise<any> {238return extensionData.getChildren();239}240241}242243class VirualDelegate implements IListVirtualDelegate<IExtensionData> {244245public getHeight(element: IExtensionData): number {246return 62;247}248public getTemplateId({ extension }: IExtensionData): string {249return extension ? ExtensionRenderer.TEMPLATE_ID : UnknownExtensionRenderer.TEMPLATE_ID;250}251}252253class ExtensionRenderer implements IListRenderer<ITreeNode<IExtensionData>, IExtensionTemplateData> {254255static readonly TEMPLATE_ID = 'extension-template';256257constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {258}259260public get templateId(): string {261return ExtensionRenderer.TEMPLATE_ID;262}263264public renderTemplate(container: HTMLElement): IExtensionTemplateData {265container.classList.add('extension');266267const iconWidget = this.instantiationService.createInstance(ExtensionIconWidget, container);268const details = dom.append(container, dom.$('.details'));269270const header = dom.append(details, dom.$('.header'));271const name = dom.append(header, dom.$('span.name'));272const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction);273const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => {274openExtensionAction.run(e.ctrlKey || e.metaKey);275e.stopPropagation();276e.preventDefault();277}), iconWidget, openExtensionAction];278const identifier = dom.append(header, dom.$('span.identifier'));279280const footer = dom.append(details, dom.$('.footer'));281const author = dom.append(footer, dom.$('.author'));282return {283name,284identifier,285author,286extensionDisposables,287set extensionData(extensionData: IExtensionData) {288iconWidget.extension = extensionData.extension;289openExtensionAction.extension = extensionData.extension;290}291};292}293294public renderElement(node: ITreeNode<IExtensionData>, index: number, data: IExtensionTemplateData): void {295const extension = node.element.extension;296data.name.textContent = extension.displayName;297data.identifier.textContent = extension.identifier.id;298data.author.textContent = extension.publisherDisplayName;299data.extensionData = node.element;300}301302public disposeTemplate(templateData: IExtensionTemplateData): void {303templateData.extensionDisposables = dispose((<IExtensionTemplateData>templateData).extensionDisposables);304}305}306307class UnknownExtensionRenderer implements IListRenderer<ITreeNode<IExtensionData>, IUnknownExtensionTemplateData> {308309static readonly TEMPLATE_ID = 'unknown-extension-template';310311public get templateId(): string {312return UnknownExtensionRenderer.TEMPLATE_ID;313}314315public renderTemplate(container: HTMLElement): IUnknownExtensionTemplateData {316const messageContainer = dom.append(container, dom.$('div.unknown-extension'));317dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error");318dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:");319320const identifier = dom.append(messageContainer, dom.$('span.message'));321return { identifier };322}323324public renderElement(node: ITreeNode<IExtensionData>, index: number, data: IUnknownExtensionTemplateData): void {325data.identifier.textContent = node.element.extension.identifier.id;326}327328public disposeTemplate(data: IUnknownExtensionTemplateData): void {329}330}331332class OpenExtensionAction extends Action {333334private _extension: IExtension | undefined;335336constructor(@IExtensionsWorkbenchService private readonly extensionsWorkdbenchService: IExtensionsWorkbenchService) {337super('extensions.action.openExtension', '');338}339340public set extension(extension: IExtension) {341this._extension = extension;342}343344override run(sideByside: boolean): Promise<any> {345if (this._extension) {346return this.extensionsWorkdbenchService.open(this._extension, { sideByside });347}348return Promise.resolve();349}350}351352export class ExtensionsTree extends WorkbenchAsyncDataTree<IExtensionData, IExtensionData> {353354constructor(355input: IExtensionData,356container: HTMLElement,357overrideStyles: IStyleOverride<IListStyles>,358@IContextKeyService contextKeyService: IContextKeyService,359@IListService listService: IListService,360@IInstantiationService instantiationService: IInstantiationService,361@IConfigurationService configurationService: IConfigurationService,362@IExtensionsWorkbenchService extensionsWorkdbenchService: IExtensionsWorkbenchService363) {364const delegate = new VirualDelegate();365const dataSource = new AsyncDataSource();366const renderers = [instantiationService.createInstance(ExtensionRenderer), instantiationService.createInstance(UnknownExtensionRenderer)];367const identityProvider = {368getId({ extension, parent }: IExtensionData): string {369return parent ? this.getId(parent) + '/' + extension.identifier.id : extension.identifier.id;370}371};372373super(374'ExtensionsTree',375container,376delegate,377renderers,378dataSource,379{380indent: 40,381identityProvider,382multipleSelectionSupport: false,383overrideStyles,384accessibilityProvider: {385getAriaLabel(extensionData: IExtensionData): string {386return getAriaLabelForExtension(extensionData.extension);387},388getWidgetAriaLabel(): string {389return localize('extensions', "Extensions");390}391}392},393instantiationService, contextKeyService, listService, configurationService394);395396this.setInput(input);397398this.disposables.add(this.onDidChangeSelection(event => {399if (dom.isKeyboardEvent(event.browserEvent)) {400extensionsWorkdbenchService.open(event.elements[0].extension, { sideByside: false });401}402}));403}404}405406export class ExtensionData implements IExtensionData {407408readonly extension: IExtension;409readonly parent: IExtensionData | null;410private readonly getChildrenExtensionIds: (extension: IExtension) => string[];411private readonly childrenExtensionIds: string[];412private readonly extensionsWorkbenchService: IExtensionsWorkbenchService;413414constructor(extension: IExtension, parent: IExtensionData | null, getChildrenExtensionIds: (extension: IExtension) => string[], extensionsWorkbenchService: IExtensionsWorkbenchService) {415this.extension = extension;416this.parent = parent;417this.getChildrenExtensionIds = getChildrenExtensionIds;418this.extensionsWorkbenchService = extensionsWorkbenchService;419this.childrenExtensionIds = this.getChildrenExtensionIds(extension);420}421422get hasChildren(): boolean {423return isNonEmptyArray(this.childrenExtensionIds);424}425426async getChildren(): Promise<IExtensionData[] | null> {427if (this.hasChildren) {428const result: IExtension[] = await getExtensions(this.childrenExtensionIds, this.extensionsWorkbenchService);429return result.map(extension => new ExtensionData(extension, this, this.getChildrenExtensionIds, this.extensionsWorkbenchService));430}431return null;432}433}434435export async function getExtensions(extensions: string[], extensionsWorkbenchService: IExtensionsWorkbenchService): Promise<IExtension[]> {436const localById = extensionsWorkbenchService.local.reduce((result, e) => { result.set(e.identifier.id.toLowerCase(), e); return result; }, new Map<string, IExtension>());437const result: IExtension[] = [];438const toQuery: string[] = [];439for (const extensionId of extensions) {440const id = extensionId.toLowerCase();441const local = localById.get(id);442if (local) {443result.push(local);444} else {445toQuery.push(id);446}447}448if (toQuery.length) {449const galleryResult = await extensionsWorkbenchService.getExtensions(toQuery.map(id => ({ id })), CancellationToken.None);450result.push(...galleryResult);451}452return result;453}454455registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {456const focusBackground = theme.getColor(listFocusBackground);457if (focusBackground) {458collector.addRule(`.extensions-grid-view .extension-container:focus { background-color: ${focusBackground}; outline: none; }`);459}460const focusForeground = theme.getColor(listFocusForeground);461if (focusForeground) {462collector.addRule(`.extensions-grid-view .extension-container:focus { color: ${focusForeground}; }`);463}464const foregroundColor = theme.getColor(foreground);465const editorBackgroundColor = theme.getColor(editorBackground);466if (foregroundColor && editorBackgroundColor) {467const authorForeground = foregroundColor.transparent(.9).makeOpaque(editorBackgroundColor);468collector.addRule(`.extensions-grid-view .extension-container:not(.disabled) .author { color: ${authorForeground}; }`);469const disabledExtensionForeground = foregroundColor.transparent(.5).makeOpaque(editorBackgroundColor);470collector.addRule(`.extensions-grid-view .extension-container.disabled { color: ${disabledExtensionForeground}; }`);471}472});473474475