diff --git a/src/sql/parts/dataExplorer/common/dataExplorerExtensionPoint.ts b/src/sql/parts/dataExplorer/common/dataExplorerExtensionPoint.ts index 7405b9c34c..11ea872e69 100644 --- a/src/sql/parts/dataExplorer/common/dataExplorerExtensionPoint.ts +++ b/src/sql/parts/dataExplorer/common/dataExplorerExtensionPoint.ts @@ -12,11 +12,13 @@ import { IExtensionPoint, ExtensionsRegistry, ExtensionMessageCollector } from ' import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { CustomTreeViewPanel, CustomTreeView } from 'vs/workbench/browser/parts/views/customView'; +import { CustomTreeViewPanel } from 'vs/workbench/browser/parts/views/customView'; import { coalesce } from 'vs/base/common/arrays'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { viewsContainersExtensionPoint } from 'vs/workbench/api/browser/viewsContainersExtensionPoint'; +import { CustomTreeView } from 'sql/workbench/browser/parts/views/customView'; + export const DataExplorerViewlet = { DataExplorer: 'dataExplorer' }; @@ -163,4 +165,4 @@ class DataExplorerContainerExtensionHandler implements IWorkbenchContribution { } const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchRegistry.registerWorkbenchContribution(DataExplorerContainerExtensionHandler, LifecyclePhase.Starting); \ No newline at end of file +workbenchRegistry.registerWorkbenchContribution(DataExplorerContainerExtensionHandler, LifecyclePhase.Starting); diff --git a/src/sql/parts/objectExplorer/common/objectExplorerViewTreeShim.ts b/src/sql/parts/objectExplorer/common/objectExplorerViewTreeShim.ts index fd40bdb469..bc1fc69cfe 100644 --- a/src/sql/parts/objectExplorer/common/objectExplorerViewTreeShim.ts +++ b/src/sql/parts/objectExplorer/common/objectExplorerViewTreeShim.ts @@ -28,7 +28,7 @@ export const IOEShimService = createDecorator(SERVICE_ID); export interface IOEShimService { _serviceBrand: any; - getChildren(node: ITreeItem, identifier: any): TPromise; + getChildren(node: ITreeItem, identifier: any): Promise; providerExists(providerId: string): boolean; } @@ -69,7 +69,7 @@ export class OEShimService implements IOEShimService { return TPromise.wrap(deferred.promise); } - public async getChildren(node: ITreeItem, identifier: any): TPromise { + public async getChildren(node: ITreeItem, identifier: any): Promise { try { if (!this.sessionMap.has(identifier)) { this.sessionMap.set(identifier, new Map()); @@ -111,27 +111,6 @@ export class OEShimService implements IOEShimService { } private mapNodeToITreeItem(node: TreeNode, parentNode: ITreeItem): ITreeItem { - let icon: string; - let iconDark: string; - if (equalsIgnoreCase(parentNode.childProvider, 'mssql')) { - if (node.iconType) { - icon = (typeof node.iconType === 'string') ? node.iconType : node.iconType.id; - } else { - icon = node.nodeTypeId; - if (node.nodeStatus) { - icon = node.nodeTypeId + '_' + node.nodeStatus; - } - if (node.nodeSubType) { - icon = node.nodeTypeId + '_' + node.nodeSubType; - } - } - icon = icon.toLowerCase(); - iconDark = icon; - } else { - icon = node.iconType as string; - // this is just because we need to have some mapping - iconDark = node.nodeSubType; - } let handle = generateUuid(); this.nodeIdMap.set(handle, node.nodePath); return { @@ -141,8 +120,6 @@ export class OEShimService implements IOEShimService { label: { label: node.label }, - icon: URI.parse(icon), - iconDark: URI.parse(iconDark), childProvider: node.childProvider || parentNode.childProvider, providerHandle: parentNode.childProvider, payload: node.payload || parentNode.payload, diff --git a/src/sql/parts/objectExplorer/viewlet/connectionTreeAction.ts b/src/sql/parts/objectExplorer/viewlet/connectionTreeAction.ts index 226665cc8f..203722773f 100644 --- a/src/sql/parts/objectExplorer/viewlet/connectionTreeAction.ts +++ b/src/sql/parts/objectExplorer/viewlet/connectionTreeAction.ts @@ -127,7 +127,6 @@ export class DisconnectConnectionAction extends Action { } } - /** * Actions to add a server to the group */ @@ -402,4 +401,4 @@ export class ClearSearchAction extends Action { this._viewlet.clearSearch(); return TPromise.as(true); } -} \ No newline at end of file +} diff --git a/src/sql/workbench/browser/parts/views/customView.ts b/src/sql/workbench/browser/parts/views/customView.ts index 88d9c73002..5298788d7a 100644 --- a/src/sql/workbench/browser/parts/views/customView.ts +++ b/src/sql/workbench/browser/parts/views/customView.ts @@ -3,65 +3,135 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDataSource, ITree, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree'; -import { ViewContainer, TreeItemCollapsibleState, ITreeView, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItem as vsITreeItem } from 'vs/workbench/common/views'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IAction, 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 { ContextAwareMenuItemActionItem, fillInActionBarActions, fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ITreeView, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ViewContainer, ITreeItemLabel } from 'vs/workbench/common/views'; import { FileIconThemableWorkbenchTree } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { Emitter, Event } from 'vs/base/common/event'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProgressService2 } 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import * as DOM from 'vs/base/browser/dom'; -import * as errors from 'vs/base/common/errors'; -import { IAction, ActionRunner } from 'vs/base/common/actions'; -import { MenuItemAction, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { ContextAwareMenuItemActionItem, fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem'; -import { isUndefinedOrNull } from 'vs/base/common/types'; -import { IActionItemProvider, ActionBar, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { URI } from 'vs/base/common/uri'; -import { LIGHT, FileThemeIcon, FolderThemeIcon } from 'vs/platform/theme/common/themeService'; -import { basename } from 'vs/base/common/paths'; +import { IDataSource, ITree, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree'; import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { ActionBar, IActionItemProvider, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { URI } from 'vs/base/common/uri'; +import { basename } from 'vs/base/common/paths'; +import { LIGHT, FileThemeIcon, FolderThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { FileKind } from 'vs/platform/files/common/files'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { deepClone } from 'vs/base/common/objects'; +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'; +import { renderMarkdown, RenderOptions } from 'vs/base/browser/htmlContentRenderer'; +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 { ITreeItem } from 'sql/workbench/common/views'; +import { IOEShimService } from 'sql/parts/objectExplorer/common/objectExplorerViewTreeShim'; import { equalsIgnoreCase } from 'vs/base/common/strings'; -import { IOEShimService } from 'sql/parts/objectExplorer/common/objectExplorerViewTreeShim'; -import { ITreeItem } from 'sql/workbench/common/views'; -import { IProgressService2 } from 'vs/platform/progress/common/progress'; +class TitleMenus implements IDisposable { + + private disposables: IDisposable[] = []; + private titleDisposable: IDisposable = Disposable.None; + private titleActions: IAction[] = []; + private titleSecondaryActions: IAction[] = []; + + private _onDidChangeTitle = new Emitter(); + get onDidChangeTitle(): Event { return this._onDidChangeTitle.event; } + + constructor( + id: string, + @IContextKeyService private contextKeyService: IContextKeyService, + @IMenuService private menuService: IMenuService, + ) { + if (this.titleDisposable) { + this.titleDisposable.dispose(); + this.titleDisposable = Disposable.None; + } + + const _contextKeyService = this.contextKeyService.createScoped(); + _contextKeyService.createKey('view', id); + + const titleMenu = this.menuService.createMenu(MenuId.ViewTitle, _contextKeyService); + const updateActions = () => { + this.titleActions = []; + this.titleSecondaryActions = []; + fillInActionBarActions(titleMenu, undefined, { primary: this.titleActions, secondary: this.titleSecondaryActions }); + this._onDidChangeTitle.fire(); + }; + + const listener = titleMenu.onDidChange(updateActions); + updateActions(); + + this.titleDisposable = toDisposable(() => { + listener.dispose(); + titleMenu.dispose(); + _contextKeyService.dispose(); + this.titleActions = []; + this.titleSecondaryActions = []; + }); + } + + getTitleActions(): IAction[] { + return this.titleActions; + } + + getTitleSecondaryActions(): IAction[] { + return this.titleSecondaryActions; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} class Root implements ITreeItem { - constructor(public readonly childProvider: string) { } - label = { - label: 'root' - }; + label = { label: 'root' }; handle = '0'; parentHandle = null; collapsibleState = TreeItemCollapsibleState.Expanded; children = void 0; } +const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); + export class CustomTreeView extends Disposable implements ITreeView { private isVisible: boolean = false; private activated: boolean = false; private _hasIconForParentNode = false; private _hasIconForLeafNode = false; + private _showCollapseAllAction = false; + private focused: boolean = false; + private domNode: HTMLElement; private treeContainer: HTMLElement; + private _messageValue: string | IMarkdownString | undefined; + private messageElement: HTMLDivElement; private tree: FileIconThemableWorkbenchTree; private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; + private menus: TitleMenus; - private _dataProvider: ITreeViewDataProvider; + private markdownRenderer: MarkdownRenderer; + private markdownResult: IMarkdownRenderResult; private _onDidExpandItem: Emitter = this._register(new Emitter()); readonly onDidExpandItem: Event = this._onDidExpandItem.event; @@ -78,9 +148,6 @@ export class CustomTreeView extends Disposable implements ITreeView { private _onDidChangeActions: Emitter = this._register(new Emitter()); readonly onDidChangeActions: Event = this._onDidChangeActions.event; - public showCollapseAllAction: boolean = false; - public message = undefined; - constructor( private id: string, private container: ViewContainer, @@ -88,10 +155,13 @@ export class CustomTreeView extends Disposable implements ITreeView { @IWorkbenchThemeService private themeService: IWorkbenchThemeService, @IInstantiationService private instantiationService: IInstantiationService, @ICommandService private commandService: ICommandService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService, + @IProgressService2 private progressService: IProgressService2 ) { super(); - this.root = new Root(id); + this.root = new Root(); + this.menus = this._register(instantiationService.createInstance(TitleMenus, this.id)); + this._register(this.menus.onDidChangeTitle(() => this._onDidChangeActions.fire())); this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.themeService.onThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.configurationService.onDidChangeConfiguration(e => { @@ -99,13 +169,50 @@ export class CustomTreeView extends Disposable implements ITreeView { this.doRefresh([this.root]); /** soft refresh **/ } })); + this.markdownRenderer = instantiationService.createInstance(MarkdownRenderer); + this._register(toDisposable(() => { + if (this.markdownResult) { + this.markdownResult.dispose(); + } + })); + this.create(); } + private _dataProvider: ITreeViewDataProvider; get dataProvider(): ITreeViewDataProvider { return this._dataProvider; } set dataProvider(dataProvider: ITreeViewDataProvider) { + if (dataProvider) { + this._dataProvider = new class implements ITreeViewDataProvider { + 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; + }); + } + }; + this.updateMessage(); + this.refresh(); + } else { + this._dataProvider = null; + this.updateMessage(); + } + } + + private _message: string | IMarkdownString | undefined; + get message(): string | IMarkdownString | undefined { + return this._message; + } + + set message(message: string | IMarkdownString | undefined) { + this._message = message; + this.updateMessage(); } get hasIconForParentNode(): boolean { @@ -120,33 +227,28 @@ export class CustomTreeView extends Disposable implements ITreeView { return this.isVisible; } - expand(itemOrItems: ITreeItem | ITreeItem[]): Thenable { - if (this.tree) { - itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - return this.tree.expandAll(itemOrItems); - } - return Promise.arguments(null); + get showCollapseAllAction(): boolean { + return this._showCollapseAllAction; } - setSelection(items: ITreeItem[]): void { - if (this.tree) { - this.tree.setSelection(items, { source: 'api' }); - } - } - - setFocus(item: ITreeItem): void { - if (this.tree) { - this.focus(); - this.tree.setFocus(item); + set showCollapseAllAction(showCollapseAllAction: boolean) { + if (this._showCollapseAllAction !== !!showCollapseAllAction) { + this._showCollapseAllAction = !!showCollapseAllAction; + this._onDidChangeActions.fire(); } } getPrimaryActions(): IAction[] { - return []; + 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()); + return [...this.menus.getTitleActions(), collapseAllAction]; + } else { + return this.menus.getTitleActions(); + } } getSecondaryActions(): IAction[] { - return []; + return this.menus.getTitleSecondaryActions(); } setVisibility(isVisible: boolean): void { @@ -183,30 +285,37 @@ export class CustomTreeView extends Disposable implements ITreeView { } focus(): void { - if (this.tree) { + 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) { - this.tree.reveal(selectedElement, 0.5).then(null, errors.onUnexpectedError); + this.tree.reveal(selectedElement, 0.5); } // Pass Focus to Viewer this.tree.domFocus(); + } else { + this.domNode.focus(); } } show(container: HTMLElement): void { - if (!this.tree) { - this.createTree(); - } - DOM.append(container, this.treeContainer); + DOM.append(container, this.domNode); + } + + private create() { + 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')); + const focusTracker = this._register(DOM.trackFocus(this.domNode)); + this._register(focusTracker.onDidFocus(() => this.focused = true)); + this._register(focusTracker.onDidBlur(() => this.focused = false)); } private createTree() { - this.treeContainer = DOM.$('.tree-explorer-viewlet-tree-view'); const actionItemProvider = (action: IAction) => action instanceof MenuItemAction ? this.instantiationService.createInstance(ContextAwareMenuItemActionItem, action) : undefined; const menus = this.instantiationService.createInstance(TreeMenus, this.id); - const dataSource = this.instantiationService.createInstance(TreeDataSource, this.container); + const dataSource = this.instantiationService.createInstance(TreeDataSource, this, this.container); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, menus, actionItemProvider); const controller = this.instantiationService.createInstance(TreeController, this.id, menus); this.tree = this.instantiationService.createInstance(FileIconThemableWorkbenchTree, this.treeContainer, { dataSource, renderer, controller }, {}); @@ -216,27 +325,72 @@ export class CustomTreeView extends Disposable implements ITreeView { 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.tree.setInput(this.root); + this.tree.setInput(this.root).then(() => this.updateContentAreas()); } + private updateMessage(): void { + if (this._message) { + this.showMessage(this._message); + } else if (!this.dataProvider) { + this.showMessage(noDataProviderMessage); + } else { + this.hideMessage(); + } + this.updateContentAreas(); + } + + private showMessage(message: string | IMarkdownString): void { + DOM.removeClass(this.messageElement, 'hide'); + if (this._messageValue !== message) { + this.resetMessageElement(); + this._messageValue = message; + if (isString(this._messageValue)) { + this.messageElement.textContent = this._messageValue; + } else { + this.markdownResult = this.markdownRenderer.render(this._messageValue); + DOM.append(this.messageElement, this.markdownResult.element); + } + this.layout(this._size); + } + } + + private hideMessage(): void { + this.resetMessageElement(); + DOM.addClass(this.messageElement, 'hide'); + this.layout(this._size); + } + + private resetMessageElement(): void { + if (this.markdownResult) { + this.markdownResult.dispose(); + this.markdownResult = null; + } + DOM.clearNode(this.messageElement); + } + + private _size: number; layout(size: number) { - if (this.tree) { - this.treeContainer.style.height = size + 'px'; - this.tree.layout(size); + if (size) { + this._size = size; + const treeSize = size - DOM.getTotalHeight(this.messageElement); + this.treeContainer.style.height = treeSize + 'px'; + if (this.tree) { + this.tree.layout(treeSize); + } } } getOptimalWidth(): number { if (this.tree) { const parentNode = this.tree.getHTMLElement(); - const childNodes = [].slice.call(parentNode.querySelectorAll('.outline-item-label > a')); + const childNodes = ([] as Element[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a')); return DOM.getLargestChildWidth(parentNode, childNodes); } return 0; } refresh(elements?: ITreeItem[]): Promise { - if (this.tree) { + if (this.dataProvider && this.tree) { elements = elements || [this.root]; for (const element of elements) { element.children = null; // reset children @@ -250,50 +404,74 @@ export class CustomTreeView extends Disposable implements ITreeView { return Promise.resolve(null); } - reveal(item: ITreeItem, parentChain?: ITreeItem[], options?: { select?: boolean, focus?: boolean }): Thenable { - if (this.tree && this.isVisible) { - options = options ? options : { select: false, focus: false }; - const select = isUndefinedOrNull(options.select) ? false : options.select; - const focus = isUndefinedOrNull(options.focus) ? false : options.focus; - - const root: Root = this.tree.getInput(); - const promise = root.children ? Promise.resolve(null) : this.refresh(); // Refresh if root is not populated - return promise.then(() => { - var result = TPromise.as(null); - if (parentChain) { - parentChain.forEach((e) => { - result = result.then(() => this.tree.expand(e)); - }); - } - return result.then(() => this.tree.reveal(item)) - .then(() => { - if (select) { - this.tree.setSelection([item], { source: 'api' }); - } - if (focus) { - this.focus(); - this.tree.setFocus(item); - } - }); - }); + expand(itemOrItems: ITreeItem | ITreeItem[]): Thenable { + if (this.tree) { + itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; + return this.tree.expandAll(itemOrItems); } - return Promise.resolve(null); + return Promise.arguments(null); + } + + setSelection(items: ITreeItem[]): void { + if (this.tree) { + this.tree.setSelection(items, { source: 'api' }); + } + } + + setFocus(item: ITreeItem): void { + if (this.tree) { + this.focus(); + this.tree.setFocus(item); + } + } + + reveal(item: ITreeItem): Thenable { + if (this.tree) { + return this.tree.reveal(item); + } + return Promise.arguments(null); } private activate() { if (!this.activated) { - this.extensionService.activateByEvent(`onView:${this.id}`); + this.createTree(); + this.progressService.withProgress({ location: this.container.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) + .then(() => timeout(2000)) + .then(() => { + this.updateMessage(); + }); this.activated = true; } } + private refreshing: boolean = false; private doRefresh(elements: ITreeItem[]): Promise { if (this.tree) { - return Promise.all(elements.map(e => this.tree.refresh(e))).then(() => null); + this.refreshing = true; + return Promise.all(elements.map(e => this.tree.refresh(e))) + .then(() => { + this.refreshing = false; + this.updateContentAreas(); + if (this.focused) { + this.focus(); + } + }); } return Promise.resolve(null); } + 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) { + DOM.addClass(this.treeContainer, 'hide'); + this.domNode.setAttribute('tabindex', '0'); + } else { + DOM.removeClass(this.treeContainer, 'hide'); + this.domNode.removeAttribute('tabindex'); + } + } + private onSelection({ payload }: any): void { if (payload && (!!payload.didClickOnTwistie || payload.source === 'api')) { return; @@ -316,6 +494,7 @@ export class CustomTreeView extends Disposable implements ITreeView { class TreeDataSource implements IDataSource { constructor( + private treeView: ITreeView, private container: ViewContainer, @IProgressService2 private progressService: IProgressService2, @IOEShimService private objectExplorerService: IOEShimService @@ -327,31 +506,28 @@ class TreeDataSource implements IDataSource { } hasChildren(tree: ITree, node: ITreeItem): boolean { - return this.objectExplorerService.providerExists(node.childProvider) && node.collapsibleState !== TreeItemCollapsibleState.None; + 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): TPromise { - if (this.objectExplorerService.providerExists(node.childProvider)) { - return TPromise.wrap(this.progressService.withProgress({ location: this.container.id }, () => { - // this is replicating what vscode does when calling initial children - if (node instanceof Root) { - node = deepClone(node); - node.handle = undefined; - } - // we need to pass this as a parameter mainly to maintain sessions - // hopefully we don't need anything like this when we remove the shim - return this.objectExplorerService.getChildren(node, this); - })); + getChildren(tree: ITree, node: ITreeItem): Promise { + if (node.childProvider) { + return this.progressService.withProgress({ location: this.container.id }, () => this.objectExplorerService.getChildren(node, this)); } - return TPromise.as([]); + if (this.treeView.dataProvider) { + return this.progressService.withProgress({ location: this.container.id }, () => this.treeView.dataProvider.getChildren(node)); + } + return Promise.resolve([]); } shouldAutoexpand(tree: ITree, node: ITreeItem): boolean { return node.collapsibleState === TreeItemCollapsibleState.Expanded; } - getParent(tree: ITree, node: any): TPromise { - return TPromise.as(null); + getParent(tree: ITree, node: any): Promise { + return Promise.resolve(null); } } @@ -362,6 +538,31 @@ interface ITreeExplorerTemplateData { 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}; }`); + } + 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; }`); + } + 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 codeBackground = theme.getColor(textCodeBlockBackground); + if (codeBackground) { + collector.addRule(`.tree-explorer-viewlet-tree-view > .message code { background-color: ${codeBackground}; }`); + } +}); + class TreeRenderer implements IRenderer { private static readonly ITEM_HEIGHT = 22; @@ -375,6 +576,7 @@ class TreeRenderer implements IRenderer { @IInstantiationService private instantiationService: IInstantiationService, @IWorkbenchThemeService private themeService: IWorkbenchThemeService, @IConfigurationService private configurationService: IConfigurationService, + @ILabelService private labelService: ILabelService ) { } @@ -390,7 +592,7 @@ class TreeRenderer implements IRenderer { 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, {}); + const resourceLabel = this.instantiationService.createInstance(ResourceLabel, container, { supportHighlights: true, donotSupportOcticons: true }); DOM.addClass(resourceLabel.element, 'custom-view-tree-node-item-resourceLabel'); const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); const actionBar = new ActionBar(actionsContainer, { @@ -403,28 +605,27 @@ class TreeRenderer implements IRenderer { renderElement(tree: ITree, node: ITreeItem, templateId: string, templateData: ITreeExplorerTemplateData): void { const resource = node.resourceUri ? URI.revive(node.resourceUri) : null; - const label = node.label ? node.label.label : resource ? basename(resource.path) : ''; - let icon = this.themeService.getTheme().type === LIGHT ? node.icon.path : node.iconDark.path; + 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 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; // reset templateData.resourceLabel.clear(); templateData.actionBar.clear(); - if ((resource || node.themeIcon) && !icon) { + if (resource || node.themeIcon) { const fileDecorations = this.configurationService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); - templateData.resourceLabel.setLabel({ name: label, resource: resource ? resource : URI.parse('_icon_resource') }, { fileKind: this.getFileKind(node), title, fileDecorations: fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'] }); + templateData.resourceLabel.setLabel({ 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 }); } else { - templateData.resourceLabel.setLabel({ name: label }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'] }); + templateData.resourceLabel.setLabel({ name: label, description }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'], matches }); } - if (templateId === TreeRenderer.TREE_TEMPLATE_ID) { - templateData.icon.style.backgroundImage = icon ? `url('${icon}')` : ''; - } else { - DOM.addClass(templateData.icon, 'icon'); - DOM.addClass(templateData.icon, icon); - } - DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!icon); + templateData.icon.style.backgroundImage = iconUrl ? `url('${iconUrl.toString(true)}')` : ''; + DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!iconUrl); templateData.actionBar.context = ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }); templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); @@ -489,7 +690,7 @@ class Aligner extends Disposable { return parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); } - private hasIcon(node: vsITreeItem): boolean { + private hasIcon(node: ITreeItem): boolean { const icon = this.themeService.getTheme().type === LIGHT ? node.icon : node.iconDark; if (icon) { return true; @@ -566,7 +767,7 @@ class MultipleSelectionActionRunner extends ActionRunner { super(); } - runAction(action: IAction, context: any): TPromise { + runAction(action: IAction, context: any): Thenable { if (action instanceof MenuItemAction) { const selection = this.getSelectedResources(); const filteredSelection = selection.filter(s => s !== context); @@ -618,3 +819,39 @@ class TreeMenus extends Disposable implements IDisposable { return result; } } + +class MarkdownRenderer { + + constructor( + @IOpenerService private readonly _openerService: IOpenerService + ) { + } + + private getOptions(disposeables: IDisposable[]): RenderOptions { + return { + actionHandler: { + callback: (content) => { + let uri: URI | undefined; + try { + uri = URI.parse(content); + } catch { + // ignore + } + if (uri && this._openerService) { + this._openerService.open(uri).catch(onUnexpectedError); + } + }, + disposeables + } + }; + } + + render(markdown: IMarkdownString): IMarkdownRenderResult { + let disposeables: IDisposable[] = []; + const element: HTMLElement = markdown ? renderMarkdown(markdown, this.getOptions(disposeables)) : document.createElement('span'); + return { + element, + dispose: () => dispose(disposeables) + }; + } +} diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 0f9d60bb10..272c9d780e 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -98,7 +98,7 @@ export const enum MenuId { ViewTitle, // {{SQL CARBON EDIT}} ObjectExplorerItemContext, - NotebookToolbar, + NotebookToolbar } export interface IMenuActionOptions { diff --git a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts index 251da50ac3..179a4b7d75 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts @@ -5,20 +5,13 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; -// {{SQL CARBON EDIT}} -import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, ViewsRegistry, ICustomViewDescriptor, IRevealOptions, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, ViewsRegistry, ICustomViewDescriptor, IRevealOptions } from 'vs/workbench/common/views'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { isUndefinedOrNull, isNumber } from 'vs/base/common/types'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -// {{SQL CARBON EDIT}} -import * as sqlops from 'sqlops'; -import { Emitter } from 'vs/base/common/event'; -import { generateUuid } from 'vs/base/common/uuid'; -import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService'; - @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -29,8 +22,6 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie extHostContext: IExtHostContext, @IViewsService private viewsService: IViewsService, @INotificationService private notificationService: INotificationService, - // {{SQL CARBON EDIT}} - @IObjectExplorerService private objectExplorerService: IObjectExplorerService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); @@ -39,10 +30,6 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void { const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService); this._dataProviders.set(treeViewId, dataProvider); - // {{SQL CARBON EDIT}} - if (this.checkForDataExplorer(treeViewId)) { - return; - } const viewer = this.getTreeView(treeViewId); if (viewer) { viewer.dataProvider = dataProvider; @@ -127,25 +114,6 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie return viewDescriptor ? viewDescriptor.treeView : null; } - // {{SQL CARBON EDIT}} - private checkForDataExplorer(treeViewId: string): boolean { - const viewDescriptor: ICustomViewDescriptor = ViewsRegistry.getView(treeViewId); - if (viewDescriptor.container.id === 'workbench.view.dataExplorer') { - const dataProvider = new OETreeViewDataProvider(treeViewId, this._proxy); - this.objectExplorerService.registerProvider(treeViewId, dataProvider); - dataProvider.registerOnExpandCompleted(e => this.objectExplorerService.onNodeExpanded({ - errorMessage: e.errorMessage, - nodePath: e.nodePath, - nodes: e.nodes, - sessionId: e.sessionId, - providerId: treeViewId - })); - dataProvider.registerOnSessionCreated(e => this.objectExplorerService.onSessionCreated(undefined, e)); - viewDescriptor.treeView.refresh(); - } - return false; - } - dispose(): void { this._dataProviders.forEach((dataProvider, treeViewId) => { const treeView = this.getTreeView(treeViewId); @@ -239,100 +207,3 @@ export class TreeViewDataProvider implements ITreeViewDataProvider { } } } - -// {{SQL CARBON EDIT}} -export class OETreeViewDataProvider implements sqlops.ObjectExplorerProvider { - - protected itemsMap: Map = new Map(); - private onExpandComplete = new Emitter(); - private onSessionCreated = new Emitter(); - - private sessionId: string; - - handle: number; - readonly providerId = this.treeViewId; - - constructor(protected treeViewId: string, - protected _proxy: ExtHostTreeViewsShape - ) { - } - - public createNewSession(connInfo: sqlops.ConnectionInfo): Thenable { - // no op - this.sessionId = generateUuid(); - setTimeout(() => { - this.onSessionCreated.fire({ - sessionId: this.sessionId, - errorMessage: undefined, - rootNode: { - errorMessage: undefined, - iconType: undefined, - isLeaf: undefined, - label: undefined, - metadata: undefined, - nodePath: undefined, - nodeStatus: undefined, - nodeSubType: undefined, - nodeType: undefined - }, - success: true - }); - }); - return Promise.resolve({ sessionId: this.sessionId }); - } - - public expandNode(nodeInfo: sqlops.ExpandNodeInfo): Thenable { - this._proxy.$getChildren(this.treeViewId, nodeInfo.nodePath).then(e => { - this.onExpandComplete.fire({ - errorMessage: undefined, - nodePath: nodeInfo.nodePath, - nodes: e.map(e => { - return { - nodePath: e.handle, - label: e.label.label, - iconType: e.icon.path, - // this is just needed since we don't have this - nodeSubType: e.iconDark.path, - isLeaf: e.collapsibleState === TreeItemCollapsibleState.None, - childProvider: e.childProvider, - payload: e.payload, - nodeType: e.contextValue - }; - }), - sessionId: this.sessionId - }); - }); - return Promise.resolve(true); - } - - public refreshNode(nodeInfo: sqlops.ExpandNodeInfo): Thenable { - // no op - return Promise.resolve(true); - } - - public closeSession(closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): Thenable { - // no op - return Promise.resolve({ sessionId: undefined, success: true }); - } - - public findNodes(findNodesInfo: sqlops.FindNodesInfo): Thenable { - // no op - return Promise.resolve({ nodes: [] }); - } - - public registerOnSessionCreated(handler: (response: sqlops.ObjectExplorerSession) => any): void { - // no op - this.onSessionCreated.event(handler); - return; - } - - public registerOnSessionDisconnected?(handler: (response: sqlops.ObjectExplorerSession) => any): void { - // no op - return; - } - - public registerOnExpandCompleted(handler: (response: sqlops.ObjectExplorerExpandInfo) => any): void { - this.onExpandComplete.event(handler); - return; - } -}