From fa335d32be3f7a328776e3a92fa39ae0a30f4987 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Thu, 29 Jun 2023 20:48:04 -0700 Subject: [PATCH] Fix Azure tree view (#23584) --- .../contrib/views/browser/treeView.ts | 478 +++++++++++++----- 1 file changed, 360 insertions(+), 118 deletions(-) diff --git a/src/sql/workbench/contrib/views/browser/treeView.ts b/src/sql/workbench/contrib/views/browser/treeView.ts index 79aa58760b..30b8c9134c 100644 --- a/src/sql/workbench/contrib/views/browser/treeView.ts +++ b/src/sql/workbench/contrib/views/browser/treeView.ts @@ -9,10 +9,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IAction, ActionRunner } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2, SubmenuItemAction, MenuRegistry, IMenu } from 'vs/platform/actions/common/actions'; -import { MenuEntryActionViewItem, createAndFillInContextMenuActions, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, 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 { IMenuService, MenuId, registerAction2, Action2, MenuRegistry, IMenu } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +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 { INotificationService } from 'vs/platform/notification/common/notification'; 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 { CancellationToken } from 'vscode'; 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 { label = { label: 'root' }; @@ -77,28 +85,30 @@ const noDataProviderMessage = localize('no-dataprovider', "There is no data prov class Tree extends WorkbenchAsyncDataTree { } -export class TreeView extends Disposable implements ITreeView { +abstract class AbstractTreeView extends Disposable implements ITreeView { private isVisible: boolean = false; private _hasIconForParentNode = false; private _hasIconForLeafNode = false; - private readonly collapseAllContextKey: RawContextKey; - private readonly collapseAllContext: IContextKey; - private readonly refreshContextKey: RawContextKey; - private readonly refreshContext: IContextKey; + private collapseAllContextKey: RawContextKey | undefined; + private collapseAllContext: IContextKey | undefined; + private collapseAllToggleContextKey: RawContextKey | undefined; + private collapseAllToggleContext: IContextKey | undefined; + private refreshContextKey: RawContextKey | undefined; + private refreshContext: IContextKey | undefined; private focused: boolean = false; private domNode!: HTMLElement; - private treeContainer!: HTMLElement; + private treeContainer: HTMLElement | undefined; private _messageValue: string | undefined; private _canSelectMany: boolean = false; - private messageElement!: HTMLDivElement; + private _manuallyManageCheckboxes: boolean = false; + private messageElement: HTMLElement | undefined; private tree: Tree | undefined; private treeLabels: ResourceLabels | undefined; - readonly badge: IViewBadge | undefined = undefined; - readonly container: any | undefined = undefined; - private _manuallyManageCheckboxes: boolean = false; + private treeViewDnd: CustomTreeViewDragAndDrop | undefined; + private _container: HTMLElement | undefined; public readonly root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; @@ -149,14 +159,31 @@ export class TreeView extends Disposable implements ITreeView { @IKeybindingService private readonly keybindingService: IKeybindingService, @INotificationService private readonly notificationService: INotificationService, @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(); 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(`treeView.${this.id}.enableCollapseAll`, false); - this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService); + this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService); this.refreshContextKey = new RawContextKey(`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.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.treeViewDnd = this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.id); + if (this._dragAndDropController) { + this.treeViewDnd.controller = this._dragAndDropController; + } + 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 } }); @@ -173,7 +206,7 @@ export class TreeView extends Disposable implements ITreeView { })); this.registerActions(); - this.nodeContext = this._register(instantiationService.createInstance(NodeContextKey)); // tracked change + this.nodeContext = this._register(this.instantiationService.createInstance(NodeContextKey)); // tracked change this.create(); } @@ -192,6 +225,17 @@ export class TreeView extends Disposable implements ITreeView { 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; get dataProvider(): ITreeViewDataProvider | undefined { return this._dataProvider; @@ -244,16 +288,6 @@ export class TreeView extends Disposable implements ITreeView { 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; get message(): string | undefined { return this._message; @@ -274,12 +308,63 @@ export class TreeView extends Disposable implements ITreeView { 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 { return this._canSelectMany; } set canSelectMany(canSelectMany: boolean) { + const oldCanSelectMany = this._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 { @@ -294,28 +379,39 @@ export class TreeView extends Disposable implements ITreeView { return this.isVisible; } + private initializeShowCollapseAllAction(startingValue: boolean = false) { + if (!this.collapseAllContext) { + this.collapseAllContextKey = new RawContextKey(`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 { - return !!this.collapseAllContext.get(); + this.initializeShowCollapseAllAction(); + return !!this.collapseAllContext?.get(); } 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(`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 { - return !!this.refreshContext.get(); + this.initializeShowRefreshAction(); + return !!this.refreshContext?.get(); } set showRefreshAction(showRefreshAction: boolean) { - this.refreshContext.set(showRefreshAction); - } - - get manuallyManageCheckboxes(): boolean { - return this._manuallyManageCheckboxes; - } - - set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) { - this._manuallyManageCheckboxes = manuallyManageCheckboxes; + this.initializeShowRefreshAction(showRefreshAction); + this.refreshContext?.set(showRefreshAction); } private registerActions() { @@ -327,11 +423,11 @@ export class TreeView extends Disposable implements ITreeView { title: localize('refresh', "Refresh"), menu: { 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', order: Number.MAX_SAFE_INTEGER - 1, }, - icon: { id: 'codicon/refresh' } + icon: Codicon.refresh }); } async run(): Promise { @@ -345,11 +441,12 @@ export class TreeView extends Disposable implements ITreeView { title: localize('collapseAll', "Collapse All"), menu: { 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', order: Number.MAX_SAFE_INTEGER, }, - icon: { id: 'codicon/collapse-all' } + precondition: that.collapseAllToggleContextKey, + icon: Codicon.collapseAll }); } async run(): Promise { @@ -361,6 +458,11 @@ export class TreeView extends Disposable implements ITreeView { } 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; if (this.isVisible === isVisible) { return; @@ -382,15 +484,25 @@ export class TreeView extends Disposable implements ITreeView { } } - this._onDidChangeVisibility.fire(this.isVisible); + setTimeout0(() => { + if (this.dataProvider) { + this._onDidChangeVisibility.fire(this.isVisible); + } + }); + + if (this.visible) { + this.activate(); + } } - focus(reveal: boolean = true): void { + protected abstract activate(): void; + + focus(reveal: boolean = true, revealItem?: ITreeItem): 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); + const element = revealItem ?? this.tree.getSelection()[0]; + if (element && reveal) { + this.tree.reveal(element, 0.5); } // Pass Focus to Viewer @@ -479,15 +591,7 @@ export class TreeView extends Disposable implements ITreeView { } private createTree() { - const actionViewItemProvider = (action: IAction) => { - 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 actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService); 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, this.id, (task: Promise) => 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 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, { identityProvider: new TreeViewIdentityProvider(), accessibilityProvider: { - getAriaLabel(element: ITreeItem): string { + getAriaLabel(element: ITreeItem): string | null { if (element.accessibilityInformation) { 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'; }, 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); } }, - 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 => { return e.collapsibleState !== TreeItemCollapsibleState.Expanded; }, multipleSelectionSupport: this.canSelectMany, + dnd: this.treeViewDnd, overrideStyles: { - listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND + listBackground: this.viewLocation === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_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); + 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.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + this._register(this.tree.onDidChangeFocus(e => { + if (e.elements.length) { + this._onDidChangeFocus.fire(e.elements[0]); + } + })); this._register(this.tree.onDidChangeCollapseState(e => { if (!e.node.element) { return; @@ -550,24 +681,60 @@ export class TreeView extends Disposable implements ITreeView { this._onDidExpandItem.fire(element); } })); + // Update resource context based on focused element this._register(this.tree.onDidChangeFocus(e => { // tracked change this.nodeContext.set({ node: e.elements[0], viewId: this.id }); })); + this.tree.setInput(this.root).then(() => this.updateContentAreas()); - this._register(this.tree.onDidOpen(e => { + this._register(this.tree.onDidOpen(async (e) => { if (!e.browserEvent) { return; } + if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(TreeItemCheckbox.checkboxClass)) { + return; + } const selection = this.tree!.getSelection(); - if ((selection.length === 1) && selection[0].command) { - this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || [])); + const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined); + + 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 { + 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, actionRunner: MultipleSelectionActionRunner): void { + this.hoverService.hideHover(); const node: ITreeItem | null = treeEvent.element; if (node === null) { return; @@ -619,9 +786,12 @@ export class TreeView extends Disposable implements ITreeView { } private showMessage(message: string): void { + this._messageValue = message; + if (!this.messageElement) { + return; + } this.messageElement.classList.remove('hide'); this.resetMessageElement(); - this._messageValue = message; if (!isFalsyOrWhitespace(this._message)) { this.messageElement.textContent = this._messageValue; } @@ -630,25 +800,25 @@ export class TreeView extends Disposable implements ITreeView { private hideMessage(): void { this.resetMessageElement(); - this.messageElement.classList.add('hide'); + this.messageElement?.classList.add('hide'); this.layout(this._height, this._width); } private resetMessageElement(): void { - DOM.clearNode(this.messageElement); + if (this.messageElement) { + DOM.clearNode(this.messageElement); + } } private _height: number = 0; private _width: number = 0; layout(height: number, width: number) { - if (height && width) { + if (height && width && this.messageElement && this.treeContainer) { 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); - } + this.tree?.layout(treeHeight, width); } } @@ -661,11 +831,7 @@ export class TreeView extends Disposable implements ITreeView { return 0; } - isCollapsed(item: ITreeItem): boolean { - return !!this.tree?.isCollapsed(item); - } - - async refresh(elements?: ITreeItem[]): Promise { + async refresh(elements?: readonly ITreeItem[]): Promise { if (this.dataProvider && this.tree) { if (this.refreshing) { await Event.toPromise(this._onDidCompleteRefresh.event); @@ -699,28 +865,40 @@ export class TreeView extends Disposable implements ITreeView { async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { const tree = this.tree; - if (tree) { + if (!tree) { + return; + } + try { itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - await Promise.all(itemOrItems.map(element => { - return tree.expand(element, false); - })); + for (const element of itemOrItems) { + await tree.expand(element, false); + } + } catch (e) { + // The extension could have changed the tree during the reveal. + // Because of that, we ignore errors. } } + isCollapsed(item: ITreeItem): boolean { + return !!this.tree?.isCollapsed(item); + } + setSelection(items: ITreeItem[]): void { - if (this.tree) { - this.tree.setSelection(items); - } + this.tree?.setSelection(items); } getSelection(): ITreeItem[] { return this.tree?.getSelection() ?? []; } - setFocus(item: ITreeItem): void { + setFocus(item?: ITreeItem): void { if (this.tree) { - this.focus(); - this.tree.setFocus([item]); + if (item) { + this.focus(true, 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 async doRefresh(elements: ITreeItem[]): Promise { + private async doRefresh(elements: readonly ITreeItem[]): Promise { const tree = this.tree; if (tree && this.visible) { this.refreshing = true; - await Promise.all(elements.map(element => tree.updateChildren(element, true, true))); + const oldSelection = tree.getSelection(); + try { + 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._onDidCompleteRefresh.fire(); this.updateContentAreas(); if (this.focused) { this.focus(false); } + this.updateCollapseAllToggle(); + } + } + + private initializeCollapseAllToggle() { + if (!this.collapseAllToggleContext) { + this.collapseAllToggleContextKey = new RawContextKey(`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 { 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'); + 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.domNode.setAttribute('tabindex', '0'); - } else { + } else if (this.treeContainer) { this.treeContainer.classList.remove('hide'); this.domNode.removeAttribute('tabindex'); } } + + get container(): HTMLElement | undefined { + return this._container; + } } class TreeViewIdentityProvider implements IIdentityProvider { @@ -938,10 +1151,13 @@ class TreeRenderer extends Disposable implements ITreeRenderer, index: number, templateData: ITreeExplorerTemplateData): void { 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]) => { + if ((Math.abs(start) > label.length) || (Math.abs(end) >= label.length)) { + return ({ start: 0, end: 0 }); + } if (start < 0) { start = label.length + start; } @@ -958,9 +1174,11 @@ class TreeRenderer extends Disposable implements ITreeRenderer('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) { const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations'); @@ -999,14 +1234,19 @@ class TreeRenderer extends Disposable implements ITreeRenderer{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; - - const menuActions = this.menus.getResourceActions(node); - if (menuActions.menu) { - templateData.elementDisposable.add(menuActions.menu); - } - templateData.actionBar.push(menuActions.actions, { icon: true, label: false }); + templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); if (this._actionRunner) { templateData.actionBar.actionRunner = this._actionRunner; @@ -1216,7 +1451,6 @@ class TreeMenus extends Disposable implements IDisposable { private contextKeyService: IContextKeyService | undefined; private _onDidChange = new Emitter(); public readonly onDidChange = this._onDidChange.event; - constructor( private id: string, @IMenuService private readonly menuService: IMenuService @@ -1224,27 +1458,32 @@ class TreeMenus extends Disposable implements IDisposable { super(); } - /** - * Caller is now responsible for disposing of the menu! - */ - getResourceActions(element: ITreeItem): { menu?: IMenu; actions: IAction[] } { - const actions = this.getActions(MenuId.ViewItemContext, element, true); - return { menu: actions.menu, actions: actions.primary }; + getResourceActions(element: ITreeItem): IAction[] { + return this.mergeActions([ // tracked change + this.getActions(MenuId.ViewItemContext, element, true).primary, + this.getActions(MenuId.DataExplorerContext, element, true).primary + ]); } 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) { 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[] } { if (!this.contextKeyService) { return { primary: [], secondary: [] }; } - const contextKeyService = this.contextKeyService.createOverlay([ ['view', this.id], ['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; @@ -1286,9 +1525,12 @@ export class CustomTreeView extends TreeView { @INotificationService notificationService: INotificationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextKeyService contextKeyService: IContextKeyService, + @IHoverService hoverService: IHoverService, @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 { @@ -1298,7 +1540,7 @@ export class CustomTreeView extends TreeView { } } - private activate() { + protected activate() { if (!this.activated) { this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000))