diff --git a/src/sql/parts/registeredServer/common/objectExplorerService.ts b/src/sql/parts/registeredServer/common/objectExplorerService.ts index 80c1252310..366444ef16 100644 --- a/src/sql/parts/registeredServer/common/objectExplorerService.ts +++ b/src/sql/parts/registeredServer/common/objectExplorerService.ts @@ -5,7 +5,7 @@ 'use strict'; import { NodeType } from 'sql/parts/registeredServer/common/nodeType'; -import { TreeNode } from 'sql/parts/registeredServer/common/treeNode'; +import { TreeNode, TreeItemCollapsibleState, ObjectExplorerCallbacks } from 'sql/parts/registeredServer/common/treeNode'; import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -20,6 +20,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { warn, error } from 'sql/base/common/log'; import { ServerTreeView } from 'sql/parts/registeredServer/viewlet/serverTreeView'; import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; +import * as vscode from 'vscode'; export const SERVICE_ID = 'ObjectExplorerService'; @@ -36,7 +37,7 @@ export interface IObjectExplorerService { refreshNode(providerId: string, session: sqlops.ObjectExplorerSession, nodePath: string): Thenable; - expandTreeNode(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable; + resolveTreeNodeChildren(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable; refreshTreeNode(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable; @@ -66,16 +67,19 @@ export interface IObjectExplorerService { onSelectionOrFocusChange: Event; getServerTreeView(): ServerTreeView; + + getActiveConnectionNodes(): TreeNode[]; + + getTreeNode(connectionId: string, nodePath: string): Thenable; } interface SessionStatus { nodes: { [nodePath: string]: NodeStatus }; connection: ConnectionProfile; - } interface NodeStatus { - expandHandler: (result: sqlops.ObjectExplorerExpandInfo) => void; + expandEmitter: Emitter; } export interface ObjectExplorerNodeEventArgs { @@ -83,6 +87,10 @@ export interface ObjectExplorerNodeEventArgs { errorMessage: string; } +export interface NodeInfoWithConnection { + connectionId: string; + nodeInfo: sqlops.NodeInfo; +} export class ObjectExplorerService implements IObjectExplorerService { @@ -153,8 +161,8 @@ export class ObjectExplorerService implements IObjectExplorerService { } let nodeStatus = this._sessions[expandResponse.sessionId].nodes[expandResponse.nodePath]; - if (nodeStatus && nodeStatus.expandHandler) { - nodeStatus.expandHandler(expandResponse); + if (nodeStatus && nodeStatus.expandEmitter) { + nodeStatus.expandEmitter.fire(expandResponse); } else { warn(`Cannot find node status for session: ${expandResponse.sessionId} and node path: ${expandResponse.nodePath}`); } @@ -268,24 +276,33 @@ export class ObjectExplorerService implements IObjectExplorerService { let self = this; return new Promise((resolve, reject) => { if (session.sessionId in self._sessions && self._sessions[session.sessionId]) { - self._sessions[session.sessionId].nodes[nodePath] = { - expandHandler: ((expandResult) => { - if (expandResult && !expandResult.errorMessage) { - resolve(expandResult); - } - else { - reject(expandResult ? expandResult.errorMessage : undefined); - } + let newRequest = false; + if (!self._sessions[session.sessionId].nodes[nodePath]) { + self._sessions[session.sessionId].nodes[nodePath] = { + expandEmitter: new Emitter() + }; + newRequest = true; + } + self._sessions[session.sessionId].nodes[nodePath].expandEmitter.event(((expandResult) => { + if (expandResult && !expandResult.errorMessage) { + resolve(expandResult); + } + else { + reject(expandResult ? expandResult.errorMessage : undefined); + } + if (newRequest) { delete self._sessions[session.sessionId].nodes[nodePath]; - }) - }; - self.callExpandOrRefreshFromProvider(provider, { - sessionId: session ? session.sessionId : undefined, - nodePath: nodePath - }, refresh).then(result => { - }, error => { - reject(error); - }); + } + })); + if (newRequest) { + self.callExpandOrRefreshFromProvider(provider, { + sessionId: session.sessionId, + nodePath: nodePath + }, refresh).then(result => { + }, error => { + reject(error); + }); + } } else { reject(`session cannot find to expand node. id: ${session.sessionId} nodePath: ${nodePath}`); } @@ -323,7 +340,7 @@ export class ObjectExplorerService implements IObjectExplorerService { this._disposables = dispose(this._disposables); } - public expandTreeNode(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable { + public resolveTreeNodeChildren(session: sqlops.ObjectExplorerSession, parentTree: TreeNode): Thenable { return this.expandOrRefreshTreeNode(session, parentTree); } @@ -377,7 +394,12 @@ export class ObjectExplorerService implements IObjectExplorerService { } return new TreeNode(nodeInfo.nodeType, nodeInfo.label, isLeaf, nodeInfo.nodePath, - nodeInfo.nodeSubType, nodeInfo.nodeStatus, parent, nodeInfo.metadata); + nodeInfo.nodeSubType, nodeInfo.nodeStatus, parent, nodeInfo.metadata, { + getChildren: treeNode => this.getChildren(treeNode), + isExpanded: treeNode => this.isExpanded(treeNode), + setNodeExpandedState: (treeNode, expandedState) => this.setNodeExpandedState(treeNode, expandedState), + setNodeSelected: (treeNode, selected, clearOtherSelections: boolean = undefined) => this.setNodeSelected(treeNode, selected, clearOtherSelections) + }); } public registerServerTreeView(view: ServerTreeView): void { @@ -424,4 +446,96 @@ export class ObjectExplorerService implements IObjectExplorerService { public getServerTreeView() { return this._serverTreeView; } + + public getActiveConnectionNodes(): TreeNode[] { + return Object.values(this._activeObjectExplorerNodes); + } + + private async setNodeExpandedState(treeNode: TreeNode, expandedState: TreeItemCollapsibleState): Promise { + treeNode = await this.getUpdatedTreeNode(treeNode); + let expandNode = this.getTreeItem(treeNode); + if (expandedState === TreeItemCollapsibleState.Expanded) { + await this._serverTreeView.reveal(expandNode); + } + return this._serverTreeView.setExpandedState(expandNode, expandedState); + } + + private async setNodeSelected(treeNode: TreeNode, selected: boolean, clearOtherSelections: boolean = undefined): Promise { + treeNode = await this.getUpdatedTreeNode(treeNode); + let selectNode = this.getTreeItem(treeNode); + if (selected) { + await this._serverTreeView.reveal(selectNode); + } + return this._serverTreeView.setSelected(selectNode, selected, clearOtherSelections); + } + + private async getChildren(treeNode: TreeNode): Promise { + treeNode = await this.getUpdatedTreeNode(treeNode); + if (treeNode.isAlwaysLeaf) { + return []; + } + if (!treeNode.children) { + await this.resolveTreeNodeChildren(treeNode.getSession(), treeNode); + } + return treeNode.children; + } + + private async isExpanded(treeNode: TreeNode): Promise { + treeNode = await this.getUpdatedTreeNode(treeNode); + do { + let expandNode = this.getTreeItem(treeNode); + if (!this._serverTreeView.isExpanded(expandNode)) { + return false; + } + treeNode = treeNode.parent; + } while (treeNode); + + return true; + } + + private getTreeItem(treeNode: TreeNode): TreeNode | ConnectionProfile { + let rootNode = this._activeObjectExplorerNodes[treeNode.getConnectionProfile().id]; + if (treeNode === rootNode) { + return treeNode.connection; + } + return treeNode; + } + + private getUpdatedTreeNode(treeNode: TreeNode): Promise { + return this.getTreeNode(treeNode.getConnectionProfile().id, treeNode.nodePath).then(treeNode => { + if (!treeNode) { + throw new Error(nls.localize('treeNodeNoLongerExists', 'The given tree node no longer exists')); + } + return treeNode; + }); + } + + public async getTreeNode(connectionId: string, nodePath: string): Promise { + let parentNode = this._activeObjectExplorerNodes[connectionId]; + if (!parentNode) { + return undefined; + } + if (!nodePath) { + return parentNode; + } + let currentNode = parentNode; + while (currentNode.nodePath !== nodePath) { + let nextNode = undefined; + if (!currentNode.isAlwaysLeaf && !currentNode.children) { + await this.resolveTreeNodeChildren(currentNode.getSession(), currentNode); + } + if (currentNode.children) { + // Look at the next node in the path, which is the child object with the longest path where the desired path starts with the child path + let children = currentNode.children.filter(child => nodePath.startsWith(child.nodePath)); + if (children.length > 0) { + nextNode = children.reduce((currentMax, candidate) => currentMax.nodePath.length < candidate.nodePath.length ? candidate : currentMax); + } + } + if (!nextNode) { + return undefined; + } + currentNode = nextNode; + } + return currentNode; + } } \ No newline at end of file diff --git a/src/sql/parts/registeredServer/common/treeNode.ts b/src/sql/parts/registeredServer/common/treeNode.ts index 7d654c3ba9..86f9711427 100644 --- a/src/sql/parts/registeredServer/common/treeNode.ts +++ b/src/sql/parts/registeredServer/common/treeNode.ts @@ -11,6 +11,19 @@ import * as sqlops from 'sqlops'; import * as UUID from 'vs/base/common/uuid'; +export enum TreeItemCollapsibleState { + None = 0, + Collapsed = 1, + Expanded = 2 +} + +export interface ObjectExplorerCallbacks { + getChildren(treeNode: TreeNode): Thenable; + isExpanded(treeNode: TreeNode): Thenable; + setNodeExpandedState(TreeNode: TreeNode, expandedState: TreeItemCollapsibleState): Thenable; + setNodeSelected(TreeNode: TreeNode, selected: boolean, clearOtherSelections?: boolean): Thenable; +} + export class TreeNode { /** * id for TreeNode @@ -59,8 +72,8 @@ export class TreeNode { public nodeStatus: string; /** - * Children of this node - */ + * Children of this node + */ public children: TreeNode[]; @@ -108,8 +121,38 @@ export class TreeNode { return false; } + public toNodeInfo(): sqlops.NodeInfo { + return { + nodePath: this.nodePath, + nodeType: this.nodeTypeId, + nodeSubType: this.nodeSubType, + nodeStatus: this.nodeStatus, + label: this.label, + isLeaf: this.isAlwaysLeaf, + metadata: this.metadata, + errorMessage: this.errorStateMessage + }; + } + + public getChildren(): Thenable { + return this._objectExplorerCallbacks.getChildren(this); + } + + public isExpanded(): Thenable { + return this._objectExplorerCallbacks.isExpanded(this); + } + + public setExpandedState(expandedState: TreeItemCollapsibleState): Thenable { + return this._objectExplorerCallbacks.setNodeExpandedState(this, expandedState); + } + + public setSelected(selected: boolean, clearOtherSelections?: boolean): Thenable { + return this._objectExplorerCallbacks.setNodeSelected(this, selected, clearOtherSelections); + } + constructor(nodeTypeId: string, label: string, isAlwaysLeaf: boolean, nodePath: string, - nodeSubType: string, nodeStatus: string, parent: TreeNode, metadata: sqlops.ObjectMetadata) { + nodeSubType: string, nodeStatus: string, parent: TreeNode, metadata: sqlops.ObjectMetadata, + private _objectExplorerCallbacks: ObjectExplorerCallbacks) { this.nodeTypeId = nodeTypeId; this.label = label; this.isAlwaysLeaf = isAlwaysLeaf; diff --git a/src/sql/parts/registeredServer/viewlet/serverTreeDataSource.ts b/src/sql/parts/registeredServer/viewlet/serverTreeDataSource.ts index e6c6024cc2..84a0106422 100644 --- a/src/sql/parts/registeredServer/viewlet/serverTreeDataSource.ts +++ b/src/sql/parts/registeredServer/viewlet/serverTreeDataSource.ts @@ -74,7 +74,7 @@ export class ServerTreeDataSource implements IDataSource { if (node.children) { resolve(node.children); } else { - this._objectExplorerService.expandTreeNode(node.getSession(), node).then(() => { + this._objectExplorerService.resolveTreeNodeChildren(node.getSession(), node).then(() => { resolve(node.children); }, expandError => { this.showError(expandError); diff --git a/src/sql/parts/registeredServer/viewlet/serverTreeView.ts b/src/sql/parts/registeredServer/viewlet/serverTreeView.ts index 22bed16467..d0a4c1d08e 100644 --- a/src/sql/parts/registeredServer/viewlet/serverTreeView.ts +++ b/src/sql/parts/registeredServer/viewlet/serverTreeView.ts @@ -28,6 +28,7 @@ import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesServ import { Button } from 'sql/base/browser/ui/button/button'; import { attachButtonStyler } from 'sql/common/theme/styler'; import Event, { Emitter } from 'vs/base/common/event'; +import { TreeNode, TreeItemCollapsibleState } from 'sql/parts/registeredServer/common/treeNode'; const $ = builder.$; @@ -428,6 +429,48 @@ export class ServerTreeView { return this._tree.isDOMFocused(); } + /** + * Set whether the given element is expanded or collapsed + */ + public setExpandedState(element: TreeNode | ConnectionProfile, expandedState: TreeItemCollapsibleState): Thenable { + if (expandedState === TreeItemCollapsibleState.Collapsed) { + return this._tree.collapse(element); + } else if (expandedState === TreeItemCollapsibleState.Expanded) { + return this._tree.expand(element); + } + return Promise.resolve(); + } + + /** + * Reveal the given element in the tree + */ + public reveal(element: TreeNode | ConnectionProfile): Thenable { + return this._tree.reveal(element); + } + + /** + * Select the given element in the tree and clear any other selections + */ + public setSelected(element: TreeNode | ConnectionProfile, selected: boolean, clearOtherSelections: boolean): Thenable { + if (clearOtherSelections || (selected && clearOtherSelections !== false)) { + this._tree.clearSelection(); + } + if (selected) { + this._tree.select(element); + return this._tree.reveal(element); + } else { + this._tree.deselect(element); + return Promise.resolve(); + } + } + + /** + * Check if the given element in the tree is expanded + */ + public isExpanded(element: TreeNode | ConnectionProfile): boolean { + return this._tree.isExpanded(element); + } + /** * dispose the server tree view */ diff --git a/src/sql/parts/registeredServer/viewlet/treeUpdateUtils.ts b/src/sql/parts/registeredServer/viewlet/treeUpdateUtils.ts index 53ccec1e7f..1014edcb03 100644 --- a/src/sql/parts/registeredServer/viewlet/treeUpdateUtils.ts +++ b/src/sql/parts/registeredServer/viewlet/treeUpdateUtils.ts @@ -209,7 +209,7 @@ export class TreeUpdateUtils { } else { var rootNode = objectExplorerService.getObjectExplorerNode(connection); if (rootNode) { - objectExplorerService.expandTreeNode(rootNode.getSession(), rootNode).then(() => { + objectExplorerService.resolveTreeNodeChildren(rootNode.getSession(), rootNode).then(() => { resolve(rootNode.children); }, expandError => { resolve([]); diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index 7f899d0b27..f73f06d7f7 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -108,6 +108,67 @@ declare module 'sqlops' { } } + /** + * Namespace for interacting with Object Explorer + */ + export namespace objectexplorer { + /** + * Get an Object Explorer node corresponding to the given connection and path. If no path + * is given, it returns the top-level node for the given connection. If there is no node at + * the given path, it returns undefined. + * @param {string} connectionId The id of the connection that the node exists on + * @param {string?} nodePath The path of the node to get + * @returns {ObjectExplorerNode} The node corresponding to the given connection and path, + * or undefined if no such node exists. + */ + export function getNode(connectionId: string, nodePath?: string): Thenable; + + /** + * Get all active Object Explorer connection nodes + * @returns {ObjectExplorerNode[]} The Object Explorer nodes for each saved connection + */ + export function getActiveConnectionNodes(): Thenable; + + /** + * Interface for representing and interacting with items in Object Explorer + */ + export interface ObjectExplorerNode extends NodeInfo { + /** + * The id of the connection that the node exists under + */ + connectionId: string; + + /** + * Whether the node is currently expanded in Object Explorer + */ + isExpanded(): Thenable; + + /** + * Set whether the node is expanded or collapsed + * @param expandedState The new state of the node. If 'None', the node will not be changed + */ + setExpandedState(expandedState: vscode.TreeItemCollapsibleState): Thenable; + + /** + * Set whether the node is selected + * @param selected Whether the node should be selected + * @param clearOtherSelections If true, clear any other selections. If false, leave any existing selections. + * Defaults to true when selected is true and false when selected is false. + */ + setSelected(selected: boolean, clearOtherSelections?: boolean): Thenable; + + /** + * Get all the child nodes. Returns an empty list if there are no children. + */ + getChildren(): Thenable; + + /** + * Get the parent node. Returns undefined if there is none. + */ + getParent(): Thenable; + } + } + // EXPORTED INTERFACES ///////////////////////////////////////////////// export interface ConnectionInfo { diff --git a/src/sql/workbench/api/node/extHostObjectExplorer.ts b/src/sql/workbench/api/node/extHostObjectExplorer.ts new file mode 100644 index 0000000000..ac567dc27f --- /dev/null +++ b/src/sql/workbench/api/node/extHostObjectExplorer.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; +import { ExtHostObjectExplorerShape, SqlMainContext, MainThreadObjectExplorerShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; + +export class ExtHostObjectExplorer implements ExtHostObjectExplorerShape { + + private _proxy: MainThreadObjectExplorerShape; + + constructor( + threadService: IThreadService + ) { + this._proxy = threadService.get(SqlMainContext.MainThreadObjectExplorer); + } + + public $getNode(connectionId: string, nodePath?: string): Thenable { + return this._proxy.$getNode(connectionId, nodePath).then(nodeInfo => nodeInfo === undefined ? undefined : new ExtHostObjectExplorerNode(nodeInfo, connectionId, this._proxy)); + } + + public $getActiveConnectionNodes(): Thenable { + return this._proxy.$getActiveConnectionNodes().then(results => results.map(result => new ExtHostObjectExplorerNode(result.nodeInfo, result.connectionId, this._proxy))); + } +} + +class ExtHostObjectExplorerNode implements sqlops.objectexplorer.ObjectExplorerNode { + public connectionId: string; + public nodePath: string; + public nodeType: string; + public nodeSubType: string; + public nodeStatus: string; + public label: string; + public isLeaf: boolean; + public metadata: sqlops.ObjectMetadata; + public errorMessage: string; + + constructor(nodeInfo: sqlops.NodeInfo, connectionId: string, private _proxy: MainThreadObjectExplorerShape) { + Object.entries(nodeInfo).forEach(([key, value]) => this[key] = value); + this.connectionId = connectionId; + } + + isExpanded(): Thenable { + return this._proxy.$isExpanded(this.connectionId, this.nodePath); + } + + setExpandedState(expandedState: vscode.TreeItemCollapsibleState): Thenable { + return this._proxy.$setExpandedState(this.connectionId, this.nodePath, expandedState); + } + + setSelected(selected: boolean, clearOtherSelections: boolean = undefined): Thenable { + return this._proxy.$setSelected(this.connectionId, this.nodePath, selected, clearOtherSelections); + } + + getChildren(): Thenable { + return this._proxy.$getChildren(this.connectionId, this.nodePath).then(children => children.map(nodeInfo => new ExtHostObjectExplorerNode(nodeInfo, this.connectionId, this._proxy))); + } + + getParent(): Thenable { + let parentPathEndIndex = this.nodePath.lastIndexOf('/'); + if (parentPathEndIndex === -1) { + return Promise.resolve(undefined); + } + return this._proxy.$getNode(this.connectionId, this.nodePath.slice(0, parentPathEndIndex)).then(nodeInfo => nodeInfo ? new ExtHostObjectExplorerNode(nodeInfo, this.connectionId, this._proxy) : undefined); + } +} diff --git a/src/sql/workbench/api/node/mainThreadObjectExplorer.ts b/src/sql/workbench/api/node/mainThreadObjectExplorer.ts new file mode 100644 index 0000000000..d335a638e8 --- /dev/null +++ b/src/sql/workbench/api/node/mainThreadObjectExplorer.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { SqlExtHostContext, SqlMainContext, ExtHostObjectExplorerShape, MainThreadObjectExplorerShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; +import { IObjectExplorerService, NodeInfoWithConnection } from 'sql/parts/registeredServer/common/objectExplorerService'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import * as TaskUtilities from 'sql/workbench/common/taskUtilities'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; + +@extHostNamedCustomer(SqlMainContext.MainThreadObjectExplorer) +export class MainThreadObjectExplorer implements MainThreadObjectExplorerShape { + + private _proxy: ExtHostObjectExplorerShape; + private _toDispose: IDisposable[]; + + constructor( + extHostContext: IExtHostContext, + @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, + @IObjectExplorerService private _objectExplorerService: IObjectExplorerService, + @IWorkbenchEditorService private _workbenchEditorService: IWorkbenchEditorService + ) { + if (extHostContext) { + this._proxy = extHostContext.get(SqlExtHostContext.ExtHostObjectExplorer); + } + this._toDispose = []; + } + + public dispose(): void { + this._toDispose = dispose(this._toDispose); + } + + public $getNode(connectionId: string, nodePath?: string): Thenable { + return this._objectExplorerService.getTreeNode(connectionId, nodePath).then(treeNode => { + if (!treeNode) { + return undefined; + } + return treeNode.toNodeInfo(); + }); + } + + public $getActiveConnectionNodes(): Thenable { + let connectionNodes = this._objectExplorerService.getActiveConnectionNodes(); + return Promise.resolve(connectionNodes.map(node => { + return {connectionId: node.connection.id, nodeInfo: node.toNodeInfo()}; + })); + } + + public $setExpandedState(connectionId: string, nodePath: string, expandedState: vscode.TreeItemCollapsibleState): Thenable { + return this._objectExplorerService.getTreeNode(connectionId, nodePath).then(treeNode => treeNode.setExpandedState(expandedState)); + } + + public $setSelected(connectionId: string, nodePath: string, selected: boolean, clearOtherSelections: boolean = undefined): Thenable { + return this._objectExplorerService.getTreeNode(connectionId, nodePath).then(treeNode => treeNode.setSelected(selected, clearOtherSelections)); + } + + public $getChildren(connectionId: string, nodePath: string): Thenable { + return this._objectExplorerService.getTreeNode(connectionId, nodePath).then(treeNode => treeNode.getChildren().then(children => children.map(node => node.toNodeInfo()))); + } + + public $isExpanded(connectionId: string, nodePath: string): Thenable { + return this._objectExplorerService.getTreeNode(connectionId, nodePath).then(treeNode => treeNode.isExpanded()); + } +} diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 92b2d0a1f7..9c8472f592 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -32,6 +32,7 @@ import { IExtensionApiFactory } from 'vs/workbench/api/node/extHost.api.impl'; import { ExtHostDashboardWebviews } from 'sql/workbench/api/node/extHostDashboardWebview'; import { ExtHostConnectionManagement } from 'sql/workbench/api/node/extHostConnectionManagement'; import { ExtHostDashboard } from 'sql/workbench/api/node/extHostDashboard'; +import { ExtHostObjectExplorer } from 'sql/workbench/api/node/extHostObjectExplorer'; export interface ISqlExtensionApiFactory { vsCodeFactory(extension: IExtensionDescription): typeof vscode; @@ -56,6 +57,7 @@ export function createApiFactory( const extHostConnectionManagement = threadService.set(SqlExtHostContext.ExtHostConnectionManagement, new ExtHostConnectionManagement(threadService)); const extHostCredentialManagement = threadService.set(SqlExtHostContext.ExtHostCredentialManagement, new ExtHostCredentialManagement(threadService)); const extHostDataProvider = threadService.set(SqlExtHostContext.ExtHostDataProtocol, new ExtHostDataProtocol(threadService)); + const extHostObjectExplorer = threadService.set(SqlExtHostContext.ExtHostObjectExplorer, new ExtHostObjectExplorer(threadService)); const extHostSerializationProvider = threadService.set(SqlExtHostContext.ExtHostSerializationProvider, new ExtHostSerializationProvider(threadService)); const extHostResourceProvider = threadService.set(SqlExtHostContext.ExtHostResourceProvider, new ExtHostResourceProvider(threadService)); const extHostModalDialogs = threadService.set(SqlExtHostContext.ExtHostModalDialogs, new ExtHostModalDialogs(threadService)); @@ -105,6 +107,16 @@ export function createApiFactory( } }; + // namespace: objectexplorer + const objectExplorer: typeof sqlops.objectexplorer = { + getNode(connectionId: string, nodePath?: string): Thenable { + return extHostObjectExplorer.$getNode(connectionId, nodePath); + }, + getActiveConnectionNodes(): Thenable { + return extHostObjectExplorer.$getActiveConnectionNodes(); + } + }; + // namespace: serialization const serialization: typeof sqlops.serialization = { registerProvider(provider: sqlops.SerializationProvider): vscode.Disposable { @@ -286,6 +298,7 @@ export function createApiFactory( accounts, connection, credentials, + objectexplorer: objectExplorer, resources, serialization, dataprotocol, diff --git a/src/sql/workbench/api/node/sqlExtHost.contribution.ts b/src/sql/workbench/api/node/sqlExtHost.contribution.ts index 295972905d..6a92d80239 100644 --- a/src/sql/workbench/api/node/sqlExtHost.contribution.ts +++ b/src/sql/workbench/api/node/sqlExtHost.contribution.ts @@ -13,6 +13,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import 'sql/workbench/api/node/mainThreadConnectionManagement'; import 'sql/workbench/api/node/mainThreadCredentialManagement'; import 'sql/workbench/api/node/mainThreadDataProtocol'; +import 'sql/workbench/api/node/mainThreadObjectExplorer'; import 'sql/workbench/api/node/mainThreadSerializationProvider'; import 'sql/workbench/api/node/mainThreadResourceProvider'; import 'sql/workbench/api/electron-browser/mainThreadTasks'; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index a5a859684b..aeb83f9843 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -13,6 +13,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks'; @@ -305,7 +306,6 @@ export abstract class ExtHostDataProtocolShape { $stopSession(handle: number, sessionId: string): Thenable { throw ni(); } } - /** * ResourceProvider extension host class. */ @@ -419,6 +419,7 @@ export const SqlMainContext = { MainThreadConnectionManagement: createMainId('MainThreadConnectionManagement'), MainThreadCredentialManagement: createMainId('MainThreadCredentialManagement'), MainThreadDataProtocol: createMainId('MainThreadDataProtocol'), + MainThreadObjectExplorer: createMainId('MainThreadObjectExplorer'), MainThreadSerializationProvider: createMainId('MainThreadSerializationProvider'), MainThreadResourceProvider: createMainId('MainThreadResourceProvider'), MainThreadModalDialog: createMainId('MainThreadModalDialog'), @@ -432,6 +433,7 @@ export const SqlExtHostContext = { ExtHostConnectionManagement: createExtId('ExtHostConnectionManagement'), ExtHostCredentialManagement: createExtId('ExtHostCredentialManagement'), ExtHostDataProtocol: createExtId('ExtHostDataProtocol'), + ExtHostObjectExplorer: createExtId('ExtHostObjectExplorer'), ExtHostSerializationProvider: createExtId('ExtHostSerializationProvider'), ExtHostResourceProvider: createExtId('ExtHostResourceProvider'), ExtHostModalDialogs: createExtId('ExtHostModalDialogs'), @@ -485,3 +487,15 @@ export interface MainThreadDashboardWebviewShape extends IDisposable { $registerProvider(widgetId: string); $setHtml(handle: number, value: string); } + +export interface ExtHostObjectExplorerShape { +} + +export interface MainThreadObjectExplorerShape extends IDisposable { + $getNode(connectionId: string, nodePath?: string): Thenable; + $getActiveConnectionNodes(): Thenable<{ nodeInfo: sqlops.NodeInfo, connectionId: string}[]>; + $setExpandedState(connectionId: string, nodePath: string, expandedState: vscode.TreeItemCollapsibleState): Thenable; + $setSelected(connectionId: string, nodePath: string, selected: boolean, clearOtherSelections?: boolean): Thenable; + $getChildren(connectionId: string, nodePath: string): Thenable; + $isExpanded(connectionId: string, nodePath: string): Thenable; +} diff --git a/src/sqltest/parts/connection/connectionTreeActions.test.ts b/src/sqltest/parts/connection/connectionTreeActions.test.ts index 2c096bc4e1..5f4bb5713f 100644 --- a/src/sqltest/parts/connection/connectionTreeActions.test.ts +++ b/src/sqltest/parts/connection/connectionTreeActions.test.ts @@ -72,8 +72,8 @@ suite('SQL Connection Tree Action tests', () => { function createObjectExplorerService(connectionManagementService: TestConnectionManagementService): TypeMoq.Mock { let objectExplorerService = TypeMoq.Mock.ofType(ObjectExplorerService, TypeMoq.MockBehavior.Strict, connectionManagementService); objectExplorerService.callBase = true; - objectExplorerService.setup(x => x.getObjectExplorerNode(TypeMoq.It.isAny())).returns(() => new TreeNode('', '', false, '', '', '', undefined, undefined)); - objectExplorerService.setup(x => x.getObjectExplorerNode(undefined)).returns(() => new TreeNode('', '', false, '', '', '', undefined, undefined)); + objectExplorerService.setup(x => x.getObjectExplorerNode(TypeMoq.It.isAny())).returns(() => new TreeNode('', '', false, '', '', '', undefined, undefined, undefined)); + objectExplorerService.setup(x => x.getObjectExplorerNode(undefined)).returns(() => new TreeNode('', '', false, '', '', '', undefined, undefined, undefined)); objectExplorerService.setup(x => x.onUpdateObjectExplorerNodes).returns(() => new Emitter().event); objectExplorerService.setup(x => x.onUpdateObjectExplorerNodes).returns(() => new Emitter().event); @@ -144,7 +144,7 @@ suite('SQL Connection Tree Action tests', () => { saveProfile: true, id: 'testId' }); - let treeNode = new TreeNode(NodeType.Database, 'db node', false, '', '', '', undefined, undefined); + let treeNode = new TreeNode(NodeType.Database, 'db node', false, '', '', '', undefined, undefined, undefined); treeNode.connection = connection; var actionContext = new ObjectExplorerActionsContext(); actionContext.treeNode = treeNode; @@ -395,11 +395,11 @@ suite('SQL Connection Tree Action tests', () => { errorMessage: '' }; - var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', null, null); + var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', null, null, undefined); tablesNode.connection = connection; tablesNode.session = objectExplorerSession; - var table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null); - var table2Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null); + var table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined); + var table2Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined); tablesNode.children = [table1Node, table2Node]; let objectExplorerService = TypeMoq.Mock.ofType(ObjectExplorerService, TypeMoq.MockBehavior.Loose, connectionManagementService.object); objectExplorerService.callBase = true; @@ -486,11 +486,11 @@ suite('SQL Connection Tree Action tests', () => { errorMessage: '' }; - var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', null, null); + var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', null, null, undefined); tablesNode.connection = connection; tablesNode.session = objectExplorerSession; - var table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null); - var table2Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null); + var table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined); + var table2Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\tables\dbo.Table1', '', '', tablesNode, null, undefined); tablesNode.children = [table1Node, table2Node]; let objectExplorerService = TypeMoq.Mock.ofType(ObjectExplorerService, TypeMoq.MockBehavior.Loose, connectionManagementService.object); objectExplorerService.callBase = true; diff --git a/src/sqltest/parts/connection/objectExplorerService.test.ts b/src/sqltest/parts/connection/objectExplorerService.test.ts index 5d924fa9ef..ed8ae21380 100644 --- a/src/sqltest/parts/connection/objectExplorerService.test.ts +++ b/src/sqltest/parts/connection/objectExplorerService.test.ts @@ -10,7 +10,7 @@ import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile import { ConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup'; import { ObjectExplorerService } from 'sql/parts/registeredServer/common/objectExplorerService'; import { NodeType } from 'sql/parts/registeredServer/common/nodeType'; -import { TreeNode } from 'sql/parts/registeredServer/common/treeNode'; +import { TreeNode, TreeItemCollapsibleState, ObjectExplorerCallbacks } from 'sql/parts/registeredServer/common/treeNode'; import { TPromise } from 'vs/base/common/winjs.base'; import * as sqlops from 'sqlops'; @@ -37,11 +37,12 @@ suite('SQL Object Explorer Service tests', () => { let sessionId = '1234'; let failedSessionId = '12345'; let numberOfFailedSession: number = 0; + let serverTreeView: TypeMoq.Mock; setup(() => { let NodeInfoTable1 = { - nodePath: 'testServerName\tables\dbo.Table1', + nodePath: 'testServerName/tables/dbo.Table1', nodeType: NodeType.Table, label: 'dbo.Table1', isLeaf: false, @@ -51,7 +52,7 @@ suite('SQL Object Explorer Service tests', () => { errorMessage: '' }; let NodeInfoTable2 = { - nodePath: 'testServerName\tables\dbo.Table2', + nodePath: 'testServerName/tables/dbo.Table2', nodeType: NodeType.Table, label: 'dbo.Table2', isLeaf: false, @@ -62,7 +63,7 @@ suite('SQL Object Explorer Service tests', () => { }; let NodeInfoTable3 = { - nodePath: 'testServerName\tables\dbo.Table3', + nodePath: 'testServerName/tables/dbo.Table3', nodeType: NodeType.Table, label: 'dbo.Table3', isLeaf: false, @@ -76,7 +77,7 @@ suite('SQL Object Explorer Service tests', () => { success: true, sessionId: sessionId, rootNode: { - nodePath: 'testServerName\tables', + nodePath: 'testServerName/tables', nodeType: NodeType.Folder, label: 'Tables', isLeaf: false, @@ -284,6 +285,14 @@ suite('SQL Object Explorer Service tests', () => { numberOfFailedSession++; } }); + + serverTreeView = TypeMoq.Mock.ofInstance({ + setExpandedState: (element, expandedState) => Promise.resolve() as Thenable, + reveal: element => Promise.resolve() as Thenable, + setSelected: (element, selected, clearOtherSelections) => undefined, + isExpanded: element => undefined, + onSelectionOrFocusChange: Event.None + } as ServerTreeView); }); test('create new session should create session successfully', (done) => { @@ -332,7 +341,7 @@ suite('SQL Object Explorer Service tests', () => { test('expand node should expand node correctly', (done) => { objectExplorerService.createNewSession('MSSQL', connection).then(result => { objectExplorerService.onSessionCreated(1, objectExplorerSession); - objectExplorerService.expandNode('MSSQL', objectExplorerSession, 'testServerName\tables').then(expandInfo => { + objectExplorerService.expandNode('MSSQL', objectExplorerSession, 'testServerName/tables').then(expandInfo => { assert.equal(expandInfo !== null || expandInfo !== undefined, true); assert.equal(expandInfo.sessionId, '1234'); assert.equal(expandInfo.nodes.length, 2); @@ -350,7 +359,7 @@ suite('SQL Object Explorer Service tests', () => { test('refresh node should refresh node correctly', (done) => { objectExplorerService.createNewSession('MSSQL', connection).then(result => { objectExplorerService.onSessionCreated(1, objectExplorerSession); - objectExplorerService.refreshNode('MSSQL', objectExplorerSession, 'testServerName\tables').then(expandInfo => { + objectExplorerService.refreshNode('MSSQL', objectExplorerSession, 'testServerName/tables').then(expandInfo => { assert.equal(expandInfo !== null || expandInfo !== undefined, true); assert.equal(expandInfo.sessionId, '1234'); assert.equal(expandInfo.nodes.length, 2); @@ -365,19 +374,19 @@ suite('SQL Object Explorer Service tests', () => { }); }); - test('expand tree node should children correctly', (done) => { - var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\tables', '', '', null, null); + test('expand tree node should get correct children', (done) => { + var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName/tables', '', '', null, null, undefined); tablesNode.connection = connection; objectExplorerService.createNewSession('MSSQL', connection).then(result => { objectExplorerService.onSessionCreated(1, objectExplorerSession); - objectExplorerService.expandTreeNode(objectExplorerSession, tablesNode).then(children => { + objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, tablesNode).then(children => { assert.equal(children !== null || children !== undefined, true); assert.equal(children[0].label, 'dbo.Table1'); assert.equal(children[0].parent, tablesNode); - assert.equal(children[0].nodePath, 'testServerName\tables\dbo.Table1'); + assert.equal(children[0].nodePath, 'testServerName/tables/dbo.Table1'); assert.equal(children[1].label, 'dbo.Table2'); assert.equal(children[1].parent, tablesNode); - assert.equal(children[1].nodePath, 'testServerName\tables\dbo.Table2'); + assert.equal(children[1].nodePath, 'testServerName/tables/dbo.Table2'); done(); }, err => { // Must call done here so test indicates it's finished if errors occur @@ -387,7 +396,7 @@ suite('SQL Object Explorer Service tests', () => { }); test('refresh tree node should children correctly', (done) => { - var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\tables', '', '', null, null); + var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName/tables', '', '', null, null, undefined); tablesNode.connection = connection; objectExplorerService.createNewSession('MSSQL', connection).then(result => { objectExplorerService.onSessionCreated(1, objectExplorerSession); @@ -395,10 +404,10 @@ suite('SQL Object Explorer Service tests', () => { assert.equal(children !== null || children !== undefined, true); assert.equal(children[0].label, 'dbo.Table1'); assert.equal(children[0].parent, tablesNode); - assert.equal(children[0].nodePath, 'testServerName\tables\dbo.Table1'); + assert.equal(children[0].nodePath, 'testServerName/tables/dbo.Table1'); assert.equal(children[1].label, 'dbo.Table3'); assert.equal(children[1].parent, tablesNode); - assert.equal(children[1].nodePath, 'testServerName\tables\dbo.Table3'); + assert.equal(children[1].nodePath, 'testServerName/tables/dbo.Table3'); done(); }, err => { // Must call done here so test indicates it's finished if errors occur @@ -416,7 +425,7 @@ suite('SQL Object Explorer Service tests', () => { assert.equal(treeNode.getSession(), objectExplorerSession); assert.equal(treeNode.getConnectionProfile(), connection); assert.equal(treeNode.label, 'Tables'); - assert.equal(treeNode.nodePath, 'testServerName\tables'); + assert.equal(treeNode.nodePath, 'testServerName/tables'); done(); }, err => { // Must call done here so test indicates it's finished if errors occur @@ -450,13 +459,13 @@ suite('SQL Object Explorer Service tests', () => { name: 'Db1', schema: null }; - var databaseNode = new TreeNode(NodeType.Database, 'Db1', false, 'testServerName\Db1', '', '', null, databaseMetaData); + var databaseNode = new TreeNode(NodeType.Database, 'Db1', false, 'testServerName\\Db1', '', '', null, databaseMetaData, undefined); databaseNode.connection = connection; databaseNode.session = objectExplorerSession; - var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\Db1\tables', '', '', databaseNode, null); + var tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\\Db1\\tables', '', '', databaseNode, null, undefined); databaseNode.children = [tablesNode]; - var table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\Db1\tables\dbo.Table1', '', '', tablesNode, null); - var table2Node = new TreeNode(NodeType.Table, 'dbo.Table2', false, 'testServerName\Db1\tables\dbo.Table2', '', '', tablesNode, null); + var table1Node = new TreeNode(NodeType.Table, 'dbo.Table1', false, 'testServerName\\Db1\\tables\\dbo.Table1', '', '', tablesNode, null, undefined); + var table2Node = new TreeNode(NodeType.Table, 'dbo.Table2', false, 'testServerName\\Db1\\tables\\dbo.Table2', '', '', tablesNode, null, undefined); tablesNode.children = [table1Node, table2Node]; assert.equal(table1Node.getSession(), objectExplorerSession); assert.equal(table1Node.getConnectionProfile(), connection); @@ -475,7 +484,7 @@ suite('SQL Object Explorer Service tests', () => { test('getSelectedProfileAndDatabase returns the profile but no database if children of a server are selected', () => { let serverTreeView = TypeMoq.Mock.ofInstance({ getSelection: () => undefined, onSelectionOrFocusChange: Event.None } as ServerTreeView); - let databaseNode = new TreeNode(NodeType.Folder, 'Folder1', false, 'testServerName\\Folder1', '', '', undefined, undefined); + let databaseNode = new TreeNode(NodeType.Folder, 'Folder1', false, 'testServerName\\Folder1', '', '', undefined, undefined, undefined); databaseNode.connection = connection; serverTreeView.setup(x => x.getSelection()).returns(() => [databaseNode]); objectExplorerService.registerServerTreeView(serverTreeView.object); @@ -495,8 +504,8 @@ suite('SQL Object Explorer Service tests', () => { schema: undefined }; let databaseName = 'Db1'; - let databaseNode = new TreeNode(NodeType.Database, databaseName, false, 'testServerName\\Db1', '', '', undefined, databaseMetadata); - let tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\\Db1\\tables', '', '', databaseNode, undefined); + let databaseNode = new TreeNode(NodeType.Database, databaseName, false, 'testServerName\\Db1', '', '', undefined, databaseMetadata, undefined); + let tablesNode = new TreeNode(NodeType.Folder, 'Tables', false, 'testServerName\\Db1\\tables', '', '', databaseNode, undefined, undefined); databaseNode.connection = connection; databaseNode.children = [tablesNode]; serverTreeView.setup(x => x.getSelection()).returns(() => [tablesNode]); @@ -515,4 +524,208 @@ suite('SQL Object Explorer Service tests', () => { let selectedProfileAndDatabase = objectExplorerService.getSelectedProfileAndDatabase(); assert.equal(selectedProfileAndDatabase, undefined); }); + + test('isExpanded returns true when the node and its parents are expanded', (done) => { + let table1NodePath = objectExplorerExpandInfo.nodes[0].nodePath; + let tableExpandInfo = { + sessionId: sessionId, + nodes: [], + errorMessage: '', + nodePath: table1NodePath + }; + serverTreeView.setup(x => x.isExpanded(TypeMoq.It.isAny())).returns(treeNode => { + return treeNode === connection || treeNode.nodePath === table1NodePath; + }); + objectExplorerService.registerServerTreeView(serverTreeView.object); + objectExplorerService.createNewSession('MSSQL', connection).then(result => { + objectExplorerService.onSessionCreated(1, objectExplorerSession); + objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, objectExplorerService.getObjectExplorerNode(connection)).then(childNodes => { + sqlOEProvider.setup(x => x.expandNode(TypeMoq.It.isAny())).callback(() => { + objectExplorerService.onNodeExpanded(1, tableExpandInfo); + }).returns(() => TPromise.as(true)); + let tableNode = childNodes.find(node => node.nodePath === table1NodePath); + objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, tableNode).then(() => { + // If I check whether the table is expanded, the answer should be yes + tableNode.isExpanded().then(isExpanded => { + try { + assert.equal(isExpanded, true); + done(); + } catch (err) { + done(err); + } + }, err => done(err)); + }, err => done(err)); + }, err => done(err)); + }, err => done(err)); + }); + + test('isExpanded returns false when the node is not expanded', (done) => { + let table1NodePath = objectExplorerExpandInfo.nodes[0].nodePath; + serverTreeView.setup(x => x.isExpanded(TypeMoq.It.isAny())).returns(treeNode => { + return treeNode === connection; + }); + objectExplorerService.registerServerTreeView(serverTreeView.object); + objectExplorerService.createNewSession('MSSQL', connection).then(result => { + objectExplorerService.onSessionCreated(1, objectExplorerSession); + objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, objectExplorerService.getObjectExplorerNode(connection)).then(childNodes => { + // If I check whether the table is expanded, the answer should be no because only its parent node is expanded + let tableNode = childNodes.find(node => node.nodePath === table1NodePath); + tableNode.isExpanded().then(isExpanded => { + try { + assert.equal(isExpanded, false); + done(); + } catch (err) { + done(err); + } + }, err => done(err)); + }, err => done(err)); + }, err => done(err)); + }); + + test('isExpanded returns false when the parent of the requested node is not expanded', (done) => { + let table1NodePath = objectExplorerExpandInfo.nodes[0].nodePath; + let tableExpandInfo = { + sessionId: sessionId, + nodes: [], + errorMessage: '', + nodePath: table1NodePath + }; + serverTreeView.setup(x => x.isExpanded(TypeMoq.It.isAny())).returns(treeNode => { + return treeNode.nodePath === table1NodePath; + }); + objectExplorerService.registerServerTreeView(serverTreeView.object); + objectExplorerService.createNewSession('MSSQL', connection).then(result => { + objectExplorerService.onSessionCreated(1, objectExplorerSession); + objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, objectExplorerService.getObjectExplorerNode(connection)).then(childNodes => { + sqlOEProvider.setup(x => x.expandNode(TypeMoq.It.isAny())).callback(() => { + objectExplorerService.onNodeExpanded(1, tableExpandInfo); + }).returns(() => TPromise.as(true)); + objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, childNodes.find(node => node.nodePath === table1NodePath)).then(() => { + // If I check whether the table is expanded, the answer should be yes + let tableNode = childNodes.find(node => node.nodePath === table1NodePath); + tableNode.isExpanded().then(isExpanded => { + try { + assert.equal(isExpanded, false); + done(); + } catch (err) { + done(err); + } + }, err => done(err)); + }, err => done(err)); + }, err => done(err)); + }, err => done(err)); + }); + + test('setting a node to expanded calls expand on the requested tree node', (done) => { + let table1NodePath = objectExplorerExpandInfo.nodes[0].nodePath; + let tableExpandInfo = { + sessionId: sessionId, + nodes: [], + errorMessage: '', + nodePath: table1NodePath + }; + // Set up the OE provider so that the second expand call expands the table + sqlOEProvider.setup(x => x.expandNode(TypeMoq.It.is(nodeInfo => nodeInfo.nodePath === table1NodePath))).callback(() => { + objectExplorerService.onNodeExpanded(1, tableExpandInfo); + }).returns(() => TPromise.as(true)); + serverTreeView.setup(x => x.setExpandedState(TypeMoq.It.isAny(), TypeMoq.It.is(state => state === TreeItemCollapsibleState.Expanded))).returns(treeNode => { + if (treeNode instanceof ConnectionProfile) { + treeNode = objectExplorerService.getObjectExplorerNode(treeNode); + } + return objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, treeNode).then(() => undefined); + }); + serverTreeView.setup(x => x.reveal(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + objectExplorerService.registerServerTreeView(serverTreeView.object); + objectExplorerService.createNewSession('MSSQL', connection).then(result => { + objectExplorerService.onSessionCreated(1, objectExplorerSession); + // If I expand the node, then it should get revealed and expanded + objectExplorerService.getTreeNode(connection.id, table1NodePath).then(tableNode => { + tableNode.setExpandedState(TreeItemCollapsibleState.Expanded).then(() => { + try { + serverTreeView.verify(x => x.setExpandedState(TypeMoq.It.isValue(tableNode), TypeMoq.It.is(state => state === TreeItemCollapsibleState.Expanded)), TypeMoq.Times.once()); + serverTreeView.verify(x => x.reveal(TypeMoq.It.isValue(tableNode)), TypeMoq.Times.once()); + done(); + } catch (err) { + done(err); + } + }, err => done(err)); + }, err => done(err)); + }); + }); + + test('setting a node to collapsed calls collapse on the requested tree node', (done) => { + serverTreeView.setup(x => x.isExpanded(TypeMoq.It.isAny())).returns(treeNode => { + return treeNode === connection; + }); + serverTreeView.setup(x => x.setExpandedState(TypeMoq.It.is(treeNode => treeNode === connection), TypeMoq.It.is(state => state === TreeItemCollapsibleState.Collapsed))).returns(() => Promise.resolve()); + objectExplorerService.registerServerTreeView(serverTreeView.object); + objectExplorerService.createNewSession('MSSQL', connection).then(result => { + objectExplorerService.onSessionCreated(1, objectExplorerSession); + objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, objectExplorerService.getObjectExplorerNode(connection)).then(childNodes => { + // If I collapse the connection node, then the tree's collapse method should get called + objectExplorerService.getTreeNode(connection.id, undefined).then(treeNode => treeNode.setExpandedState(TreeItemCollapsibleState.Collapsed).then(() => { + try { + serverTreeView.verify(x => x.setExpandedState(TypeMoq.It.is(treeNode => treeNode === connection), TypeMoq.It.is(state => state === TreeItemCollapsibleState.Collapsed)), TypeMoq.Times.once()); + done(); + } catch (err) { + done(err); + } + }, err => done(err))); + }, err => done(err)); + }, err => done(err)); + }); + + test('setNodeSelected sets the tree selection to the requested tree node', (done) => { + let table1NodePath = objectExplorerExpandInfo.nodes[0].nodePath; + serverTreeView.setup(x => x.setSelected(TypeMoq.It.is((treeNode: TreeNode) => treeNode.nodePath === table1NodePath), TypeMoq.It.isAny(), undefined)).returns(() => Promise.resolve()); + serverTreeView.setup(x => x.reveal(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + objectExplorerService.registerServerTreeView(serverTreeView.object); + objectExplorerService.createNewSession('MSSQL', connection).then(result => { + objectExplorerService.onSessionCreated(1, objectExplorerSession); + // If I select the table node, then it should be selected and revealed + objectExplorerService.getTreeNode(connection.id, table1NodePath).then(tableNode => { + tableNode.setSelected(true).then(() => { + try { + serverTreeView.verify(x => x.setSelected(TypeMoq.It.isValue(tableNode), TypeMoq.It.isValue(true), undefined), TypeMoq.Times.once()); + serverTreeView.verify(x => x.reveal(TypeMoq.It.isValue(tableNode)), TypeMoq.Times.once()); + done(); + } catch (err) { + done(err); + } + }, err => done(err)); + }, err => done(err)); + }, err => done(err)); + }); + + test('findTreeNode returns the tree node for the relevant node', (done) => { + let table1NodePath = objectExplorerExpandInfo.nodes[0].nodePath; + objectExplorerService.createNewSession('MSSQL', connection).then(result => { + objectExplorerService.onSessionCreated(1, objectExplorerSession); + objectExplorerService.getTreeNode(connection.id, table1NodePath).then(treeNode => { + try { + assert.equal(treeNode.nodePath, objectExplorerExpandInfo.nodes[0].nodePath); + assert.equal(treeNode.nodeTypeId, objectExplorerExpandInfo.nodes[0].nodeType); + assert.equal(treeNode.label, objectExplorerExpandInfo.nodes[0].label); + done(); + } catch (err) { + done(err); + } + }, err => done(err)); + }); + }); + + test('findTreeNode returns undefined if the requested node does not exist', (done) => { + let invalidNodePath = objectExplorerSession.rootNode.nodePath + '/invalidNode'; + objectExplorerService.createNewSession('MSSQL', connection).then(result => { + objectExplorerService.onSessionCreated(1, objectExplorerSession); + objectExplorerService.getTreeNode(connection.id, invalidNodePath).then(nodeInfo => { + try { + assert.equal(nodeInfo, undefined); + done(); + } catch (err) { + done(err); + } + }, err => done(err)); + }); + }); }); \ No newline at end of file