Fix Azure tree view (#23584)

This commit is contained in:
Cheena Malhotra
2023-06-29 20:48:04 -07:00
committed by GitHub
parent 116270f2f6
commit fa335d32be

View File

@@ -9,10 +9,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IAction, ActionRunner } from 'vs/base/common/actions'; import { IAction, ActionRunner } from 'vs/base/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2, SubmenuItemAction, MenuRegistry, IMenu } from 'vs/platform/actions/common/actions'; import { IMenuService, MenuId, registerAction2, Action2, MenuRegistry, IMenu } from 'vs/platform/actions/common/actions';
import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { createAndFillInContextMenuActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, IViewBadge, ResolvableTreeItem, TreeCommand } from 'vs/workbench/common/views'; import { TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, IViewBadge, ResolvableTreeItem, TreeCommand, ITreeViewDragAndDropController } from 'vs/workbench/common/views';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService } from 'vs/platform/notification/common/notification'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { IProgressService } from 'vs/platform/progress/common/progress'; import { IProgressService } from 'vs/platform/progress/common/progress';
@@ -52,6 +52,14 @@ import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
import { CancellationToken } from 'vscode'; import { CancellationToken } from 'vscode';
import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { ILogService } from 'vs/platform/log/common/log';
import { CustomTreeViewDragAndDrop, RawCustomTreeViewContextKey } from 'vs/workbench/browser/parts/views/treeView';
import { setTimeout0 } from 'vs/base/common/platform';
import { Codicon } from 'vs/base/common/codicons';
import { AriaRole } from 'vs/base/browser/ui/aria/aria';
import { API_OPEN_EDITOR_COMMAND_ID, API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
class Root implements ITreeItem { class Root implements ITreeItem {
label = { label: 'root' }; label = { label: 'root' };
@@ -77,28 +85,30 @@ const noDataProviderMessage = localize('no-dataprovider', "There is no data prov
class Tree extends WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> { } class Tree extends WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> { }
export class TreeView extends Disposable implements ITreeView { abstract class AbstractTreeView extends Disposable implements ITreeView {
private isVisible: boolean = false; private isVisible: boolean = false;
private _hasIconForParentNode = false; private _hasIconForParentNode = false;
private _hasIconForLeafNode = false; private _hasIconForLeafNode = false;
private readonly collapseAllContextKey: RawContextKey<boolean>; private collapseAllContextKey: RawContextKey<boolean> | undefined;
private readonly collapseAllContext: IContextKey<boolean>; private collapseAllContext: IContextKey<boolean> | undefined;
private readonly refreshContextKey: RawContextKey<boolean>; private collapseAllToggleContextKey: RawContextKey<boolean> | undefined;
private readonly refreshContext: IContextKey<boolean>; private collapseAllToggleContext: IContextKey<boolean> | undefined;
private refreshContextKey: RawContextKey<boolean> | undefined;
private refreshContext: IContextKey<boolean> | undefined;
private focused: boolean = false; private focused: boolean = false;
private domNode!: HTMLElement; private domNode!: HTMLElement;
private treeContainer!: HTMLElement; private treeContainer: HTMLElement | undefined;
private _messageValue: string | undefined; private _messageValue: string | undefined;
private _canSelectMany: boolean = false; private _canSelectMany: boolean = false;
private messageElement!: HTMLDivElement; private _manuallyManageCheckboxes: boolean = false;
private messageElement: HTMLElement | undefined;
private tree: Tree | undefined; private tree: Tree | undefined;
private treeLabels: ResourceLabels | undefined; private treeLabels: ResourceLabels | undefined;
readonly badge: IViewBadge | undefined = undefined; private treeViewDnd: CustomTreeViewDragAndDrop | undefined;
readonly container: any | undefined = undefined; private _container: HTMLElement | undefined;
private _manuallyManageCheckboxes: boolean = false;
public readonly root: ITreeItem; public readonly root: ITreeItem;
private elementsToRefresh: ITreeItem[] = []; private elementsToRefresh: ITreeItem[] = [];
@@ -149,14 +159,31 @@ export class TreeView extends Disposable implements ITreeView {
@IKeybindingService private readonly keybindingService: IKeybindingService, @IKeybindingService private readonly keybindingService: IKeybindingService,
@INotificationService private readonly notificationService: INotificationService, @INotificationService private readonly notificationService: INotificationService,
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
@IContextKeyService contextKeyService: IContextKeyService @IHoverService private readonly hoverService: IHoverService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IActivityService private readonly activityService: IActivityService,
@ILogService private readonly logService: ILogService
) { ) {
super(); super();
this.root = new Root(); this.root = new Root();
// Try not to add anything that could be costly to this constructor. It gets called once per tree view
// during startup, and anything added here can affect performance.
}
private _isInitialized: boolean = false;
private initialize() {
if (this._isInitialized) {
return;
}
this._isInitialized = true;
// Remember when adding to this method that it isn't called until the the view is visible, meaning that
// properties could be set and events could be fired before we're initialized and that this needs to be handled.
this.collapseAllContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableCollapseAll`, false); this.collapseAllContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableCollapseAll`, false);
this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService); this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService);
this.refreshContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableRefresh`, false); this.refreshContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableRefresh`, false);
this.refreshContext = this.refreshContextKey.bindTo(contextKeyService); this.refreshContext = this.refreshContextKey.bindTo(this.contextKeyService);
this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); 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.themeService.onDidColorThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/));
@@ -166,6 +193,12 @@ export class TreeView extends Disposable implements ITreeView {
this.doRefresh([this.root]); /** soft refresh **/ this.doRefresh([this.root]); /** soft refresh **/
} }
})); }));
this.treeViewDnd = this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.id);
if (this._dragAndDropController) {
this.treeViewDnd.controller = this._dragAndDropController;
}
this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => {
if (views.some(v => v.id === this.id)) { if (views.some(v => v.id === this.id)) {
this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } }); this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } });
@@ -173,7 +206,7 @@ export class TreeView extends Disposable implements ITreeView {
})); }));
this.registerActions(); this.registerActions();
this.nodeContext = this._register(instantiationService.createInstance(NodeContextKey)); // tracked change this.nodeContext = this._register(this.instantiationService.createInstance(NodeContextKey)); // tracked change
this.create(); this.create();
} }
@@ -192,6 +225,17 @@ export class TreeView extends Disposable implements ITreeView {
return this.viewDescriptorService.getViewLocationById(this.id)!; return this.viewDescriptorService.getViewLocationById(this.id)!;
} }
private _dragAndDropController: ITreeViewDragAndDropController | undefined;
get dragAndDropController(): ITreeViewDragAndDropController | undefined {
return this._dragAndDropController;
}
set dragAndDropController(dnd: ITreeViewDragAndDropController | undefined) {
this._dragAndDropController = dnd;
if (this.treeViewDnd) {
this.treeViewDnd.controller = dnd;
}
}
private _dataProvider: ITreeViewDataProvider | undefined; private _dataProvider: ITreeViewDataProvider | undefined;
get dataProvider(): ITreeViewDataProvider | undefined { get dataProvider(): ITreeViewDataProvider | undefined {
return this._dataProvider; return this._dataProvider;
@@ -244,16 +288,6 @@ export class TreeView extends Disposable implements ITreeView {
this._onDidChangeWelcomeState.fire(); this._onDidChangeWelcomeState.fire();
} }
private _description: string | undefined;
get description(): string | undefined {
return this._description;
}
set description(_description: string | undefined) {
this._description = _description;
this._onDidChangeDescription.fire(this._description);
}
private _message: string | undefined; private _message: string | undefined;
get message(): string | undefined { get message(): string | undefined {
return this._message; return this._message;
@@ -274,12 +308,63 @@ export class TreeView extends Disposable implements ITreeView {
this._onDidChangeTitle.fire(this._title); 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);
}
private _badge: IViewBadge | undefined;
private _badgeActivity: IDisposable | undefined;
get badge(): IViewBadge | undefined {
return this._badge;
}
set badge(badge: IViewBadge | undefined) {
if (this._badge?.value === badge?.value &&
this._badge?.tooltip === badge?.tooltip) {
return;
}
if (this._badgeActivity) {
this._badgeActivity.dispose();
this._badgeActivity = undefined;
}
this._badge = badge;
if (badge) {
const activity = {
badge: new NumberBadge(badge.value, () => badge.tooltip),
priority: 50
};
this._badgeActivity = this.activityService.showViewActivity(this.id, activity);
}
}
get canSelectMany(): boolean { get canSelectMany(): boolean {
return this._canSelectMany; return this._canSelectMany;
} }
set canSelectMany(canSelectMany: boolean) { set canSelectMany(canSelectMany: boolean) {
const oldCanSelectMany = this._canSelectMany;
this._canSelectMany = canSelectMany; this._canSelectMany = canSelectMany;
if (this._canSelectMany !== oldCanSelectMany) {
this.tree?.updateOptions({ multipleSelectionSupport: this.canSelectMany });
}
}
get manuallyManageCheckboxes(): boolean {
return this._manuallyManageCheckboxes;
}
set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) {
this._manuallyManageCheckboxes = manuallyManageCheckboxes;
} }
get hasIconForParentNode(): boolean { get hasIconForParentNode(): boolean {
@@ -294,28 +379,39 @@ export class TreeView extends Disposable implements ITreeView {
return this.isVisible; return this.isVisible;
} }
private initializeShowCollapseAllAction(startingValue: boolean = false) {
if (!this.collapseAllContext) {
this.collapseAllContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableCollapseAll`, startingValue, localize('treeView.enableCollapseAll', "Whether the the tree view with id {0} enables collapse all.", this.id));
this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService);
}
return true;
}
get showCollapseAllAction(): boolean { get showCollapseAllAction(): boolean {
return !!this.collapseAllContext.get(); this.initializeShowCollapseAllAction();
return !!this.collapseAllContext?.get();
} }
set showCollapseAllAction(showCollapseAllAction: boolean) { set showCollapseAllAction(showCollapseAllAction: boolean) {
this.collapseAllContext.set(showCollapseAllAction); this.initializeShowCollapseAllAction(showCollapseAllAction);
this.collapseAllContext?.set(showCollapseAllAction);
}
private initializeShowRefreshAction(startingValue: boolean = false) {
if (!this.refreshContext) {
this.refreshContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableRefresh`, startingValue, localize('treeView.enableRefresh', "Whether the tree view with id {0} enables refresh.", this.id));
this.refreshContext = this.refreshContextKey.bindTo(this.contextKeyService);
}
} }
get showRefreshAction(): boolean { get showRefreshAction(): boolean {
return !!this.refreshContext.get(); this.initializeShowRefreshAction();
return !!this.refreshContext?.get();
} }
set showRefreshAction(showRefreshAction: boolean) { set showRefreshAction(showRefreshAction: boolean) {
this.refreshContext.set(showRefreshAction); this.initializeShowRefreshAction(showRefreshAction);
} this.refreshContext?.set(showRefreshAction);
get manuallyManageCheckboxes(): boolean {
return this._manuallyManageCheckboxes;
}
set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) {
this._manuallyManageCheckboxes = manuallyManageCheckboxes;
} }
private registerActions() { private registerActions() {
@@ -327,11 +423,11 @@ export class TreeView extends Disposable implements ITreeView {
title: localize('refresh', "Refresh"), title: localize('refresh', "Refresh"),
menu: { menu: {
id: MenuId.ViewTitle, id: MenuId.ViewTitle,
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.refreshContextKey), when: ContextKeyExpr.and(ContextKeyExpr.equals('view', that.id), that.refreshContextKey),
group: 'navigation', group: 'navigation',
order: Number.MAX_SAFE_INTEGER - 1, order: Number.MAX_SAFE_INTEGER - 1,
}, },
icon: { id: 'codicon/refresh' } icon: Codicon.refresh
}); });
} }
async run(): Promise<void> { async run(): Promise<void> {
@@ -345,11 +441,12 @@ export class TreeView extends Disposable implements ITreeView {
title: localize('collapseAll', "Collapse All"), title: localize('collapseAll', "Collapse All"),
menu: { menu: {
id: MenuId.ViewTitle, id: MenuId.ViewTitle,
when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.collapseAllContextKey), when: ContextKeyExpr.and(ContextKeyExpr.equals('view', that.id), that.collapseAllContextKey),
group: 'navigation', group: 'navigation',
order: Number.MAX_SAFE_INTEGER, order: Number.MAX_SAFE_INTEGER,
}, },
icon: { id: 'codicon/collapse-all' } precondition: that.collapseAllToggleContextKey,
icon: Codicon.collapseAll
}); });
} }
async run(): Promise<void> { async run(): Promise<void> {
@@ -361,6 +458,11 @@ export class TreeView extends Disposable implements ITreeView {
} }
setVisibility(isVisible: boolean): void { setVisibility(isVisible: boolean): void {
// Throughout setVisibility we need to check if the tree view's data provider still exists.
// This can happen because the `getChildren` call to the extension can return
// after the tree has been disposed.
this.initialize();
isVisible = !!isVisible; isVisible = !!isVisible;
if (this.isVisible === isVisible) { if (this.isVisible === isVisible) {
return; return;
@@ -382,15 +484,25 @@ export class TreeView extends Disposable implements ITreeView {
} }
} }
setTimeout0(() => {
if (this.dataProvider) {
this._onDidChangeVisibility.fire(this.isVisible); this._onDidChangeVisibility.fire(this.isVisible);
} }
});
focus(reveal: boolean = true): void { if (this.visible) {
this.activate();
}
}
protected abstract activate(): void;
focus(reveal: boolean = true, revealItem?: ITreeItem): void {
if (this.tree && this.root.children && this.root.children.length > 0) { if (this.tree && this.root.children && this.root.children.length > 0) {
// Make sure the current selected element is revealed // Make sure the current selected element is revealed
const selectedElement = this.tree.getSelection()[0]; const element = revealItem ?? this.tree.getSelection()[0];
if (selectedElement && reveal) { if (element && reveal) {
this.tree.reveal(selectedElement, 0.5); this.tree.reveal(element, 0.5);
} }
// Pass Focus to Viewer // Pass Focus to Viewer
@@ -479,15 +591,7 @@ export class TreeView extends Disposable implements ITreeView {
} }
private createTree() { private createTree() {
const actionViewItemProvider = (action: IAction) => { const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService);
if (action instanceof MenuItemAction) {
return this.instantiationService.createInstance(MenuEntryActionViewItem, action, undefined);
} else if (action instanceof SubmenuItemAction) {
return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action, undefined);
}
return undefined;
};
const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id));
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this)); this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
const dataSource = this.instantiationService.createInstance(TreeDataSource, this, this.id, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task)); const dataSource = this.instantiationService.createInstance(TreeDataSource, this, this.id, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task));
@@ -499,18 +603,34 @@ export class TreeView extends Disposable implements ITreeView {
const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler);
const widgetAriaLabel = this._title; const widgetAriaLabel = this._title;
this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer, new TreeViewDelegate(), [renderer], this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer!, new TreeViewDelegate(), [renderer],
dataSource, { dataSource, {
identityProvider: new TreeViewIdentityProvider(), identityProvider: new TreeViewIdentityProvider(),
accessibilityProvider: { accessibilityProvider: {
getAriaLabel(element: ITreeItem): string { getAriaLabel(element: ITreeItem): string | null {
if (element.accessibilityInformation) { if (element.accessibilityInformation) {
return element.accessibilityInformation.label; return element.accessibilityInformation.label;
} }
return isString(element.tooltip) ? element.tooltip : element.label ? element.label.label : ''; if (isString(element.tooltip)) {
return element.tooltip;
} else {
if (element.resourceUri && !element.label) {
// The custom tree has no good information on what should be used for the aria label.
// Allow the tree widget's default aria label to be used.
return null;
}
let buildAriaLabel: string = '';
if (element.label) {
buildAriaLabel += element.label.label + ' ';
}
if (element.description) {
buildAriaLabel += element.description;
}
return buildAriaLabel;
}
}, },
getRole(element: ITreeItem): string | undefined { getRole(element: ITreeItem): AriaRole | undefined {
return element.accessibilityInformation?.role ?? 'treeitem'; return element.accessibilityInformation?.role ?? 'treeitem';
}, },
getWidgetAriaLabel(): string { getWidgetAriaLabel(): string {
@@ -522,22 +642,33 @@ export class TreeView extends Disposable implements ITreeView {
return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined);
} }
}, },
expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command, expandOnlyOnTwistieClick: (e: ITreeItem) => {
return !!e.command || !!e.checkbox || this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick';
},
collapseByDefault: (e: ITreeItem): boolean => { collapseByDefault: (e: ITreeItem): boolean => {
return e.collapsibleState !== TreeItemCollapsibleState.Expanded; return e.collapsibleState !== TreeItemCollapsibleState.Expanded;
}, },
multipleSelectionSupport: this.canSelectMany, multipleSelectionSupport: this.canSelectMany,
dnd: this.treeViewDnd,
overrideStyles: { overrideStyles: {
listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND listBackground: this.viewLocation === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND
} }
}) as WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>); }) as WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>);
treeMenus.setContextKeyService(this.tree.contextKeyService);
aligner.tree = this.tree; aligner.tree = this.tree;
const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()); const actionRunner = new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection());
renderer.actionRunner = actionRunner; renderer.actionRunner = actionRunner;
this.tree.contextKeyService.createKey<boolean>(this.id, true); this.tree.contextKeyService.createKey<boolean>(this.id, true);
const customTreeKey = RawCustomTreeViewContextKey.bindTo(this.tree.contextKeyService);
customTreeKey.set(true);
this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner)));
this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(<any>e.elements))); this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(<any>e.elements)));
this._register(this.tree.onDidChangeFocus(e => {
if (e.elements.length) {
this._onDidChangeFocus.fire(e.elements[0]);
}
}));
this._register(this.tree.onDidChangeCollapseState(e => { this._register(this.tree.onDidChangeCollapseState(e => {
if (!e.node.element) { if (!e.node.element) {
return; return;
@@ -550,24 +681,60 @@ export class TreeView extends Disposable implements ITreeView {
this._onDidExpandItem.fire(element); this._onDidExpandItem.fire(element);
} }
})); }));
// Update resource context based on focused element // Update resource context based on focused element
this._register(this.tree.onDidChangeFocus(e => { // tracked change this._register(this.tree.onDidChangeFocus(e => { // tracked change
this.nodeContext.set({ node: e.elements[0], viewId: this.id }); this.nodeContext.set({ node: e.elements[0], viewId: this.id });
})); }));
this.tree.setInput(this.root).then(() => this.updateContentAreas()); this.tree.setInput(this.root).then(() => this.updateContentAreas());
this._register(this.tree.onDidOpen(e => { this._register(this.tree.onDidOpen(async (e) => {
if (!e.browserEvent) { if (!e.browserEvent) {
return; return;
} }
if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(TreeItemCheckbox.checkboxClass)) {
return;
}
const selection = this.tree!.getSelection(); const selection = this.tree!.getSelection();
if ((selection.length === 1) && selection[0].command) { const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined);
this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || []));
if (command && isTreeCommandEnabled(command, this.contextKeyService)) {
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];
}
try {
await this.commandService.executeCommand(command.id, ...args);
} catch (err) {
this.notificationService.error(err);
}
}
}));
this._register(treeMenus.onDidChange((changed) => {
if (this.tree?.hasNode(changed)) {
this.tree?.rerender(changed);
} }
})); }));
} }
private async resolveCommand(element: ITreeItem | undefined): Promise<TreeCommand | undefined> {
let command = element?.command;
if (element && !command) {
if ((element instanceof ResolvableTreeItem) && element.hasResolve) {
await element.resolve(new CancellationTokenSource().token);
command = element.command;
}
}
return command;
}
private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void { private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void {
this.hoverService.hideHover();
const node: ITreeItem | null = treeEvent.element; const node: ITreeItem | null = treeEvent.element;
if (node === null) { if (node === null) {
return; return;
@@ -619,9 +786,12 @@ export class TreeView extends Disposable implements ITreeView {
} }
private showMessage(message: string): void { private showMessage(message: string): void {
this._messageValue = message;
if (!this.messageElement) {
return;
}
this.messageElement.classList.remove('hide'); this.messageElement.classList.remove('hide');
this.resetMessageElement(); this.resetMessageElement();
this._messageValue = message;
if (!isFalsyOrWhitespace(this._message)) { if (!isFalsyOrWhitespace(this._message)) {
this.messageElement.textContent = this._messageValue; this.messageElement.textContent = this._messageValue;
} }
@@ -630,25 +800,25 @@ export class TreeView extends Disposable implements ITreeView {
private hideMessage(): void { private hideMessage(): void {
this.resetMessageElement(); this.resetMessageElement();
this.messageElement.classList.add('hide'); this.messageElement?.classList.add('hide');
this.layout(this._height, this._width); this.layout(this._height, this._width);
} }
private resetMessageElement(): void { private resetMessageElement(): void {
if (this.messageElement) {
DOM.clearNode(this.messageElement); DOM.clearNode(this.messageElement);
} }
}
private _height: number = 0; private _height: number = 0;
private _width: number = 0; private _width: number = 0;
layout(height: number, width: number) { layout(height: number, width: number) {
if (height && width) { if (height && width && this.messageElement && this.treeContainer) {
this._height = height; this._height = height;
this._width = width; this._width = width;
const treeHeight = height - DOM.getTotalHeight(this.messageElement); const treeHeight = height - DOM.getTotalHeight(this.messageElement);
this.treeContainer.style.height = treeHeight + 'px'; this.treeContainer.style.height = treeHeight + 'px';
if (this.tree) { this.tree?.layout(treeHeight, width);
this.tree.layout(treeHeight, width);
}
} }
} }
@@ -661,11 +831,7 @@ export class TreeView extends Disposable implements ITreeView {
return 0; return 0;
} }
isCollapsed(item: ITreeItem): boolean { async refresh(elements?: readonly ITreeItem[]): Promise<void> {
return !!this.tree?.isCollapsed(item);
}
async refresh(elements?: ITreeItem[]): Promise<void> {
if (this.dataProvider && this.tree) { if (this.dataProvider && this.tree) {
if (this.refreshing) { if (this.refreshing) {
await Event.toPromise(this._onDidCompleteRefresh.event); await Event.toPromise(this._onDidCompleteRefresh.event);
@@ -699,28 +865,40 @@ export class TreeView extends Disposable implements ITreeView {
async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> { async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> {
const tree = this.tree; const tree = this.tree;
if (tree) { if (!tree) {
return;
}
try {
itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
await Promise.all(itemOrItems.map(element => { for (const element of itemOrItems) {
return tree.expand(element, false); await tree.expand(element, false);
})); }
} catch (e) {
// The extension could have changed the tree during the reveal.
// Because of that, we ignore errors.
} }
} }
setSelection(items: ITreeItem[]): void { isCollapsed(item: ITreeItem): boolean {
if (this.tree) { return !!this.tree?.isCollapsed(item);
this.tree.setSelection(items);
} }
setSelection(items: ITreeItem[]): void {
this.tree?.setSelection(items);
} }
getSelection(): ITreeItem[] { getSelection(): ITreeItem[] {
return this.tree?.getSelection() ?? []; return this.tree?.getSelection() ?? [];
} }
setFocus(item: ITreeItem): void { setFocus(item?: ITreeItem): void {
if (this.tree) { if (this.tree) {
this.focus(); if (item) {
this.focus(true, item);
this.tree.setFocus([item]); this.tree.setFocus([item]);
} else {
this.tree.setFocus([]);
}
} }
} }
@@ -731,31 +909,66 @@ export class TreeView extends Disposable implements ITreeView {
} }
private refreshing: boolean = false; private refreshing: boolean = false;
private async doRefresh(elements: ITreeItem[]): Promise<void> { private async doRefresh(elements: readonly ITreeItem[]): Promise<void> {
const tree = this.tree; const tree = this.tree;
if (tree && this.visible) { if (tree && this.visible) {
this.refreshing = true; this.refreshing = true;
const oldSelection = tree.getSelection();
try {
await Promise.all(elements.map(element => tree.updateChildren(element, true, true))); await Promise.all(elements.map(element => tree.updateChildren(element, true, true)));
} catch (e) {
// When multiple calls are made to refresh the tree in quick succession,
// we can get a "Tree element not found" error. This is expected.
// Ideally this is fixable, so log instead of ignoring so the error is preserved.
this.logService.error(e);
}
const newSelection = tree.getSelection();
if (oldSelection.length !== newSelection.length || oldSelection.some((value, index) => value.handle !== newSelection[index].handle)) {
this._onDidChangeSelection.fire(newSelection);
}
this.refreshing = false; this.refreshing = false;
this._onDidCompleteRefresh.fire(); this._onDidCompleteRefresh.fire();
this.updateContentAreas(); this.updateContentAreas();
if (this.focused) { if (this.focused) {
this.focus(false); this.focus(false);
} }
this.updateCollapseAllToggle();
}
}
private initializeCollapseAllToggle() {
if (!this.collapseAllToggleContext) {
this.collapseAllToggleContextKey = new RawContextKey<boolean>(`treeView.${this.id}.toggleCollapseAll`, false, localize('treeView.toggleCollapseAll', "Whether collapse all is toggled for the tree view with id {0}.", this.id));
this.collapseAllToggleContext = this.collapseAllToggleContextKey.bindTo(this.contextKeyService);
}
}
private updateCollapseAllToggle() {
if (this.showCollapseAllAction) {
this.initializeCollapseAllToggle();
this.collapseAllToggleContext?.set(!!this.root.children && (this.root.children.length > 0) &&
this.root.children.some(value => value.collapsibleState !== TreeItemCollapsibleState.None));
} }
} }
private updateContentAreas(): void { private updateContentAreas(): void {
const isTreeEmpty = !this.root.children || this.root.children.length === 0; 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 // Hide tree container only when there is a message and tree is empty and not refreshing
if (this._messageValue && isTreeEmpty && !this.refreshing) { if (this._messageValue && isTreeEmpty && !this.refreshing && this.treeContainer) {
// If there's a dnd controller then hiding the tree prevents it from being dragged into.
if (!this.dragAndDropController) {
this.treeContainer.classList.add('hide'); this.treeContainer.classList.add('hide');
}
this.domNode.setAttribute('tabindex', '0'); this.domNode.setAttribute('tabindex', '0');
} else { } else if (this.treeContainer) {
this.treeContainer.classList.remove('hide'); this.treeContainer.classList.remove('hide');
this.domNode.removeAttribute('tabindex'); this.domNode.removeAttribute('tabindex');
} }
} }
get container(): HTMLElement | undefined {
return this._container;
}
} }
class TreeViewIdentityProvider implements IIdentityProvider<ITreeItem> { class TreeViewIdentityProvider implements IIdentityProvider<ITreeItem> {
@@ -938,10 +1151,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void { renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {
const node = element.element; const node = element.element;
const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; 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 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 label = treeItemLabel ? treeItemLabel.label : undefined;
const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => { const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => {
if ((Math.abs(start) > label.length) || (Math.abs(end) >= label.length)) {
return ({ start: 0, end: 0 });
}
if (start < 0) { if (start < 0) {
start = label.length + start; start = label.length + start;
} }
@@ -958,9 +1174,11 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
} }
return ({ start, end }); return ({ start, end });
}) : undefined; }) : undefined;
const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark; const isLightTheme = [ColorScheme.LIGHT, ColorScheme.HIGH_CONTRAST_LIGHT].includes(this.themeService.getColorTheme().type);
const iconUrl = icon ? URI.revive(icon) : undefined; const icon = isLightTheme ? node.icon : node.iconDark;
const iconUrl = icon ? URI.revive(icon) : null;
const title = this.getHover(label, resource, node); const title = this.getHover(label, resource, node);
const sqlIcon = node.sqlIcon;
// reset // reset
templateData.actionBar.clear(); templateData.actionBar.clear();
@@ -972,6 +1190,23 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
} }
this.renderCheckbox(node, templateData); this.renderCheckbox(node, templateData);
if (resource || this.isFileKindThemeIcon(node.themeIcon)) {
const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations');
templateData.resourceLabel.setResource({
name: label, description,
resource: resource ? resource : URI.parse('missing:_icon_resource')
},
{
fileKind: this.getFileKind(node),
title,
hideIcon: !!iconUrl,
fileDecorations,
extraClasses: ['custom-view-tree-node-item-resourceLabel'],
matches: matches ? matches : createMatches(element.filterData)
});
} else {
templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: matches ? matches : createMatches(element.filterData) });
}
if (resource) { if (resource) {
const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations'); const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');
@@ -999,14 +1234,19 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
}); });
} }
if (iconUrl) { if (iconUrl || sqlIcon) {
templateData.icon.title = title ? title.toString() : '';
templateData.icon.className = 'custom-view-tree-node-item-icon'; templateData.icon.className = 'custom-view-tree-node-item-icon';
templateData.icon.style.backgroundImage = DOM.asCSSUrl(iconUrl); if (sqlIcon) {
templateData.icon.classList.toggle(sqlIcon, !!sqlIcon); // tracked change
}
templateData.icon.classList.toggle('icon', !!sqlIcon);
templateData.icon.style.backgroundImage = iconUrl ? DOM.asCSSUrl(iconUrl) : '';
} else { } else {
let iconClass: string | undefined; let iconClass: string | undefined;
if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) { if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) {
iconClass = ThemeIcon.asClassName(node.themeIcon); iconClass = ThemeIcon.asClassName(node.themeIcon);
if (node.themeIcon.color) { if (node.themeIcon && !this.isFileKindThemeIcon(node.themeIcon)) {
templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? ''; templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? '';
} }
} }
@@ -1022,12 +1262,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
} }
templateData.actionBar.context = <TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; templateData.actionBar.context = <TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle };
templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false });
const menuActions = this.menus.getResourceActions(node);
if (menuActions.menu) {
templateData.elementDisposable.add(menuActions.menu);
}
templateData.actionBar.push(menuActions.actions, { icon: true, label: false });
if (this._actionRunner) { if (this._actionRunner) {
templateData.actionBar.actionRunner = this._actionRunner; templateData.actionBar.actionRunner = this._actionRunner;
@@ -1216,7 +1451,6 @@ class TreeMenus extends Disposable implements IDisposable {
private contextKeyService: IContextKeyService | undefined; private contextKeyService: IContextKeyService | undefined;
private _onDidChange = new Emitter<ITreeItem>(); private _onDidChange = new Emitter<ITreeItem>();
public readonly onDidChange = this._onDidChange.event; public readonly onDidChange = this._onDidChange.event;
constructor( constructor(
private id: string, private id: string,
@IMenuService private readonly menuService: IMenuService @IMenuService private readonly menuService: IMenuService
@@ -1224,27 +1458,32 @@ class TreeMenus extends Disposable implements IDisposable {
super(); super();
} }
/** getResourceActions(element: ITreeItem): IAction[] {
* Caller is now responsible for disposing of the menu! return this.mergeActions([ // tracked change
*/ this.getActions(MenuId.ViewItemContext, element, true).primary,
getResourceActions(element: ITreeItem): { menu?: IMenu; actions: IAction[] } { this.getActions(MenuId.DataExplorerContext, element, true).primary
const actions = this.getActions(MenuId.ViewItemContext, element, true); ]);
return { menu: actions.menu, actions: actions.primary };
} }
getResourceContextActions(element: ITreeItem): IAction[] { getResourceContextActions(element: ITreeItem): IAction[] {
return this.getActions(MenuId.ViewItemContext, element).secondary; return this.mergeActions([ // tracked change
this.getActions(MenuId.ViewItemContext, element).secondary,
this.getActions(MenuId.DataExplorerContext, element).secondary
]);
} }
public setContextKeyService(service: IContextKeyService) { public setContextKeyService(service: IContextKeyService) {
this.contextKeyService = service; this.contextKeyService = service;
} }
private mergeActions(actions: IAction[][]): IAction[] {
return actions.reduce((p, c) => p.concat(...c.filter(a => p.findIndex(x => x.id === a.id) === -1)), [] as IAction[]);
}
private getActions(menuId: MenuId, element: ITreeItem, listen: boolean = false): { menu?: IMenu; primary: IAction[]; secondary: IAction[] } { private getActions(menuId: MenuId, element: ITreeItem, listen: boolean = false): { menu?: IMenu; primary: IAction[]; secondary: IAction[] } {
if (!this.contextKeyService) { if (!this.contextKeyService) {
return { primary: [], secondary: [] }; return { primary: [], secondary: [] };
} }
const contextKeyService = this.contextKeyService.createOverlay([ const contextKeyService = this.contextKeyService.createOverlay([
['view', this.id], ['view', this.id],
['viewItem', element.contextValue] ['viewItem', element.contextValue]
@@ -1269,7 +1508,7 @@ class TreeMenus extends Disposable implements IDisposable {
} }
} }
export class CustomTreeView extends TreeView { export class CustomTreeView extends AbstractTreeView {
private activated: boolean = false; private activated: boolean = false;
@@ -1286,9 +1525,12 @@ export class CustomTreeView extends TreeView {
@INotificationService notificationService: INotificationService, @INotificationService notificationService: INotificationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IContextKeyService contextKeyService: IContextKeyService, @IContextKeyService contextKeyService: IContextKeyService,
@IHoverService hoverService: IHoverService,
@IExtensionService private readonly extensionService: IExtensionService, @IExtensionService private readonly extensionService: IExtensionService,
@IActivityService activityService: IActivityService,
@ILogService logService: ILogService,
) { ) {
super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, contextKeyService); super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService);
} }
override setVisibility(isVisible: boolean): void { override setVisibility(isVisible: boolean): void {
@@ -1298,7 +1540,7 @@ export class CustomTreeView extends TreeView {
} }
} }
private activate() { protected activate() {
if (!this.activated) { if (!this.activated) {
this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))
.then(() => timeout(2000)) .then(() => timeout(2000))