diff --git a/src/sql/workbench/browser/parts/views/customView.ts b/src/sql/workbench/browser/parts/views/customView.ts index 6566bca961..3594945482 100644 --- a/src/sql/workbench/browser/parts/views/customView.ts +++ b/src/sql/workbench/browser/parts/views/customView.ts @@ -4,33 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, toDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IAction, ActionRunner, Action } from 'vs/base/common/actions'; +import { IAction, IActionViewItem, ActionRunner, Action } 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 } from 'vs/platform/actions/common/actions'; +import { ContextAwareMenuEntryActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ViewContainer, ITreeItemLabel } from 'vs/workbench/common/views'; -import { FileIconThemableWorkbenchTree } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IViewsService, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeViewDescriptor, IViewsRegistry, ViewContainer, ITreeItemLabel, Extensions } from 'vs/workbench/common/views'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import * as DOM from 'vs/base/browser/dom'; -import { IDataSource, ITree, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree'; -import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { URI } from 'vs/base/common/uri'; -import { basename } from 'vs/base/common/path'; +import { dirname, basename } from 'vs/base/common/resources'; import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { FileKind } from 'vs/platform/files/common/files'; -import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService'; +import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { localize } from 'vs/nls'; import { timeout } from 'vs/base/common/async'; -import { CollapseAllAction } from 'vs/base/parts/tree/browser/treeDefaults'; import { editorFindMatchHighlight, editorFindMatchHighlightBorder, textLinkForeground, textCodeBlockBackground, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { isString } from 'vs/base/common/types'; @@ -39,55 +39,106 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer'; import { ILabelService } from 'vs/platform/label/common/label'; -import { dirname } from 'vs/base/common/resources'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { ITreeItem, ITreeView } from 'sql/workbench/common/views'; import { IOEShimService } from 'sql/workbench/parts/objectExplorer/common/objectExplorerViewTreeShim'; import { NodeContextKey } from 'sql/workbench/parts/dataExplorer/common/nodeContext'; -import { ContextAwareMenuEntryActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; -class TitleMenus implements IDisposable { +export class CustomTreeViewPanel extends ViewletPanel { + + private treeView: ITreeView; + + constructor( + options: IViewletViewOptions, + @INotificationService private readonly notificationService: INotificationService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IViewsService viewsService: IViewsService, + ) { + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); + const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); + this.treeView = treeView as ITreeView; + this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); + this._register(toDisposable(() => this.treeView.setVisibility(false))); + this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); + this.updateTreeVisibility(); + } + + focus(): void { + super.focus(); + this.treeView.focus(); + } + + renderBody(container: HTMLElement): void { + this.treeView.show(container); + } + + layoutBody(size: number): void { + this.treeView.layout(size); + } + + getActions(): IAction[] { + return [...this.treeView.getPrimaryActions()]; + } + + getSecondaryActions(): IAction[] { + return [...this.treeView.getSecondaryActions()]; + } + + getActionViewItem(action: IAction): IActionViewItem | undefined { + return action instanceof MenuItemAction ? new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService) : undefined; + } + + getOptimalWidth(): number { + return this.treeView.getOptimalWidth(); + } + + private updateTreeVisibility(): void { + this.treeView.setVisibility(this.isBodyVisible()); + } +} + +class TitleMenus extends Disposable { - private disposables: IDisposable[] = []; - private titleDisposable: IDisposable = Disposable.None; private titleActions: IAction[] = []; + private readonly titleActionsDisposable = this._register(new MutableDisposable()); private titleSecondaryActions: IAction[] = []; - private _onDidChangeTitle = new Emitter(); - get onDidChangeTitle(): Event { return this._onDidChangeTitle.event; } + private _onDidChangeTitle = this._register(new Emitter()); + readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; constructor( id: string, - @IContextKeyService private contextKeyService: IContextKeyService, - @IMenuService private menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, ) { - if (this.titleDisposable) { - this.titleDisposable.dispose(); - this.titleDisposable = Disposable.None; - } + super(); - const _contextKeyService = this.contextKeyService.createScoped(); - _contextKeyService.createKey('view', id); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); + scopedContextKeyService.createKey('view', id); - const titleMenu = this.menuService.createMenu(MenuId.ViewTitle, _contextKeyService); + const titleMenu = this._register(this.menuService.createMenu(MenuId.ViewTitle, scopedContextKeyService)); const updateActions = () => { this.titleActions = []; this.titleSecondaryActions = []; - createAndFillInActionBarActions(titleMenu, undefined, { primary: this.titleActions, secondary: this.titleSecondaryActions }); + this.titleActionsDisposable.value = createAndFillInActionBarActions(titleMenu, undefined, { primary: this.titleActions, secondary: this.titleSecondaryActions }); this._onDidChangeTitle.fire(); }; - const listener = titleMenu.onDidChange(updateActions); + this._register(titleMenu.onDidChange(updateActions)); updateActions(); - this.titleDisposable = toDisposable(() => { - listener.dispose(); - titleMenu.dispose(); - _contextKeyService.dispose(); + this._register(toDisposable(() => { this.titleActions = []; this.titleSecondaryActions = []; - }); + })); } getTitleActions(): IAction[] { @@ -97,18 +148,14 @@ class TitleMenus implements IDisposable { getTitleSecondaryActions(): IAction[] { return this.titleSecondaryActions; } - - dispose(): void { - this.disposables = dispose(this.disposables); - } } class Root implements ITreeItem { label = { label: 'root' }; handle = '0'; - parentHandle = null; + parentHandle: string | undefined = undefined; collapsibleState = TreeItemCollapsibleState.Expanded; - children = void 0; + children: ITreeItem[] | undefined = undefined; } const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); @@ -126,40 +173,44 @@ export class CustomTreeView extends Disposable implements ITreeView { private treeContainer: HTMLElement; private _messageValue: string | IMarkdownString | undefined; private messageElement: HTMLDivElement; - private tree: FileIconThemableWorkbenchTree; + private tree: WorkbenchAsyncDataTree; + private treeLabels: ResourceLabels; private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; private menus: TitleMenus; private markdownRenderer: MarkdownRenderer; - private markdownResult: IMarkdownRenderResult; + private markdownResult: IMarkdownRenderResult | null; - private _onDidExpandItem: Emitter = this._register(new Emitter()); + private readonly _onDidExpandItem: Emitter = this._register(new Emitter()); readonly onDidExpandItem: Event = this._onDidExpandItem.event; - private _onDidCollapseItem: Emitter = this._register(new Emitter()); + 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 _onDidChangeVisibility: Emitter = this._register(new Emitter()); + private readonly _onDidChangeVisibility: Emitter = this._register(new Emitter()); readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; - private _onDidChangeActions: Emitter = this._register(new Emitter()); + private readonly _onDidChangeActions: Emitter = this._register(new Emitter()); readonly onDidChangeActions: Event = this._onDidChangeActions.event; private nodeContext: NodeContextKey; constructor( private id: string, - private container: ViewContainer, - @IExtensionService private extensionService: IExtensionService, - @IWorkbenchThemeService private themeService: IWorkbenchThemeService, - @IInstantiationService private instantiationService: IInstantiationService, - @ICommandService private commandService: ICommandService, - @IConfigurationService private configurationService: IConfigurationService, - @IProgressService private progressService: IProgressService + private title: string, + private viewContainer: ViewContainer, + @IExtensionService private readonly extensionService: IExtensionService, + @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IProgressService private readonly progressService: IProgressService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IKeybindingService private readonly keybindingService: IKeybindingService ) { super(); this.root = new Root(); @@ -178,29 +229,31 @@ export class CustomTreeView extends Disposable implements ITreeView { this.markdownResult.dispose(); } })); + this._register(Registry.as(Extensions.ViewsRegistry).onDidChangeContainer(({ views, from, to }) => { + if (from === this.viewContainer && views.some(v => v.id === this.id)) { + this.viewContainer = to; + } + })); this.nodeContext = this._register(instantiationService.createInstance(NodeContextKey)); - this.create(); } - private _dataProvider: ITreeViewDataProvider; - get dataProvider(): ITreeViewDataProvider { + private _dataProvider: ITreeViewDataProvider | null; + get dataProvider(): ITreeViewDataProvider | null { return this._dataProvider; } - set dataProvider(dataProvider: ITreeViewDataProvider) { + set dataProvider(dataProvider: ITreeViewDataProvider | null) { if (dataProvider) { this._dataProvider = new class implements ITreeViewDataProvider { - getChildren(node?: ITreeItem): Promise { + async getChildren(node: ITreeItem): Promise { if (node && node.children) { return Promise.resolve(node.children); } - const promise = node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node); - return promise.then(children => { - node.children = children; - return children; - }); + const children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); + node.children = children; + return children; } }; this.updateMessage(); @@ -246,7 +299,7 @@ export class CustomTreeView extends Disposable implements ITreeView { getPrimaryActions(): IAction[] { if (this.showCollapseAllAction) { - const collapseAllAction = new Action('vs.tree.collapse', localize('collapse', "Collapse"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); + const collapseAllAction = new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve()); return [...this.menus.getTitleActions(), collapseAllAction]; } else { return this.menus.getTitleActions(); @@ -275,12 +328,6 @@ export class CustomTreeView 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.tree.onVisible(); - } else { - this.tree.onHidden(); - } - if (this.isVisible && this.elementsToRefresh.length) { this.doRefresh(this.elementsToRefresh); this.elementsToRefresh = []; @@ -313,29 +360,110 @@ export class CustomTreeView extends Disposable implements ITreeView { this.domNode = DOM.$('.tree-explorer-viewlet-tree-view'); this.messageElement = DOM.append(this.domNode, DOM.$('.message')); this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree')); + DOM.addClass(this.treeContainer, 'file-icon-themable-tree'); + DOM.addClass(this.treeContainer, '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)); } private createTree() { - const actionItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; - const menus = this.instantiationService.createInstance(TreeMenus, this.id); - const dataSource = this.instantiationService.createInstance(TreeDataSource, this, this.container, this.id); - const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, menus, actionItemProvider); - const controller = this.instantiationService.createInstance(TreeController, this.id, this.container.id, menus); - this.tree = this.instantiationService.createInstance(FileIconThemableWorkbenchTree, this.treeContainer, { dataSource, renderer, controller }, {}); + const actionViewItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuEntryActionViewItem, action) : undefined; + 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.viewContainer.id }, () => task)); + const aligner = new Aligner(this.themeService); + const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); + + this.tree = this._register(this.instantiationService.createInstance(WorkbenchAsyncDataTree, this.treeContainer, new CustomTreeDelegate(), [renderer], + dataSource, { + identityProvider: new CustomViewIdentityProvider(), + accessibilityProvider: { + getAriaLabel(element: ITreeItem): string { + return element.label ? element.label.label : ''; + } + }, + ariaLabel: this.title, + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (item: ITreeItem) => { + return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined); + } + }, + expandOnlyOnTwistieClick: (e: ITreeItem) => !!e.command, + collapseByDefault: (e: ITreeItem): boolean => { + return e.collapsibleState !== TreeItemCollapsibleState.Expanded; + } + }) as WorkbenchAsyncDataTree); + aligner.tree = this.tree; + this.tree.contextKeyService.createKey(this.id, true); - this._register(this.tree); - this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); - this._register(this.tree.onDidExpandItem(e => this._onDidExpandItem.fire(e.item.getElement()))); - this._register(this.tree.onDidCollapseItem(e => this._onDidCollapseItem.fire(e.item.getElement()))); - this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.selection))); + this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e))); + this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + this._register(this.tree.onDidChangeCollapseState(e => { + const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element; + if (e.node.collapsed) { + this._onDidCollapseItem.fire(element); + } else { + this._onDidExpandItem.fire(element); + } + })); // Update resource context based on focused element - this._register(this.tree.onDidChangeFocus((e: { focus: ITreeItem }) => { - this.nodeContext.set({ node: e.focus, viewId: this.id }); + this._register(this.tree.onDidChangeFocus(e => { + this.nodeContext.set({ node: e.elements[0], viewId: this.id }); })); this.tree.setInput(this.root).then(() => this.updateContentAreas()); + + const customTreeNavigator = new TreeResourceNavigator2(this.tree); + this._register(customTreeNavigator); + this._register(customTreeNavigator.onDidOpenResource(e => { + if (!e.browserEvent) { + return; + } + const selection = this.tree.getSelection(); + if ((selection.length === 1) && selection[0].command) { + this.commandService.executeCommand(selection[0].command.id, ...(selection[0].command.arguments || [])); + } + })); + } + + private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent): void { + const node: ITreeItem | null = treeEvent.element; + if (node === null) { + return; + } + const event: UIEvent = treeEvent.browserEvent; + + event.preventDefault(); + event.stopPropagation(); + + this.tree.setFocus([node]); + const actions = treeMenus.getResourceContextActions(node); + if (!actions.length) { + return; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => treeEvent.anchor, + + getActions: () => actions, + + getActionViewItem: (action) => { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return undefined; + }, + + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + this.tree.domFocus(); + } + }, + + getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle, $treeItem: node, $treeContainerId: this.treeContainer.id }), + + actionRunner: new MultipleSelectionActionRunner(() => this.tree.getSelection()) + }); } private updateMessage(): void { @@ -401,59 +529,74 @@ export class CustomTreeView extends Disposable implements ITreeView { refresh(elements?: ITreeItem[]): Promise { if (this.dataProvider && this.tree) { - elements = elements || [this.root]; + if (!elements) { + elements = [this.root]; + // remove all waiting elements to refresh if root is asked to refresh + this.elementsToRefresh = []; + } for (const element of elements) { - element.children = null; // reset children + element.children = undefined; // reset children } if (this.isVisible) { return this.doRefresh(elements); } else { - this.elementsToRefresh.push(...elements); + if (this.elementsToRefresh.length) { + const seen: Set = new Set(); + this.elementsToRefresh.forEach(element => seen.add(element.handle)); + for (const element of elements) { + if (!seen.has(element.handle)) { + this.elementsToRefresh.push(element); + } + } + } else { + this.elementsToRefresh.push(...elements); + } } } - return Promise.resolve(null); + return Promise.resolve(undefined); } - expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { + collapse(element: ITreeItem): boolean { if (this.tree) { - itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - return this.tree.expandAll(itemOrItems); + return this.tree.collapse(element); } return Promise.arguments(null); } - collapse(itemOrItems: ITreeItem | ITreeItem[]): Thenable { + async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise { if (this.tree) { itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - return this.tree.collapseAll(itemOrItems); + await Promise.all(itemOrItems.map(element => { + return this.tree.expand(element, false); + })); } - return Promise.arguments(null); + return Promise.resolve(undefined); } setSelection(items: ITreeItem[]): void { if (this.tree) { - this.tree.setSelection(items, { source: 'api' }); + this.tree.setSelection(items); } } setFocus(item: ITreeItem): void { if (this.tree) { this.focus(); - this.tree.setFocus(item); + this.tree.setFocus([item]); } } reveal(item: ITreeItem): Promise { if (this.tree) { - return this.tree.reveal(item); + return Promise.resolve(this.tree.reveal(item)); } - return Promise.arguments(null); + return Promise.resolve(); } private activate() { if (!this.activated) { this.createTree(); - this.progressService.withProgress({ location: this.container.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) + this.progressService.withProgress({ location: this.viewContainer.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) .then(() => timeout(2000)) .then(() => { this.updateMessage(); @@ -463,19 +606,16 @@ export class CustomTreeView extends Disposable implements ITreeView { } private refreshing: boolean = false; - private doRefresh(elements: ITreeItem[]): Promise { + private async doRefresh(elements: ITreeItem[]): Promise { if (this.tree) { this.refreshing = true; - return Promise.all(elements.map(e => this.tree.refresh(e))) - .then(() => { - this.refreshing = false; - this.updateContentAreas(); - if (this.focused) { - this.focus(); - } - }); + await Promise.all(elements.map(element => this.tree.updateChildren(element, true))); + this.refreshing = false; + this.updateContentAreas(); + if (this.focused) { + this.focus(); + } } - return Promise.resolve(null); } private updateContentAreas(): void { @@ -489,100 +629,83 @@ export class CustomTreeView extends Disposable implements ITreeView { this.domNode.removeAttribute('tabindex'); } } +} - private onSelection({ payload }: any): void { - if (payload && (!!payload.didClickOnTwistie || payload.source === 'api')) { - return; - } - const selection: ITreeItem = this.tree.getSelection()[0]; - if (selection) { - if (selection.command) { - const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; - const isMouseEvent = payload && payload.origin === 'mouse'; - const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2; - - if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) { - this.commandService.executeCommand(selection.command.id, ...(selection.command.arguments || [])); - } - } - } +class CustomViewIdentityProvider implements IIdentityProvider { + getId(element: ITreeItem): { toString(): string; } { + return element.handle; } } -class TreeDataSource implements IDataSource { +class CustomTreeDelegate implements IListVirtualDelegate { + + getHeight(element: ITreeItem): number { + return TreeRenderer.ITEM_HEIGHT; + } + + getTemplateId(element: ITreeItem): string { + return TreeRenderer.TREE_TEMPLATE_ID; + } +} + +class TreeDataSource implements IAsyncDataSource { constructor( private treeView: ITreeView, - private container: ViewContainer, private id: string, - @IProgressService private progressService: IProgressService, + private withProgress: (task: Promise) => Promise, @IOEShimService private objectExplorerService: IOEShimService ) { } - getId(tree: ITree, node: ITreeItem): string { - return node.handle; - } - - hasChildren(tree: ITree, node: ITreeItem): boolean { + hasChildren(node: ITreeItem): boolean { if (node.childProvider) { return this.objectExplorerService.providerExists(node.childProvider) && node.collapsibleState !== TreeItemCollapsibleState.None; } return this.treeView.dataProvider && node.collapsibleState !== TreeItemCollapsibleState.None; } - getChildren(tree: ITree, node: ITreeItem): Promise { + getChildren(node: ITreeItem): Promise { if (node.childProvider) { - return this.progressService.withProgress({ location: this.container.id }, () => this.objectExplorerService.getChildren(node, this.id)).catch(e => { + return this.withProgress(this.objectExplorerService.getChildren(node, this.id)).catch(e => { // if some error is caused we assume something tangently happened // i.e the user could retry if they wanted. // So in order to enable this we need to tell the tree to refresh this node so it will ask us for the data again setTimeout(() => { - tree.collapse(node).then(() => tree.refresh(node)); + this.treeView.collapse(node); + this.treeView.refresh([node]); }); return []; }); } if (this.treeView.dataProvider) { - return this.progressService.withProgress({ location: this.container.id }, () => this.treeView.dataProvider.getChildren(node)); + return this.withProgress(this.treeView.dataProvider.getChildren(node)); } return Promise.resolve([]); } - - shouldAutoexpand(tree: ITree, node: ITreeItem): boolean { - return node.collapsibleState === TreeItemCollapsibleState.Expanded; - } - - getParent(tree: ITree, node: any): Promise { - return Promise.resolve(null); - } } -interface ITreeExplorerTemplateData { - resourceLabel: ResourceLabel; - icon: HTMLElement; - actionBar: ActionBar; - aligner: Aligner; -} // todo@joh,sandy make this proper and contributable from extensions registerThemingParticipant((theme, collector) => { const findMatchHighlightColor = theme.getColor(editorFindMatchHighlight); if (findMatchHighlightColor) { - collector.addRule(`.file-icon-themable-tree .monaco-tree-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`); + collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`); + collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; background-color: ${findMatchHighlightColor}; }`); } const findMatchHighlightColorBorder = theme.getColor(editorFindMatchHighlightBorder); if (findMatchHighlightColorBorder) { - collector.addRule(`.file-icon-themable-tree .monaco-tree-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`); + collector.addRule(`.file-icon-themable-tree .monaco-list-row .content .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`); + collector.addRule(`.monaco-tl-contents .monaco-highlighted-label .highlight { color: unset !important; border: 1px dotted ${findMatchHighlightColorBorder}; box-sizing: border-box; }`); } const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.tree-explorer-viewlet-tree-view > .message a { color: ${link}; }`); } - const focustBorderColor = theme.getColor(focusBorder); - if (focustBorderColor) { - collector.addRule(`.tree-explorer-viewlet-tree-view > .message a:focus { outline: 1px solid ${focustBorderColor}; outline-offset: -1px; }`); + 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) { @@ -590,77 +713,84 @@ registerThemingParticipant((theme, collector) => { } }); -class TreeRenderer implements IRenderer { +interface ITreeExplorerTemplateData { + elementDisposable: IDisposable; + container: HTMLElement; + resourceLabel: IResourceLabel; + icon: HTMLElement; + actionBar: ActionBar; +} - private static readonly ITEM_HEIGHT = 22; - private static readonly TREE_TEMPLATE_ID = 'treeExplorer'; - private static readonly MSSQL_TREE_TEMPLATE_ID = 'mssqltreeExplorer'; +class TreeRenderer extends Disposable implements ITreeRenderer { + static readonly ITEM_HEIGHT = 22; + static readonly TREE_TEMPLATE_ID = 'treeExplorer'; constructor( private treeViewId: string, private menus: TreeMenus, - private actionItemProvider: IActionViewItemProvider, - @IInstantiationService private instantiationService: IInstantiationService, - @IWorkbenchThemeService private themeService: IWorkbenchThemeService, - @IConfigurationService private configurationService: IConfigurationService, - @ILabelService private labelService: ILabelService + private labels: ResourceLabels, + private actionViewItemProvider: IActionViewItemProvider, + private aligner: Aligner, + @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ILabelService private readonly labelService: ILabelService ) { + super(); } - getHeight(tree: ITree, element: any): number { - return TreeRenderer.ITEM_HEIGHT; + get templateId(): string { + return TreeRenderer.TREE_TEMPLATE_ID; } - getTemplateId(tree: ITree, element: ITreeItem): string { - return element.providerHandle === mssqlProviderName ? TreeRenderer.MSSQL_TREE_TEMPLATE_ID : TreeRenderer.TREE_TEMPLATE_ID; - } - - renderTemplate(tree: ITree, templateId: string, container: HTMLElement): ITreeExplorerTemplateData { + renderTemplate(container: HTMLElement): ITreeExplorerTemplateData { DOM.addClass(container, 'custom-view-tree-node-item'); const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); - const resourceLabel = this.instantiationService.createInstance(ResourceLabel, container, { supportHighlights: true, donotSupportOcticons: true }); - DOM.addClass(resourceLabel.element.element, 'custom-view-tree-node-item-resourceLabel'); - const actionsContainer = DOM.append(resourceLabel.element.element, DOM.$('.actions')); + + const resourceLabel = this.labels.create(container, { supportHighlights: true, donotSupportOcticons: true }); + const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); const actionBar = new ActionBar(actionsContainer, { - actionViewItemProvider: this.actionItemProvider, - actionRunner: new MultipleSelectionActionRunner(() => tree.getSelection()) + actionViewItemProvider: this.actionViewItemProvider }); - return { resourceLabel, icon, actionBar, aligner: new Aligner(container, tree, this.themeService) }; + return { resourceLabel, icon, actionBar, container, elementDisposable: Disposable.None }; } - renderElement(tree: ITree, node: ITreeItem, templateId: string, templateData: ITreeExplorerTemplateData): void { + renderElement(element: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + const node = element.element; const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; - const treeItemLabel: ITreeItemLabel = node.label ? node.label : resource ? { label: basename(resource.path) } : void 0; - const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : void 0; - const label = treeItemLabel ? treeItemLabel.label : void 0; - const matches = treeItemLabel && treeItemLabel.highlights ? treeItemLabel.highlights.map(([start, end]) => ({ start, end })) : void 0; + 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 icon = this.themeService.getTheme().type === LIGHT ? node.icon : node.iconDark; const iconUrl = icon ? URI.revive(icon) : null; - const title = node.tooltip ? node.tooltip : resource ? void 0 : label; + const title = node.tooltip ? node.tooltip : resource ? undefined : label; const sqlIcon = node.sqlIcon; // reset - templateData.resourceLabel.clear(); templateData.actionBar.clear(); if (resource || node.themeIcon) { const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); - templateData.resourceLabel.element.setResource({ name: label, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), title, fileDecorations: fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches }); + 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: createMatches(element.filterData) }); } else { - templateData.resourceLabel.element.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches }); + templateData.resourceLabel.setResource({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches: createMatches(element.filterData) }); } - // clear out icons to prevent duplication from other templates + templateData.icon.className = ''; - templateData.icon.style.backgroundImage = iconUrl ? `url('${iconUrl.toString(true)}')` : ''; + templateData.icon.style.backgroundImage = iconUrl ? `url('${DOM.asDomUri(iconUrl).toString(true)}')` : ''; DOM.toggleClass(templateData.icon, sqlIcon, !!sqlIcon); DOM.toggleClass(templateData.icon, 'icon', !!sqlIcon); DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!iconUrl || !!sqlIcon); templateData.actionBar.context = ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }); templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); + this.setAlignment(templateData.container, node); + templateData.elementDisposable = (this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node))); + } - templateData.aligner.treeItem = node; + private setAlignment(container: HTMLElement, treeItem: ITreeItem) { + DOM.toggleClass(container.parentElement!, 'align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem)); } private getFileKind(node: ITreeItem): FileKind { @@ -675,50 +805,41 @@ class TreeRenderer implements IRenderer { return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; } - disposeTemplate(tree: ITree, templateId: string, templateData: ITreeExplorerTemplateData): void { + disposeElement(resource: ITreeNode, index: number, templateData: ITreeExplorerTemplateData): void { + templateData.elementDisposable.dispose(); + } + + disposeTemplate(templateData: ITreeExplorerTemplateData): void { templateData.resourceLabel.dispose(); templateData.actionBar.dispose(); - templateData.aligner.dispose(); + templateData.elementDisposable.dispose(); } } class Aligner extends Disposable { + private _tree: WorkbenchAsyncDataTree; - private _treeItem: ITreeItem; - - constructor( - private container: HTMLElement, - private tree: ITree, - private themeService: IWorkbenchThemeService - ) { + constructor(private themeService: IWorkbenchThemeService) { super(); - this._register(this.themeService.onDidFileIconThemeChange(() => this.render())); } - set treeItem(treeItem: ITreeItem) { - this._treeItem = treeItem; - this.render(); + set tree(tree: WorkbenchAsyncDataTree) { + this._tree = tree; } - private render(): void { - if (this._treeItem) { - DOM.toggleClass(this.container, 'align-icon-with-twisty', this.hasToAlignIconWithTwisty()); - } - } - - private hasToAlignIconWithTwisty(): boolean { - if (this._treeItem.collapsibleState !== TreeItemCollapsibleState.None) { + public alignIconWithTwisty(treeItem: ITreeItem): boolean { + if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) { return false; } - if (!this.hasIcon(this._treeItem)) { + if (!this.hasIcon(treeItem)) { return false; - } - const parent: ITreeItem = this.tree.getNavigator(this._treeItem).parent() || this.tree.getInput(); + + const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput(); if (this.hasIcon(parent)) { return false; } - return parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); + return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); } private hasIcon(node: ITreeItem): boolean { @@ -738,64 +859,9 @@ class Aligner extends Disposable { } } -class TreeController extends WorkbenchTreeController { - - constructor( - private treeViewId: string, - private containerId: string, - private menus: TreeMenus, - @IContextMenuService private contextMenuService: IContextMenuService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IConfigurationService configurationService: IConfigurationService - ) { - super({}, configurationService); - } - - protected shouldToggleExpansion(element: ITreeItem, event: IMouseEvent, origin: string): boolean { - return element.command ? this.isClickOnTwistie(event) : super.shouldToggleExpansion(element, event, origin); - } - - onContextMenu(tree: ITree, node: ITreeItem, event: ContextMenuEvent): boolean { - event.preventDefault(); - event.stopPropagation(); - - tree.setFocus(node); - const actions = this.menus.getResourceContextActions(node); - if (!actions.length) { - return true; - } - const anchor = { x: event.posx, y: event.posy }; - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - - getActions: () => actions, - - getActionViewItem: (action) => { - const keybinding = this._keybindingService.lookupKeybinding(action.id); - if (keybinding) { - return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() }); - } - return null; - }, - - onHide: (wasCancelled?: boolean) => { - if (wasCancelled) { - tree.domFocus(); - } - }, - - getActionsContext: () => ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle, $treeItem: node, $treeContainerId: this.containerId }), - - actionRunner: new MultipleSelectionActionRunner(() => tree.getSelection()) - }); - - return true; - } -} - class MultipleSelectionActionRunner extends ActionRunner { - constructor(private getSelectedResources: () => any[]) { + constructor(private getSelectedResources: (() => any[])) { super(); } @@ -819,9 +885,9 @@ class TreeMenus extends Disposable implements IDisposable { constructor( private id: string, - @IContextKeyService private contextKeyService: IContextKeyService, - @IMenuService private menuService: IMenuService, - @IContextMenuService private contextMenuService: IContextMenuService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + @IContextMenuService private readonly contextMenuService: IContextMenuService ) { super(); } @@ -889,11 +955,11 @@ class MarkdownRenderer { } render(markdown: IMarkdownString): IMarkdownRenderResult { - let disposeables = new DisposableStore(); + const disposeables = new DisposableStore(); const element: HTMLElement = markdown ? renderMarkdown(markdown, this.getOptions(disposeables)) : document.createElement('span'); return { element, - dispose: () => dispose(disposeables) + dispose: () => disposeables.dispose() }; } } diff --git a/src/sql/workbench/common/views.ts b/src/sql/workbench/common/views.ts index af3d83c8f9..4abfe9d6ab 100644 --- a/src/sql/workbench/common/views.ts +++ b/src/sql/workbench/common/views.ts @@ -28,7 +28,7 @@ export interface ITreeItem extends vsITreeItem { export interface ITreeView extends vsITreeView { - collapse(itemOrItems: ITreeItem | ITreeItem[]): Thenable; + collapse(itemOrItems: ITreeItem): boolean; } diff --git a/src/sql/workbench/parts/dataExplorer/browser/dataExplorerExtensionPoint.ts b/src/sql/workbench/parts/dataExplorer/browser/dataExplorerExtensionPoint.ts index f106b22432..b6aa8aee03 100644 --- a/src/sql/workbench/parts/dataExplorer/browser/dataExplorerExtensionPoint.ts +++ b/src/sql/workbench/parts/dataExplorer/browser/dataExplorerExtensionPoint.ts @@ -113,7 +113,7 @@ class DataExplorerContainerExtensionHandler implements IWorkbenchContribution { when: ContextKeyExpr.deserialize(item.when), canToggleVisibility: true, collapsed: this.showCollapsed(container), - treeView: this.instantiationService.createInstance(CustomTreeView, item.id, container) + treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name, container) }; viewIds.push(viewDescriptor.id); diff --git a/src/sql/workbench/parts/dataExplorer/electron-browser/nodeCommands.ts b/src/sql/workbench/parts/dataExplorer/electron-browser/nodeCommands.ts index 3fe0a3033f..a25931e45c 100644 --- a/src/sql/workbench/parts/dataExplorer/electron-browser/nodeCommands.ts +++ b/src/sql/workbench/parts/dataExplorer/electron-browser/nodeCommands.ts @@ -41,7 +41,8 @@ CommandsRegistry.registerCommand({ return oeService.disconnectNode(args.$treeViewId, args.$treeItem).then(() => { const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(args.$treeViewId)); // we need to collapse it then refresh it so that the tree doesn't try and use it's cache next time the user expands the node - return treeView.collapse(args.$treeItem).then(() => treeView.refresh([args.$treeItem]).then(() => true)); + treeView.collapse(args.$treeItem); + treeView.refresh([args.$treeItem]).then(() => true); }); } return Promise.resolve(true);