Skip to content

Commit

Permalink
Add resolveTreeItem to custom tree
Browse files Browse the repository at this point in the history
Part of #100741
  • Loading branch information
alexr00 committed Jun 24, 2020
1 parent fe66be1 commit 39a406d
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 15 deletions.
5 changes: 5 additions & 0 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,11 @@ declare module 'vscode' {

}

// https://github.com/microsoft/vscode/issues/100741
export interface TreeDataProvider<T> {
resolveTreeItem?(element: T, item: TreeItem2): TreeItem2 | Thenable<TreeItem2>;
}

export class TreeItem2 extends TreeItem {
/**
* Label describing this item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri).
Expand Down
14 changes: 10 additions & 4 deletions src/vs/workbench/api/browser/mainThreadTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { Disposable } from 'vs/base/common/lifecycle';
import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions } from 'vs/workbench/common/views';
import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem } from 'vs/workbench/common/views';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { distinct } from 'vs/base/common/arrays';
import { INotificationService } from 'vs/platform/notification/common/notification';
Expand Down Expand Up @@ -163,11 +163,13 @@ type TreeItemHandle = string;
class TreeViewDataProvider implements ITreeViewDataProvider {

private readonly itemsMap: Map<TreeItemHandle, ITreeItem> = new Map<TreeItemHandle, ITreeItem>();
private hasResolve: Promise<boolean>;

constructor(private readonly treeViewId: string,
private readonly _proxy: ExtHostTreeViewsShape,
private readonly notificationService: INotificationService
) {
this.hasResolve = this._proxy.$hasResolve(this.treeViewId);
}

getChildren(treeItem?: ITreeItem): Promise<ITreeItem[]> {
Expand Down Expand Up @@ -214,12 +216,16 @@ class TreeViewDataProvider implements ITreeViewDataProvider {
return this.itemsMap.size === 0;
}

private postGetChildren(elements: ITreeItem[]): ITreeItem[] {
const result: ITreeItem[] = [];
private async postGetChildren(elements: ITreeItem[]): Promise<ResolvableTreeItem[]> {
const result: ResolvableTreeItem[] = [];
const hasResolve = await this.hasResolve;
if (elements) {
for (const element of elements) {
const resolvable = new ResolvableTreeItem(element, hasResolve ? () => {
return this._proxy.$resolve(this.treeViewId, element.handle);
} : undefined);
this.itemsMap.set(element.handle, element);
result.push(element);
result.push(resolvable);
}
}
return result;
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,8 @@ export interface ExtHostTreeViewsShape {
$setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void;
$setSelection(treeViewId: string, treeItemHandles: string[]): void;
$setVisible(treeViewId: string, visible: boolean): void;
$hasResolve(treeViewId: string): Promise<boolean>;
$resolve(treeViewId: string, treeItemHandle: string): Promise<ITreeItem | undefined>;
}

export interface ExtHostWorkspaceShape {
Expand Down
39 changes: 39 additions & 0 deletions src/vs/workbench/api/common/extHostTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return treeView.getChildren(treeItemHandle);
}

async $hasResolve(treeViewId: string): Promise<boolean> {
const treeView = this.treeViews.get(treeViewId);
if (!treeView) {
throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId));
}
return treeView.hasResolve;
}

$resolve(treeViewId: string, treeItemHandle: string): Promise<ITreeItem | undefined> {
const treeView = this.treeViews.get(treeViewId);
if (!treeView) {
throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId));
}
return treeView.resolveTreeItem(treeItemHandle);
}

$setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void {
const treeView = this.treeViews.get(treeViewId);
if (!treeView) {
Expand Down Expand Up @@ -160,6 +176,7 @@ type TreeData<T> = { message: boolean, element: T | Root | false };

interface TreeNode extends IDisposable {
item: ITreeItem;
extensionItem: vscode.TreeItem2;
parent: TreeNode | Root;
children?: TreeNode[];
}
Expand Down Expand Up @@ -329,6 +346,27 @@ class ExtHostTreeView<T> extends Disposable {
}
}

get hasResolve(): boolean {
return !!this.dataProvider.resolveTreeItem;
}

async resolveTreeItem(treeItemHandle: string): Promise<ITreeItem | undefined> {
if (!this.dataProvider.resolveTreeItem) {
return;
}
const element = this.elements.get(treeItemHandle);
if (element) {
const node = this.nodes.get(element);
if (node) {
const resolve = await this.dataProvider.resolveTreeItem(element, node.extensionItem);
// Resolvable elements. Currently only tooltip.
node.item.tooltip = resolve.tooltip;
return node.item;
}
}
return;
}

private resolveUnknownParentChain(element: T): Promise<TreeNode[]> {
return this.resolveParent(element)
.then((parent) => {
Expand Down Expand Up @@ -521,6 +559,7 @@ class ExtHostTreeView<T> extends Disposable {

return {
item,
extensionItem: extensionTreeItem,
parent,
children: undefined,
dispose(): void { disposable.dispose(); }
Expand Down
50 changes: 50 additions & 0 deletions src/vs/workbench/common/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,56 @@ export interface ITreeItem {
accessibilityInformation?: IAccessibilityInformation;
}

export class ResolvableTreeItem implements ITreeItem {
handle: string;
parentHandle?: string;
collapsibleState: TreeItemCollapsibleState;
label?: ITreeItemLabel;
description?: string | boolean;
icon?: UriComponents;
iconDark?: UriComponents;
themeIcon?: ThemeIcon;
resourceUri?: UriComponents;
tooltip?: string | IMarkdownString;
contextValue?: string;
command?: Command;
children?: ITreeItem[];
accessibilityInformation?: IAccessibilityInformation;
resolve: () => Promise<void>;
private resolved: boolean = false;
private _hasResolve: boolean = false;
constructor(treeItem: ITreeItem, resolve?: (() => Promise<ITreeItem | undefined>)) {
this.handle = treeItem.handle;
this.parentHandle = treeItem.parentHandle;
this.collapsibleState = treeItem.collapsibleState;
this.label = treeItem.label;
this.description = treeItem.description;
this.icon = treeItem.icon;
this.iconDark = treeItem.iconDark;
this.themeIcon = treeItem.themeIcon;
this.resourceUri = treeItem.resourceUri;
this.tooltip = treeItem.tooltip;
this.contextValue = treeItem.contextValue;
this.command = treeItem.command;
this.children = treeItem.children;
this.accessibilityInformation = treeItem.accessibilityInformation;
this._hasResolve = !!resolve;
this.resolve = async () => {
if (resolve && !this.resolved) {
const resolvedItem = await resolve();
if (resolvedItem) {
// Resolvable elements. Currently only tooltip.
this.tooltip = resolvedItem.tooltip;
}
}
this.resolved = true;
};
}
get hasResolve(): boolean {
return this._hasResolve;
}
}

export interface ITreeViewDataProvider {
readonly isTreeEmpty?: boolean;
onDidChangeEmpty?: Event<void>;
Expand Down
24 changes: 13 additions & 11 deletions src/vs/workbench/contrib/views/browser/treeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views';
import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IProgressService } from 'vs/platform/progress/common/progress';
Expand All @@ -38,7 +38,6 @@ 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 { IMarkdownString } from 'vs/base/common/htmlContent';
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';

class Root implements ITreeItem {
Expand Down Expand Up @@ -727,7 +726,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
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 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]) => {
Expand All @@ -749,7 +748,8 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
}) : undefined;
const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark;
const iconUrl = icon ? URI.revive(icon) : null;
const title = node.tooltip ? isString(node.tooltip) ? node.tooltip : undefined : resource ? undefined : label;
const canResolve = node instanceof ResolvableTreeItem && node.hasResolve;
const title = node.tooltip ? (isString(node.tooltip) ? node.tooltip : undefined) : (resource ? undefined : (canResolve ? undefined : label));

// reset
templateData.actionBar.clear();
Expand Down Expand Up @@ -785,14 +785,14 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
const disposableStore = new DisposableStore();
templateData.elementDisposable = disposableStore;
disposableStore.add(this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node)));
this.setupHovers(node.tooltip, templateData.container, disposableStore);
this.setupHovers(node, templateData.container, disposableStore, label);
}

private setupHovers(tooltip: string | IMarkdownString | undefined, htmlElement: HTMLElement, disposableStore: DisposableStore): void {
if (!tooltip || isString(tooltip)) {
private setupHovers(node: ITreeItem, htmlElement: HTMLElement, disposableStore: DisposableStore, label: string | undefined): void {
if ((node.tooltip && isString(node.tooltip)) || !(node instanceof ResolvableTreeItem) || !node.hasResolve) {
return;
}
const text: IMarkdownString = tooltip;
const resolvableNode: ResolvableTreeItem = node;
const hoverService = this.hoverService;
const hoverDelay = this.hoverDelay;
function mouseOver(this: HTMLElement, e: MouseEvent): any {
Expand All @@ -801,9 +801,11 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
isHovering = false;
}
this.addEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave, { passive: true });
setTimeout(() => {
if (isHovering) {
hoverService.showHover({ text, target: this });
setTimeout(async () => {
await resolvableNode.resolve();
const tooltip = resolvableNode.tooltip ?? label;
if (isHovering && tooltip) {
hoverService.showHover({ text: isString(tooltip) ? { value: tooltip } : tooltip, target: this });
}
this.removeEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave);
}, hoverDelay);
Expand Down

0 comments on commit 39a406d

Please sign in to comment.