Skip to content

Commit 39a406d

Browse files
committed
Add resolveTreeItem to custom tree
Part of microsoft#100741
1 parent fe66be1 commit 39a406d

6 files changed

Lines changed: 119 additions & 15 deletions

File tree

src/vs/vscode.proposed.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,11 @@ declare module 'vscode' {
11461146

11471147
}
11481148

1149+
// https://github.com/microsoft/vscode/issues/100741
1150+
export interface TreeDataProvider<T> {
1151+
resolveTreeItem?(element: T, item: TreeItem2): TreeItem2 | Thenable<TreeItem2>;
1152+
}
1153+
11491154
export class TreeItem2 extends TreeItem {
11501155
/**
11511156
* Label describing this item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri).

src/vs/workbench/api/browser/mainThreadTreeViews.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

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

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

167168
constructor(private readonly treeViewId: string,
168169
private readonly _proxy: ExtHostTreeViewsShape,
169170
private readonly notificationService: INotificationService
170171
) {
172+
this.hasResolve = this._proxy.$hasResolve(this.treeViewId);
171173
}
172174

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

217-
private postGetChildren(elements: ITreeItem[]): ITreeItem[] {
218-
const result: ITreeItem[] = [];
219+
private async postGetChildren(elements: ITreeItem[]): Promise<ResolvableTreeItem[]> {
220+
const result: ResolvableTreeItem[] = [];
221+
const hasResolve = await this.hasResolve;
219222
if (elements) {
220223
for (const element of elements) {
224+
const resolvable = new ResolvableTreeItem(element, hasResolve ? () => {
225+
return this._proxy.$resolve(this.treeViewId, element.handle);
226+
} : undefined);
221227
this.itemsMap.set(element.handle, element);
222-
result.push(element);
228+
result.push(resolvable);
223229
}
224230
}
225231
return result;

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,8 @@ export interface ExtHostTreeViewsShape {
987987
$setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void;
988988
$setSelection(treeViewId: string, treeItemHandles: string[]): void;
989989
$setVisible(treeViewId: string, visible: boolean): void;
990+
$hasResolve(treeViewId: string): Promise<boolean>;
991+
$resolve(treeViewId: string, treeItemHandle: string): Promise<ITreeItem | undefined>;
990992
}
991993

992994
export interface ExtHostWorkspaceShape {

src/vs/workbench/api/common/extHostTreeViews.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
119119
return treeView.getChildren(treeItemHandle);
120120
}
121121

122+
async $hasResolve(treeViewId: string): Promise<boolean> {
123+
const treeView = this.treeViews.get(treeViewId);
124+
if (!treeView) {
125+
throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId));
126+
}
127+
return treeView.hasResolve;
128+
}
129+
130+
$resolve(treeViewId: string, treeItemHandle: string): Promise<ITreeItem | undefined> {
131+
const treeView = this.treeViews.get(treeViewId);
132+
if (!treeView) {
133+
throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId));
134+
}
135+
return treeView.resolveTreeItem(treeItemHandle);
136+
}
137+
122138
$setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void {
123139
const treeView = this.treeViews.get(treeViewId);
124140
if (!treeView) {
@@ -160,6 +176,7 @@ type TreeData<T> = { message: boolean, element: T | Root | false };
160176

161177
interface TreeNode extends IDisposable {
162178
item: ITreeItem;
179+
extensionItem: vscode.TreeItem2;
163180
parent: TreeNode | Root;
164181
children?: TreeNode[];
165182
}
@@ -329,6 +346,27 @@ class ExtHostTreeView<T> extends Disposable {
329346
}
330347
}
331348

349+
get hasResolve(): boolean {
350+
return !!this.dataProvider.resolveTreeItem;
351+
}
352+
353+
async resolveTreeItem(treeItemHandle: string): Promise<ITreeItem | undefined> {
354+
if (!this.dataProvider.resolveTreeItem) {
355+
return;
356+
}
357+
const element = this.elements.get(treeItemHandle);
358+
if (element) {
359+
const node = this.nodes.get(element);
360+
if (node) {
361+
const resolve = await this.dataProvider.resolveTreeItem(element, node.extensionItem);
362+
// Resolvable elements. Currently only tooltip.
363+
node.item.tooltip = resolve.tooltip;
364+
return node.item;
365+
}
366+
}
367+
return;
368+
}
369+
332370
private resolveUnknownParentChain(element: T): Promise<TreeNode[]> {
333371
return this.resolveParent(element)
334372
.then((parent) => {
@@ -521,6 +559,7 @@ class ExtHostTreeView<T> extends Disposable {
521559

522560
return {
523561
item,
562+
extensionItem: extensionTreeItem,
524563
parent,
525564
children: undefined,
526565
dispose(): void { disposable.dispose(); }

src/vs/workbench/common/views.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,56 @@ export interface ITreeItem {
648648
accessibilityInformation?: IAccessibilityInformation;
649649
}
650650

651+
export class ResolvableTreeItem implements ITreeItem {
652+
handle: string;
653+
parentHandle?: string;
654+
collapsibleState: TreeItemCollapsibleState;
655+
label?: ITreeItemLabel;
656+
description?: string | boolean;
657+
icon?: UriComponents;
658+
iconDark?: UriComponents;
659+
themeIcon?: ThemeIcon;
660+
resourceUri?: UriComponents;
661+
tooltip?: string | IMarkdownString;
662+
contextValue?: string;
663+
command?: Command;
664+
children?: ITreeItem[];
665+
accessibilityInformation?: IAccessibilityInformation;
666+
resolve: () => Promise<void>;
667+
private resolved: boolean = false;
668+
private _hasResolve: boolean = false;
669+
constructor(treeItem: ITreeItem, resolve?: (() => Promise<ITreeItem | undefined>)) {
670+
this.handle = treeItem.handle;
671+
this.parentHandle = treeItem.parentHandle;
672+
this.collapsibleState = treeItem.collapsibleState;
673+
this.label = treeItem.label;
674+
this.description = treeItem.description;
675+
this.icon = treeItem.icon;
676+
this.iconDark = treeItem.iconDark;
677+
this.themeIcon = treeItem.themeIcon;
678+
this.resourceUri = treeItem.resourceUri;
679+
this.tooltip = treeItem.tooltip;
680+
this.contextValue = treeItem.contextValue;
681+
this.command = treeItem.command;
682+
this.children = treeItem.children;
683+
this.accessibilityInformation = treeItem.accessibilityInformation;
684+
this._hasResolve = !!resolve;
685+
this.resolve = async () => {
686+
if (resolve && !this.resolved) {
687+
const resolvedItem = await resolve();
688+
if (resolvedItem) {
689+
// Resolvable elements. Currently only tooltip.
690+
this.tooltip = resolvedItem.tooltip;
691+
}
692+
}
693+
this.resolved = true;
694+
};
695+
}
696+
get hasResolve(): boolean {
697+
return this._hasResolve;
698+
}
699+
}
700+
651701
export interface ITreeViewDataProvider {
652702
readonly isTreeEmpty?: boolean;
653703
onDidChangeEmpty?: Event<void>;

src/vs/workbench/contrib/views/browser/treeView.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
1313
import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
1414
import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
1515
import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
16-
import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views';
16+
import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views';
1717
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1818
import { INotificationService } from 'vs/platform/notification/common/notification';
1919
import { IProgressService } from 'vs/platform/progress/common/progress';
@@ -38,7 +38,6 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters';
3838
import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults';
3939
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
4040
import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme';
41-
import { IMarkdownString } from 'vs/base/common/htmlContent';
4241
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
4342

4443
class Root implements ITreeItem {
@@ -727,7 +726,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
727726
templateData.elementDisposable.dispose();
728727
const node = element.element;
729728
const resource = node.resourceUri ? URI.revive(node.resourceUri) : null;
730-
const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : resource ? { label: basename(resource) } : undefined;
729+
const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : (resource ? { label: basename(resource) } : undefined);
731730
const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined;
732731
const label = treeItemLabel ? treeItemLabel.label : undefined;
733732
const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => {
@@ -749,7 +748,8 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
749748
}) : undefined;
750749
const icon = this.themeService.getColorTheme().type === LIGHT ? node.icon : node.iconDark;
751750
const iconUrl = icon ? URI.revive(icon) : null;
752-
const title = node.tooltip ? isString(node.tooltip) ? node.tooltip : undefined : resource ? undefined : label;
751+
const canResolve = node instanceof ResolvableTreeItem && node.hasResolve;
752+
const title = node.tooltip ? (isString(node.tooltip) ? node.tooltip : undefined) : (resource ? undefined : (canResolve ? undefined : label));
753753

754754
// reset
755755
templateData.actionBar.clear();
@@ -785,14 +785,14 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
785785
const disposableStore = new DisposableStore();
786786
templateData.elementDisposable = disposableStore;
787787
disposableStore.add(this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node)));
788-
this.setupHovers(node.tooltip, templateData.container, disposableStore);
788+
this.setupHovers(node, templateData.container, disposableStore, label);
789789
}
790790

791-
private setupHovers(tooltip: string | IMarkdownString | undefined, htmlElement: HTMLElement, disposableStore: DisposableStore): void {
792-
if (!tooltip || isString(tooltip)) {
791+
private setupHovers(node: ITreeItem, htmlElement: HTMLElement, disposableStore: DisposableStore, label: string | undefined): void {
792+
if ((node.tooltip && isString(node.tooltip)) || !(node instanceof ResolvableTreeItem) || !node.hasResolve) {
793793
return;
794794
}
795-
const text: IMarkdownString = tooltip;
795+
const resolvableNode: ResolvableTreeItem = node;
796796
const hoverService = this.hoverService;
797797
const hoverDelay = this.hoverDelay;
798798
function mouseOver(this: HTMLElement, e: MouseEvent): any {
@@ -801,9 +801,11 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
801801
isHovering = false;
802802
}
803803
this.addEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave, { passive: true });
804-
setTimeout(() => {
805-
if (isHovering) {
806-
hoverService.showHover({ text, target: this });
804+
setTimeout(async () => {
805+
await resolvableNode.resolve();
806+
const tooltip = resolvableNode.tooltip ?? label;
807+
if (isHovering && tooltip) {
808+
hoverService.showHover({ text: isString(tooltip) ? { value: tooltip } : tooltip, target: this });
807809
}
808810
this.removeEventListener(DOM.EventType.MOUSE_LEAVE, mouseLeave);
809811
}, hoverDelay);

0 commit comments

Comments
 (0)