From 35597c921ab03be88248fbca640b3358f0bd9ef2 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:19:22 -0700 Subject: [PATCH] Bring parity with vs treeview for all non tracked code. (#23598) --- src/sql/workbench/common/views.ts | 2 +- .../contrib/views/browser/treeView.ts | 178 +++++++----------- 2 files changed, 73 insertions(+), 107 deletions(-) diff --git a/src/sql/workbench/common/views.ts b/src/sql/workbench/common/views.ts index 96a59b466c..4e60793891 100644 --- a/src/sql/workbench/common/views.ts +++ b/src/sql/workbench/common/views.ts @@ -38,7 +38,7 @@ export interface ITreeItem extends vsITreeItem { export interface ITreeView extends vsITreeView { collapse(element: ITreeItem): boolean - readonly onDidChangeSelection: Event; + readonly onDidChangeSelection: Event; } export type TreeViewItemHandleArg = { diff --git a/src/sql/workbench/contrib/views/browser/treeView.ts b/src/sql/workbench/contrib/views/browser/treeView.ts index 30b8c9134c..81b9f0cfb3 100644 --- a/src/sql/workbench/contrib/views/browser/treeView.ts +++ b/src/sql/workbench/contrib/views/browser/treeView.ts @@ -12,7 +12,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView 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 { TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, IViewDescriptorService, ViewContainer, ViewContainerLocation, IViewBadge, ResolvableTreeItem, TreeCommand, ITreeViewDragAndDropController, TreeViewPaneHandleArg } 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'; @@ -23,12 +23,11 @@ import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { URI } from 'vs/base/common/uri'; import { dirname, basename } from 'vs/base/common/resources'; -import { FileThemeIcon, FolderThemeIcon, registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; +import { FileThemeIcon, FolderThemeIcon, IThemeService } from 'vs/platform/theme/common/themeService'; import { FileKind } from 'vs/platform/files/common/files'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { localize } from 'vs/nls'; import { timeout } from 'vs/base/common/async'; -import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMatchHighlight, listFilterMatchHighlightBorder } from 'vs/platform/theme/common/colorRegistry'; import { isString } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; @@ -60,6 +59,7 @@ 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'; +import { isCancellationError } from 'vs/base/common/errors'; class Root implements ITreeItem { label = { label: 'root' }; @@ -119,8 +119,11 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private readonly _onDidCollapseItem: Emitter = this._register(new Emitter()); readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; - private _onDidChangeSelection: Emitter = this._register(new Emitter()); - readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; + private _onDidChangeSelection: Emitter = this._register(new Emitter()); + readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; + + private _onDidChangeFocus: Emitter = this._register(new Emitter()); + readonly onDidChangeFocus: Event = this._onDidChangeFocus.event; private readonly _onDidChangeVisibility: Emitter = this._register(new Emitter()); readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; @@ -140,9 +143,6 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private readonly _onDidChangeCheckboxState: Emitter = this._register(new Emitter()); readonly onDidChangeCheckboxState: Event = this._onDidChangeCheckboxState.event; - private _onDidChangeFocus: Emitter = this._register(new Emitter()); - readonly onDidChangeFocus: Event = this._onDidChangeFocus.event; - private readonly _onDidCompleteRefresh: Emitter = this._register(new Emitter()); private nodeContext: NodeContextKey; @@ -180,28 +180,26 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { // 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(this.contextKeyService); - this.refreshContextKey = new RawContextKey(`treeView.${this.id}.enableRefresh`, false); - 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 **/)); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('explorer.decorations')) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.doRefresh([this.root]); /** soft refresh **/ - } - })); + this.contextKeyService.bufferChangeEvents(() => { + this.initializeShowCollapseAllAction(); + this.initializeCollapseAllToggle(); + this.initializeShowRefreshAction(); + }); this.treeViewDnd = this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.id); if (this._dragAndDropController) { this.treeViewDnd.controller = this._dragAndDropController; } + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('explorer.decorations')) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.doRefresh([this.root]); /** soft refresh **/ + } + })); this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => { if (views.some(v => v.id === this.id)) { - this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } }); + this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND } }); } })); this.registerActions(); @@ -224,7 +222,6 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { get viewLocation(): ViewContainerLocation { return this.viewDescriptorService.getViewLocationById(this.id)!; } - private _dragAndDropController: ITreeViewDragAndDropController | undefined; get dragAndDropController(): ITreeViewDragAndDropController | undefined { return this._dragAndDropController; @@ -242,11 +239,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } set dataProvider(dataProvider: ITreeViewDataProvider | undefined) { - if (this.tree === undefined) { - this.createTree(); - } - if (dataProvider) { + const self = this; this._dataProvider = new class implements ITreeViewDataProvider { private _isEmpty: boolean = true; private _onDidChangeEmpty: Emitter = new Emitter(); @@ -256,26 +250,31 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { return this._isEmpty; } - async getChildren(node: ITreeItem): Promise { - let children: ITreeItem[] | undefined = undefined; + async getChildren(node?: ITreeItem): Promise { + let children: ITreeItem[]; if (node && node.children) { children = node.children; } else { - children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); - node.children = children; + node = node ?? self.root; + node.children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); + children = node.children ?? []; + children.forEach(child => child.parent = node); } if (node instanceof Root) { const oldEmpty = this._isEmpty; - this._isEmpty = !children || children.length === 0; + this._isEmpty = children.length === 0; if (oldEmpty !== this._isEmpty) { this._onDidChangeEmpty.fire(); } } - return children ?? []; + return children; } }; if (this._dataProvider.onDidChangeEmpty) { - this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire())); + this._register(this._dataProvider.onDidChangeEmpty(() => { + this.updateCollapseAllToggle(); + this._onDidChangeWelcomeState.fire(); + })); } this.updateMessage(); // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -397,6 +396,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { 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)); @@ -477,7 +477,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it } - if (this.isVisible && this.elementsToRefresh.length) { + if (this.isVisible && this.elementsToRefresh.length && this.dataProvider) { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.doRefresh(this.elementsToRefresh); this.elementsToRefresh = []; @@ -515,15 +515,16 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } show(container: HTMLElement): void { + this._container = container; DOM.append(container, this.domNode); } private create() { this.domNode = DOM.$('.tree-explorer-viewlet-tree-view'); this.messageElement = DOM.append(this.domNode, DOM.$('.message')); + this.updateMessage(); this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree')); - this.treeContainer.classList.add('file-icon-themable-tree'); - this.treeContainer.classList.add('show-file-icons'); + this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); const focusTracker = this._register(DOM.trackFocus(this.domNode)); this._register(focusTracker.onDidFocus(() => this.focused = true)); this._register(focusTracker.onDidBlur(() => this.focused = false)); @@ -590,7 +591,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this._onDidChangeCheckboxState.fire(items); } - private createTree() { + + protected createTree() { 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)); @@ -663,7 +665,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { 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.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); this._register(this.tree.onDidChangeFocus(e => { if (e.elements.length) { this._onDidChangeFocus.fire(e.elements[0]); @@ -745,7 +747,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { event.stopPropagation(); this.tree!.setFocus([node]); - const actions = treeMenus.getResourceContextActions(node); + const actions = treeMenus.getResourceContextActions(node).actions; if (!actions.length) { return; } @@ -972,7 +974,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } class TreeViewIdentityProvider implements IIdentityProvider { - getId(element: ITreeItem): { toString(): string; } { + getId(element: ITreeItem): { toString(): string } { return element.handle; } } @@ -1030,33 +1032,6 @@ class TreeDataSource implements IAsyncDataSource { } } -// todo@joh,sandy make this proper and contributable from extensions -registerThemingParticipant((theme, collector) => { - - const matchBackgroundColor = theme.getColor(listFilterMatchHighlight); - if (matchBackgroundColor) { - collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); - collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${matchBackgroundColor}; }`); - } - const matchBorderColor = theme.getColor(listFilterMatchHighlightBorder); - if (matchBorderColor) { - collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); - collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${matchBorderColor}; box-sizing: border-box; }`); - } - const link = theme.getColor(textLinkForeground); - if (link) { - collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`); - } - const focusBorderColor = theme.getColor(focusBorder); - if (focusBorderColor) { - collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focusBorderColor}; outline-offset: -1px; }`); - } - const codeBackground = theme.getColor(textCodeBlockBackground); - if (codeBackground) { - collector.addRule(`.tree-explorer-viewlet-tree-view > .message code { background-color: ${codeBackground}; }`); - } -}); - interface ITreeExplorerTemplateData { readonly elementDisposable: DisposableStore; readonly container: HTMLElement; @@ -1076,7 +1051,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer, ITreeExplorerTemplateData>(); - constructor( private treeViewId: string, private menus: TreeMenus, @@ -1151,7 +1125,7 @@ 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]) => { @@ -1174,6 +1148,7 @@ 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'); @@ -1234,7 +1192,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer{ $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.menus) { + menuActions.menus.map(menu => templateData.elementDisposable.add(menu)); + } + templateData.actionBar.push(menuActions.actions, { icon: true, label: false }); if (this._actionRunner) { templateData.actionBar.actionRunner = this._actionRunner; @@ -1389,7 +1352,7 @@ class Aligner extends Disposable { if (this._tree) { const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); if (this.hasIcon(parent)) { - return false; + return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIcon(c)); } return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); } else { @@ -1398,8 +1361,7 @@ class Aligner extends Disposable { } private hasIcon(node: ITreeItem): boolean { - const isLightTheme = [ColorScheme.LIGHT, ColorScheme.HIGH_CONTRAST_LIGHT].includes(this.themeService.getColorTheme().type); - const icon = isLightTheme ? node.icon : node.iconDark; + const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark; if (icon) { return true; } @@ -1420,19 +1382,19 @@ class MultipleSelectionActionRunner extends ActionRunner { constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) { super(); this._register(this.onDidRun(e => { - if (e.error) { + if (e.error && !isCancellationError(e.error)) { notificationService.error(localize('command-error', 'Error running command {1}: {0}. This is likely caused by the extension that contributes {1}.', e.error.message, e.action.id)); } })); } - protected override async runAction(action: IAction, context: TreeViewItemHandleArg): Promise { + protected override async runAction(action: IAction, context: TreeViewItemHandleArg | TreeViewPaneHandleArg): Promise { const selection = this.getSelectedResources(); let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined; let actionInSelected: boolean = false; if (selection.length > 1) { selectionHandleArgs = selection.map(selected => { - if (selected.handle === context.$treeItemHandle) { + if ((selected.handle === (context as TreeViewItemHandleArg).$treeItemHandle) || (context as TreeViewPaneHandleArg).$selectedTreeItems) { actionInSelected = true; } return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle }; @@ -1443,7 +1405,7 @@ class MultipleSelectionActionRunner extends ActionRunner { selectionHandleArgs = undefined; } - await action.run(...[context, selectionHandleArgs]); + await action.run(context, selectionHandleArgs); } } @@ -1458,18 +1420,20 @@ class TreeMenus extends Disposable implements IDisposable { super(); } - getResourceActions(element: ITreeItem): IAction[] { - return this.mergeActions([ // tracked change - this.getActions(MenuId.ViewItemContext, element, true).primary, - this.getActions(MenuId.DataExplorerContext, element, true).primary - ]); + getResourceActions(element: ITreeItem): { menus?: IMenu[]; actions: IAction[] } { + const viewItemActions = this.getActions(MenuId.ViewItemContext, element, true); + const dataExplorerActions = this.getActions(MenuId.DataExplorerContext, element, true) + const actions = this.mergeActions([viewItemActions.primary, dataExplorerActions.primary]); + const menus = [viewItemActions.menu, dataExplorerActions.menu]; + return { menus, actions }; } - getResourceContextActions(element: ITreeItem): IAction[] { - return this.mergeActions([ // tracked change - this.getActions(MenuId.ViewItemContext, element).secondary, - this.getActions(MenuId.DataExplorerContext, element).secondary - ]); + getResourceContextActions(element: ITreeItem): { menus?: IMenu[]; actions: IAction[] } { + const viewItemActions = this.getActions(MenuId.ViewItemContext, element); + const dataExplorerActions = this.getActions(MenuId.DataExplorerContext, element) + const actions = this.mergeActions([viewItemActions.secondary, dataExplorerActions.secondary]); + const menus = [viewItemActions.menu, dataExplorerActions.menu]; + return { menus, actions }; } public setContextKeyService(service: IContextKeyService) { @@ -1484,6 +1448,7 @@ class TreeMenus extends Disposable implements IDisposable { if (!this.contextKeyService) { return { primary: [], secondary: [] }; } + const contextKeyService = this.contextKeyService.createOverlay([ ['view', this.id], ['viewItem', element.contextValue] @@ -1542,6 +1507,7 @@ export class CustomTreeView extends AbstractTreeView { protected activate() { if (!this.activated) { + this.createTree(); this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000)) .then(() => {