/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/views'; import { toDisposable, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { MenuId, IMenuService, MenuItemAction, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { Registry } from 'vs/platform/registry/common/platform'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Event, Emitter } from 'vs/base/common/event'; import { IAction, ActionRunner, IActionViewItemProvider } from 'vs/base/common/actions'; import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import * as DOM from 'vs/base/browser/dom'; import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { URI } from 'vs/base/common/uri'; import { dirname, basename } from 'vs/base/common/resources'; import { FileKind } from 'vs/platform/files/common/files'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { localize } from 'vs/nls'; import { timeout } from 'vs/base/common/async'; import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMatchHighlight, listFilterMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; import { isString } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; export class TreeViewPane extends ViewPane { protected readonly treeView: ITreeView; constructor( options: IViewletViewOptions, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IInstantiationService instantiationService: IInstantiationService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, ) { super({ ...(options as IViewPaneOptions), titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); this._register(this.treeView.onDidChangeTitle((newTitle) => this.updateTitle(newTitle))); this._register(this.treeView.onDidChangeDescription((newDescription) => this.updateTitleDescription(newDescription))); this._register(toDisposable(() => this.treeView.setVisibility(false))); this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); this._register(this.treeView.onDidChangeWelcomeState(() => this._onDidChangeViewWelcomeState.fire())); if (options.title !== this.treeView.title) { this.updateTitle(this.treeView.title); } if (options.titleDescription !== this.treeView.description) { this.updateTitleDescription(this.treeView.description); } this.updateTreeVisibility(); } focus(): void { super.focus(); this.treeView.focus(); } renderBody(container: HTMLElement): void { super.renderBody(container); this.renderTreeView(container); } shouldShowWelcome(): boolean { return ((this.treeView.dataProvider === undefined) || !!this.treeView.dataProvider.isTreeEmpty) && (this.treeView.message === undefined); } layoutBody(height: number, width: number): void { super.layoutBody(height, width); this.layoutTreeView(height, width); } getOptimalWidth(): number { return this.treeView.getOptimalWidth(); } protected renderTreeView(container: HTMLElement): void { this.treeView.show(container); } protected layoutTreeView(height: number, width: number): void { this.treeView.layout(height, width); } private updateTreeVisibility(): void { this.treeView.setVisibility(this.isBodyVisible()); } } class Root implements ITreeItem { label = { label: 'root' }; handle = '0'; parentHandle: string | undefined = undefined; collapsibleState = TreeItemCollapsibleState.Expanded; children: ITreeItem[] | undefined = undefined; } const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); class Tree extends WorkbenchAsyncDataTree { } export class TreeView extends Disposable implements ITreeView { private isVisible: boolean = false; private _hasIconForParentNode = false; private _hasIconForLeafNode = false; private readonly collapseAllContextKey: RawContextKey; private readonly collapseAllContext: IContextKey; private readonly collapseAllToggleContextKey: RawContextKey; private readonly collapseAllToggleContext: IContextKey; private readonly refreshContextKey: RawContextKey; private readonly refreshContext: IContextKey; private focused: boolean = false; private domNode!: HTMLElement; private treeContainer!: HTMLElement; private _messageValue: string | undefined; private _canSelectMany: boolean = false; private messageElement!: HTMLDivElement; private tree: Tree | undefined; private treeLabels: ResourceLabels | undefined; private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; private readonly _onDidExpandItem: Emitter = this._register(new Emitter()); readonly onDidExpandItem: Event = this._onDidExpandItem.event; private readonly _onDidCollapseItem: Emitter = this._register(new Emitter()); readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; private _onDidChangeSelection: Emitter = this._register(new Emitter()); readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; private readonly _onDidChangeVisibility: Emitter = this._register(new Emitter()); readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; private readonly _onDidChangeActions: Emitter = this._register(new Emitter()); readonly onDidChangeActions: Event = this._onDidChangeActions.event; private readonly _onDidChangeWelcomeState: Emitter = this._register(new Emitter()); readonly onDidChangeWelcomeState: Event = this._onDidChangeWelcomeState.event; private readonly _onDidChangeTitle: Emitter = this._register(new Emitter()); readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; private readonly _onDidChangeDescription: Emitter = this._register(new Emitter()); readonly onDidChangeDescription: Event = this._onDidChangeDescription.event; private readonly _onDidCompleteRefresh: Emitter = this._register(new Emitter()); constructor( readonly id: string, private _title: string, @IThemeService private readonly themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IProgressService protected readonly progressService: IProgressService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IKeybindingService private readonly keybindingService: IKeybindingService, @INotificationService private readonly notificationService: INotificationService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IHoverService private readonly hoverService: IHoverService, @IContextKeyService contextKeyService: IContextKeyService ) { super(); this.root = new Root(); this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, false); this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService); this.collapseAllToggleContextKey = new RawContextKey(`treeView.${this.id}.toggleCollapseAll`, false); this.collapseAllToggleContext = this.collapseAllToggleContextKey.bindTo(contextKeyService); this.refreshContextKey = new RawContextKey(`treeView.${this.id}.enableRefresh`, false); this.refreshContext = this.refreshContextKey.bindTo(contextKeyService); this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.themeService.onDidColorThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('explorer.decorations')) { this.doRefresh([this.root]); /** soft refresh **/ } })); this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { if (views.some(v => v.id === this.id)) { this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } }); } })); this.registerActions(); this.create(); } get viewContainer(): ViewContainer { return this.viewDescriptorService.getViewContainerByViewId(this.id)!; } get viewLocation(): ViewContainerLocation { return this.viewDescriptorService.getViewLocationById(this.id)!; } private _dataProvider: ITreeViewDataProvider | undefined; get dataProvider(): ITreeViewDataProvider | undefined { return this._dataProvider; } set dataProvider(dataProvider: ITreeViewDataProvider | undefined) { if (this.tree === undefined) { this.createTree(); } if (dataProvider) { const self = this; this._dataProvider = new class implements ITreeViewDataProvider { private _isEmpty: boolean = true; private _onDidChangeEmpty: Emitter = new Emitter(); public onDidChangeEmpty: Event = this._onDidChangeEmpty.event; get isTreeEmpty(): boolean { return this._isEmpty; } async getChildren(node?: ITreeItem): Promise { let children: ITreeItem[]; if (node && node.children) { children = node.children; } else { node = node ?? self.root; children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); node.children = children; } if (node instanceof Root) { const oldEmpty = this._isEmpty; this._isEmpty = children.length === 0; if (oldEmpty !== this._isEmpty) { this._onDidChangeEmpty.fire(); } } return children; } }; if (this._dataProvider.onDidChangeEmpty) { this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire())); } this.updateMessage(); this.refresh(); } else { this._dataProvider = undefined; this.updateMessage(); } this._onDidChangeWelcomeState.fire(); } private _message: string | undefined; get message(): string | undefined { return this._message; } set message(message: string | undefined) { this._message = message; this.updateMessage(); this._onDidChangeWelcomeState.fire(); } get title(): string { return this._title; } set title(name: string) { this._title = name; this._onDidChangeTitle.fire(this._title); } private _description: string | undefined; get description(): string | undefined { return this._description; } set description(description: string | undefined) { this._description = description; this._onDidChangeDescription.fire(this._description); } get canSelectMany(): boolean { return this._canSelectMany; } set canSelectMany(canSelectMany: boolean) { this._canSelectMany = canSelectMany; } get hasIconForParentNode(): boolean { return this._hasIconForParentNode; } get hasIconForLeafNode(): boolean { return this._hasIconForLeafNode; } get visible(): boolean { return this.isVisible; } get showCollapseAllAction(): boolean { return !!this.collapseAllContext.get(); } set showCollapseAllAction(showCollapseAllAction: boolean) { this.collapseAllContext.set(showCollapseAllAction); } get showRefreshAction(): boolean { return !!this.refreshContext.get(); } set showRefreshAction(showRefreshAction: boolean) { this.refreshContext.set(showRefreshAction); } private registerActions() { const that = this; this._register(registerAction2(class extends Action2 { constructor() { super({ id: `workbench.actions.treeView.${that.id}.refresh`, title: localize('refresh', "Refresh"), menu: { id: MenuId.ViewTitle, when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.refreshContextKey), group: 'navigation', order: Number.MAX_SAFE_INTEGER - 1, }, icon: { id: 'codicon/refresh' } }); } async run(): Promise { return that.refresh(); } })); this._register(registerAction2(class extends Action2 { constructor() { super({ id: `workbench.actions.treeView.${that.id}.collapseAll`, title: localize('collapseAll', "Collapse All"), menu: { id: MenuId.ViewTitle, when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.collapseAllContextKey), group: 'navigation', order: Number.MAX_SAFE_INTEGER, }, precondition: that.collapseAllToggleContextKey, icon: { id: 'codicon/collapse-all' } }); } async run(): Promise { if (that.tree) { return new CollapseAllAction(that.tree, true).run(); } } })); } setVisibility(isVisible: boolean): void { isVisible = !!isVisible; if (this.isVisible === isVisible) { return; } this.isVisible = isVisible; if (this.tree) { if (this.isVisible) { DOM.show(this.tree.getHTMLElement()); } else { DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it } if (this.isVisible && this.elementsToRefresh.length) { this.doRefresh(this.elementsToRefresh); this.elementsToRefresh = []; } } this._onDidChangeVisibility.fire(this.isVisible); } focus(reveal: boolean = true): void { if (this.tree && this.root.children && this.root.children.length > 0) { // Make sure the current selected element is revealed const selectedElement = this.tree.getSelection()[0]; if (selectedElement && reveal) { this.tree.reveal(selectedElement, 0.5); } // Pass Focus to Viewer this.tree.domFocus(); } else if (this.tree) { this.tree.domFocus(); } else { this.domNode.focus(); } } show(container: HTMLElement): void { DOM.append(container, this.domNode); } private create() { this.domNode = DOM.$('.tree-explorer-viewlet-tree-view'); this.messageElement = DOM.append(this.domNode, DOM.$('.message')); this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree')); this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); const focusTracker = this._register(DOM.trackFocus(this.domNode)); this._register(focusTracker.onDidFocus(() => this.focused = true)); this._register(focusTracker.onDidBlur(() => this.focused = false)); } private createTree() { const actionViewItemProvider = (action: IAction) => { if (action instanceof MenuItemAction) { return this.instantiationService.createInstance(MenuEntryActionViewItem, action); } else if (action instanceof SubmenuItemAction) { return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action); } return undefined; }; const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); const dataSource = this.instantiationService.createInstance(TreeDataSource, this, (task: Promise) => this.progressService.withProgress({ location: this.id }, () => task)); const aligner = new Aligner(this.themeService); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); const widgetAriaLabel = this._title; this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer, new TreeViewDelegate(), [renderer], dataSource, { identityProvider: new TreeViewIdentityProvider(), accessibilityProvider: { getAriaLabel(element: ITreeItem): string { if (element.accessibilityInformation) { return element.accessibilityInformation.label; } if (isString(element.tooltip)) { return element.tooltip; } else { let buildAriaLabel: string = ''; if (element.label) { buildAriaLabel += element.label.label + ' '; } if (element.description) { buildAriaLabel += element.description; } return buildAriaLabel; } }, getRole(element: ITreeItem): string | undefined { return element.accessibilityInformation?.role ?? 'treeitem'; }, getWidgetAriaLabel(): string { return widgetAriaLabel; } }, keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (item: ITreeItem) => { return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); } }, expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command, collapseByDefault: (e: ITreeItem): boolean => { return e.collapsibleState !== TreeItemCollapsibleState.Expanded; }, multipleSelectionSupport: this.canSelectMany, overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } }) as WorkbenchAsyncDataTree); treeMenus.setContextKeyService(this.tree.contextKeyService); aligner.tree = this.tree; const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()); renderer.actionRunner = actionRunner; this.tree.contextKeyService.createKey(this.id, true); this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); this._register(this.tree.onDidChangeCollapseState(e => { if (!e.node.element) { return; } const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element; if (e.node.collapsed) { this._onDidCollapseItem.fire(element); } else { this._onDidExpandItem.fire(element); } })); this.tree.setInput(this.root).then(() => this.updateContentAreas()); this._register(this.tree.onDidOpen(e => { if (!e.browserEvent) { return; } const selection = this.tree!.getSelection(); const command = selection.length === 1 ? selection[0].command : undefined; if (command) { let args = command.arguments || []; if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) { // Some commands owned by us should receive the // `IOpenEvent` as context to open properly args = [...args, e]; } this.commandService.executeCommand(command.id, ...args); } })); } private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent, actionRunner: MultipleSelectionActionRunner): void { this.hoverService.hideHover(); const node: ITreeItem | null = treeEvent.element; if (node === null) { return; } const event: UIEvent = treeEvent.browserEvent; event.preventDefault(); event.stopPropagation(); this.tree!.setFocus([node]); const actions = treeMenus.getResourceContextActions(node); if (!actions.length) { return; } this.contextMenuService.showContextMenu({ getAnchor: () => treeEvent.anchor, getActions: () => actions, getActionViewItem: (action) => { const keybinding = this.keybindingService.lookupKeybinding(action.id); if (keybinding) { return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); } return undefined; }, onHide: (wasCancelled?: boolean) => { if (wasCancelled) { this.tree!.domFocus(); } }, getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle }), actionRunner }); } protected updateMessage(): void { if (this._message) { this.showMessage(this._message); } else if (!this.dataProvider) { this.showMessage(noDataProviderMessage); } else { this.hideMessage(); } this.updateContentAreas(); } private showMessage(message: string): void { this.messageElement.classList.remove('hide'); this.resetMessageElement(); this._messageValue = message; if (!isFalsyOrWhitespace(this._message)) { this.messageElement.textContent = this._messageValue; } this.layout(this._height, this._width); } private hideMessage(): void { this.resetMessageElement(); this.messageElement.classList.add('hide'); this.layout(this._height, this._width); } private resetMessageElement(): void { DOM.clearNode(this.messageElement); } private _height: number = 0; private _width: number = 0; layout(height: number, width: number) { if (height && width) { this._height = height; this._width = width; const treeHeight = height - DOM.getTotalHeight(this.messageElement); this.treeContainer.style.height = treeHeight + 'px'; if (this.tree) { this.tree.layout(treeHeight, width); } } } getOptimalWidth(): number { if (this.tree) { const parentNode = this.tree.getHTMLElement(); const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a')); return DOM.getLargestChildWidth(parentNode, childNodes); } return 0; } async refresh(elements?: ITreeItem[]): Promise { if (this.dataProvider && this.tree) { if (this.refreshing) { await Event.toPromise(this._onDidCompleteRefresh.event); } if (!elements) { elements = [this.root]; // remove all waiting elements to refresh if root is asked to refresh this.elementsToRefresh = []; } for (const element of elements) { element.children = undefined; // reset children } if (this.isVisible) { return this.doRefresh(elements); } else { if (this.elementsToRefresh.length) { const seen: Set = new Set(); this.elementsToRefresh.forEach(element => seen.add(element.handle)); for (const element of elements) { if (!seen.has(element.handle)) { this.elementsToRefresh.push(element); } } } else { this.elementsToRefresh.push(...elements); } } } return undefined; } async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { const tree = this.tree; if (tree) { itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; await Promise.all(itemOrItems.map(element => { return tree.expand(element, false); })); } } setSelection(items: ITreeItem[]): void { if (this.tree) { this.tree.setSelection(items); } } setFocus(item: ITreeItem): void { if (this.tree) { this.focus(); this.tree.setFocus([item]); } } async reveal(item: ITreeItem): Promise { if (this.tree) { return this.tree.reveal(item); } } private refreshing: boolean = false; private async doRefresh(elements: ITreeItem[]): Promise { const tree = this.tree; if (tree && this.visible) { this.refreshing = true; await Promise.all(elements.map(element => tree.updateChildren(element, true, true))); this.refreshing = false; this._onDidCompleteRefresh.fire(); this.updateContentAreas(); if (this.focused) { this.focus(false); } this.updateCollapseAllToggle(); } } private updateCollapseAllToggle() { if (this.showCollapseAllAction) { this.collapseAllToggleContext.set(!!this.root.children && (this.root.children.length > 0) && this.root.children.some(value => value.collapsibleState !== TreeItemCollapsibleState.None)); } } private updateContentAreas(): void { const isTreeEmpty = !this.root.children || this.root.children.length === 0; // Hide tree container only when there is a message and tree is empty and not refreshing if (this._messageValue && isTreeEmpty && !this.refreshing) { this.treeContainer.classList.add('hide'); this.domNode.setAttribute('tabindex', '0'); } else { this.treeContainer.classList.remove('hide'); this.domNode.removeAttribute('tabindex'); } } } class TreeViewIdentityProvider implements IIdentityProvider { getId(element: ITreeItem): { toString(): string; } { return element.handle; } } class TreeViewDelegate implements IListVirtualDelegate { getHeight(element: ITreeItem): number { return TreeRenderer.ITEM_HEIGHT; } getTemplateId(element: ITreeItem): string { return TreeRenderer.TREE_TEMPLATE_ID; } } class TreeDataSource implements IAsyncDataSource { constructor( private treeView: ITreeView, private withProgress: (task: Promise) => Promise ) { } hasChildren(element: ITreeItem): boolean { return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None); } async getChildren(element: ITreeItem): Promise { if (this.treeView.dataProvider) { return this.withProgress(this.treeView.dataProvider.getChildren(element)); } return []; } } // todo@jrieken,sandy make this proper and contributable from extensions registerThemingParticipant((theme, collector) => { const matchBackgroundColor = theme.getColor(listFilterMatchHighlight); if (matchBackgroundColor) { collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); } const matchBorderColor = theme.getColor(listFilterMatchHighlightBorder); if (matchBorderColor) { collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); } const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`); } const focusBorderColor = theme.getColor(focusBorder); if (focusBorderColor) { collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focusBorderColor}; outline-offset: -1px; }`); } const codeBackground = theme.getColor(textCodeBlockBackground); if (codeBackground) { collector.addRule(`.tree-explorer-viewlet-tree-view > .message code { background-color: ${codeBackground}; }`); } }); interface ITreeExplorerTemplateData { elementDisposable: IDisposable; container: HTMLElement; resourceLabel: IResourceLabel; icon: HTMLElement; actionBar: ActionBar; } class TreeRenderer extends Disposable implements ITreeRenderer { static readonly ITEM_HEIGHT = 22; static readonly TREE_TEMPLATE_ID = 'treeExplorer'; private _actionRunner: MultipleSelectionActionRunner | undefined; private _hoverDelegate: IHoverDelegate; constructor( private treeViewId: string, private menus: TreeMenus, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, private aligner: Aligner, @IThemeService private readonly themeService: IThemeService, @IConfigurationService private readonly configurationService: IConfigurationService, @ILabelService private readonly labelService: ILabelService, @IHoverService private readonly hoverService: IHoverService ) { super(); this._hoverDelegate = { showHover: (options: IHoverDelegateOptions): IDisposable | undefined => { return this.hoverService.showHover(options); } }; } get templateId(): string { return TreeRenderer.TREE_TEMPLATE_ID; } set actionRunner(actionRunner: MultipleSelectionActionRunner) { this._actionRunner = actionRunner; } renderTemplate(container: HTMLElement): ITreeExplorerTemplateData { container.classList.add('custom-view-tree-node-item'); const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); const resourceLabel = this.labels.create(container, { supportHighlights: true, hoverDelegate: this._hoverDelegate }); const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); const actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider }); return { resourceLabel, icon, actionBar, container, elementDisposable: Disposable.None }; } private getHover(label: string | undefined, resource: URI | null, node: ITreeItem): string | IIconLabelMarkdownString | undefined { if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) { if (resource) { return undefined; } else if (!node.tooltip) { return label; } else if (!isString(node.tooltip)) { return { markdown: node.tooltip, markdownNotSupportedFallback: resource ? undefined : renderMarkdownAsPlaintext(node.tooltip) }; // Passing undefined as the fallback for a resource falls back to the old native hover } else { return node.tooltip; } } return { markdown: new Promise(async (resolve) => { await node.resolve(); resolve(node.tooltip); }), markdownNotSupportedFallback: resource ? undefined : '' // Passing undefined as the fallback for a resource falls back to the old native hover }; } renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { templateData.elementDisposable.dispose(); const node = element.element; const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : (resource ? { label: basename(resource) } : undefined); const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined; const label = treeItemLabel ? treeItemLabel.label : undefined; const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => { if (start < 0) { start = label.length + start; } if (end < 0) { end = label.length + end; } if ((start >= label.length) || (end > label.length)) { return ({ start: 0, end: 0 }); } if (start > end) { const swap = start; start = end; end = swap; } return ({ start, end }); }) : undefined; const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark; const iconUrl = icon ? URI.revive(icon) : null; const title = this.getHover(label, resource, node); // reset templateData.actionBar.clear(); templateData.icon.style.color = ''; if (resource || this.isFileKindThemeIcon(node.themeIcon)) { const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); const labelResource = resource ? resource : URI.parse('missing:_icon_resource'); templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, { fileKind: this.getFileKind(node), title, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData), strikethrough: treeItemLabel?.strikethrough }); } else { templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData), strikethrough: treeItemLabel?.strikethrough }); } if (iconUrl) { templateData.icon.className = 'custom-view-tree-node-item-icon'; templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl); } else { let iconClass: string | undefined; if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) { iconClass = ThemeIcon.asClassName(node.themeIcon); if (node.themeIcon.color) { templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? ''; } } templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : ''; templateData.icon.style.backgroundImage = ''; } templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); if (this._actionRunner) { templateData.actionBar.actionRunner = this._actionRunner; } this.setAlignment(templateData.container, node); const disposableStore = new DisposableStore(); templateData.elementDisposable = disposableStore; disposableStore.add(this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); } private setAlignment(container: HTMLElement, treeItem: ITreeItem) { container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); } private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean { if (icon) { return icon.id === FileThemeIcon.id || icon.id === FolderThemeIcon.id; } else { return false; } } private getFileKind(node: ITreeItem): FileKind { if (node.themeIcon) { switch (node.themeIcon.id) { case FileThemeIcon.id: return FileKind.FILE; case FolderThemeIcon.id: return FileKind.FOLDER; } } return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; } disposeElement(resource: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { templateData.elementDisposable.dispose(); } disposeTemplate(templateData: ITreeExplorerTemplateData): void { templateData.resourceLabel.dispose(); templateData.actionBar.dispose(); templateData.elementDisposable.dispose(); } } class Aligner extends Disposable { private _tree: WorkbenchAsyncDataTree | undefined; constructor(private themeService: IThemeService) { super(); } set tree(tree: WorkbenchAsyncDataTree) { this._tree = tree; } public alignIconWithTwisty(treeItem: ITreeItem): boolean { if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) { return false; } if (!this.hasIcon(treeItem)) { return false; } if (this._tree) { const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); if (this.hasIcon(parent)) { return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIcon(c)); } return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); } else { return false; } } private hasIcon(node: ITreeItem): boolean { const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark; if (icon) { return true; } if (node.resourceUri || node.themeIcon) { const fileIconTheme = this.themeService.getFileIconTheme(); const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None; if (isFolder) { return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons; } return fileIconTheme.hasFileIcons; } return false; } } class MultipleSelectionActionRunner extends ActionRunner { constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) { super(); this._register(this.onDidRun(e => { if (e.error) { notificationService.error(localize('command-error', 'Error running command {1}: {0}. This is likely caused by the extension that contributes {1}.', e.error.message, e.action.id)); } })); } runAction(action: IAction, context: TreeViewItemHandleArg): Promise { const selection = this.getSelectedResources(); let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined; let actionInSelected: boolean = false; if (selection.length > 1) { selectionHandleArgs = selection.map(selected => { if (selected.handle === context.$treeItemHandle) { actionInSelected = true; } return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle }; }); } if (!actionInSelected) { selectionHandleArgs = undefined; } return action.run(...[context, selectionHandleArgs]); } } class TreeMenus extends Disposable implements IDisposable { private contextKeyService: IContextKeyService | undefined; constructor( private id: string, @IMenuService private readonly menuService: IMenuService ) { super(); } getResourceActions(element: ITreeItem): IAction[] { return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).primary; } getResourceContextActions(element: ITreeItem): IAction[] { return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).secondary; } public setContextKeyService(service: IContextKeyService) { this.contextKeyService = service; } private getActions(menuId: MenuId, context: { key: string, value?: string }): { primary: IAction[]; secondary: IAction[]; } { if (!this.contextKeyService) { return { primary: [], secondary: [] }; } const contextKeyService = this.contextKeyService.createScoped(); contextKeyService.createKey('view', this.id); contextKeyService.createKey(context.key, context.value); const menu = this.menuService.createMenu(menuId, contextKeyService); const primary: IAction[] = []; const secondary: IAction[] = []; const result = { primary, secondary }; createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g)); menu.dispose(); contextKeyService.dispose(); return result; } } export class CustomTreeView extends TreeView { private activated: boolean = false; constructor( id: string, title: string, @IThemeService themeService: IThemeService, @IInstantiationService instantiationService: IInstantiationService, @ICommandService commandService: ICommandService, @IConfigurationService configurationService: IConfigurationService, @IProgressService progressService: IProgressService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextKeyService contextKeyService: IContextKeyService, @IHoverService hoverService: IHoverService, @IExtensionService private readonly extensionService: IExtensionService, ) { super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService); } setVisibility(isVisible: boolean): void { super.setVisibility(isVisible); if (this.visible) { this.activate(); } } private activate() { if (!this.activated) { this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000)) .then(() => { this.updateMessage(); }); this.activated = true; } } }