diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts index 60546165a6..ec9d9a5be8 100644 --- a/extensions/azurecore/src/azureResource/azure-resource.d.ts +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -3,8 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TreeDataProvider, TreeItem } from 'vscode'; +import { TreeDataProvider } from 'vscode'; import { DataProvider, Account } from 'sqlops'; +import { TreeItem } from 'sqlops'; export namespace azureResource { export interface IAzureResourceProvider extends DataProvider { diff --git a/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts index b166ac22f9..4493861882 100644 --- a/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/database/databaseTreeDataProvider.ts @@ -16,6 +16,7 @@ import { IAzureResourceDatabaseService, IAzureResourceDatabaseNode } from './int import { AzureResourceDatabase } from './models'; import { AzureResourceItemType } from '../../../azureResource/constants'; import { ApiWrapper } from '../../../apiWrapper'; +import { generateGuid } from '../../utils'; export class AzureResourceDatabaseTreeDataProvider implements azureResource.IAzureResourceTreeDataProvider { public constructor( @@ -54,8 +55,24 @@ export class AzureResourceDatabaseTreeDataProvider implements azureResource.IAzu dark: this._extensionContext.asAbsolutePath('resources/dark/sql_database_inverse.svg'), light: this._extensionContext.asAbsolutePath('resources/light/sql_database.svg') }, - collapsibleState: TreeItemCollapsibleState.None, - contextValue: AzureResourceItemType.database + collapsibleState: process.env.NODE_ENV === 'development' ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None, + contextValue: AzureResourceItemType.database, + payload: { + id: generateGuid(), + connectionName: undefined, + serverName: database.serverFullName, + databaseName: database.name, + userName: database.loginName, + password: '', + authenticationType: 'SqlLogin', + savePassword: true, + groupFullName: '', + groupId: '', + providerName: 'MSSQL', + saveProfile: false, + options: {} + }, + childProvider: 'MSSQL' } }); } diff --git a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts index b564557fcc..f58ca68788 100644 --- a/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts +++ b/extensions/azurecore/src/azureResource/providers/databaseServer/databaseServerTreeDataProvider.ts @@ -16,6 +16,7 @@ import { IAzureResourceDatabaseServerService, IAzureResourceDatabaseServerNode } import { AzureResourceDatabaseServer } from './models'; import { AzureResourceItemType } from '../../../azureResource/constants'; import { ApiWrapper } from '../../../apiWrapper'; +import { generateGuid } from '../../utils'; export class AzureResourceDatabaseServerTreeDataProvider implements azureResource.IAzureResourceTreeDataProvider { public constructor( @@ -54,8 +55,24 @@ export class AzureResourceDatabaseServerTreeDataProvider implements azureResourc dark: this._extensionContext.asAbsolutePath('resources/dark/sql_server_inverse.svg'), light: this._extensionContext.asAbsolutePath('resources/light/sql_server.svg') }, - collapsibleState: TreeItemCollapsibleState.None, - contextValue: AzureResourceItemType.databaseServer + collapsibleState: process.env.NODE_ENV === 'development' ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None, + contextValue: AzureResourceItemType.databaseServer, + payload: { + id: generateGuid(), + connectionName: undefined, + serverName: databaseServer.fullName, + databaseName: databaseServer.defaultDatabaseName, + userName: databaseServer.loginName, + password: '', + authenticationType: 'SqlLogin', + savePassword: true, + groupFullName: '', + groupId: '', + providerName: 'MSSQL', + saveProfile: false, + options: {} + }, + childProvider: 'MSSQL' } }); } diff --git a/src/sql/parts/dataExplorer/common/dataExplorerExtensionPoint.ts b/src/sql/parts/dataExplorer/common/dataExplorerExtensionPoint.ts index 66d0b326f7..72fce6af50 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, CustomTreeViewer } 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 { CustomTreeViewer } from 'sql/workbench/browser/parts/views/customView'; + export const DataExplorerViewlet = { DataExplorer: 'dataExplorer' }; diff --git a/src/sql/parts/objectExplorer/common/objectExplorerViewTreeShim.ts b/src/sql/parts/objectExplorer/common/objectExplorerViewTreeShim.ts new file mode 100644 index 0000000000..f2b679a26d --- /dev/null +++ b/src/sql/parts/objectExplorer/common/objectExplorerViewTreeShim.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ITreeItem } from 'sql/workbench/common/views'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; +import { TreeNode } from 'sql/parts/objectExplorer/common/treeNode'; +import { IConnectionManagementService, ConnectionType } from 'sql/platform/connection/common/connectionManagement'; +import { Deferred } from 'sql/base/common/promise'; +import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService'; +import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService'; + +import { IConnectionProfile } from 'sqlops'; + +import { TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { hash } from 'vs/base/common/hash'; +import { generateUuid } from 'vs/base/common/uuid'; + +export const SERVICE_ID = 'oeShimService'; +export const IOEShimService = createDecorator(SERVICE_ID); + +export interface IOEShimService { + _serviceBrand: any; + getChildren(node: ITreeItem, identifier: any): TPromise; + providerExists(providerId: string): boolean; +} + +export class OEShimService implements IOEShimService { + _serviceBrand: any; + + // maps a datasource to a provider handle to a session + private sessionMap = new Map>(); + private nodeIdMap = new Map(); + + constructor( + @IObjectExplorerService private oe: IObjectExplorerService, + @IConnectionManagementService private cm: IConnectionManagementService, + @IConnectionDialogService private cd: IConnectionDialogService, + @ICapabilitiesService private capabilities: ICapabilitiesService + ) { + } + + private async createSession(providerId: string, node: ITreeItem): TPromise { + let deferred = new Deferred(); + let connProfile = new ConnectionProfile(this.capabilities, node.payload); + connProfile.saveProfile = false; + if (this.cm.providerRegistered(providerId)) { + connProfile = new ConnectionProfile(this.capabilities, await this.cd.openDialogAndWait(this.cm, { connectionType: ConnectionType.default, showDashboard: false }, connProfile, undefined, false)); + } + let sessionResp = await this.oe.createNewSession(providerId, connProfile); + let disp = this.oe.onUpdateObjectExplorerNodes(e => { + if (e.connection.id === connProfile.id) { + let rootNode = this.oe.getSession(sessionResp.sessionId).rootNode; + // this is how we know it was shimmed + if (rootNode.nodePath) { + node.handle = this.oe.getSession(sessionResp.sessionId).rootNode.nodePath; + } + } + disp.dispose(); + deferred.resolve(sessionResp.sessionId); + }); + return TPromise.wrap(deferred.promise); + } + + public async getChildren(node: ITreeItem, identifier: any): TPromise { + try { + if (!this.sessionMap.has(identifier)) { + this.sessionMap.set(identifier, new Map()); + } + if (!this.sessionMap.get(identifier).has(hash(node.payload || node.childProvider))) { + this.sessionMap.get(identifier).set(hash(node.payload || node.childProvider), await this.createSession(node.childProvider, node)); + } + if (this.nodeIdMap.has(node.handle)) { + node.handle = this.nodeIdMap.get(node.handle); + } + let sessionId = this.sessionMap.get(identifier).get(hash(node.payload || node.childProvider)); + let treeNode = new TreeNode(undefined, undefined, undefined, node.handle, undefined, undefined, undefined, undefined, undefined, undefined); + let profile: IConnectionProfile = node.payload || { + providerName: node.childProvider, + authenticationType: undefined, + azureTenantId: undefined, + connectionName: undefined, + databaseName: undefined, + groupFullName: undefined, + groupId: undefined, + id: undefined, + options: undefined, + password: undefined, + savePassword: undefined, + saveProfile: undefined, + serverName: undefined, + userName: undefined, + }; + treeNode.connection = new ConnectionProfile(this.capabilities, profile); + return TPromise.wrap(this.oe.resolveTreeNodeChildren({ + success: undefined, + sessionId, + rootNode: undefined, + errorMessage: undefined + }, treeNode).then(e => e.map(n => this.mapNodeToITreeItem(n, node)))); + } catch (e) { + return TPromise.as([]); + } + } + + 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 { + parentHandle: node.parent.id, + handle, + collapsibleState: node.isAlwaysLeaf ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed, + label: node.label, + icon, + iconDark, + childProvider: node.childProvider || parentNode.childProvider, + providerHandle: parentNode.childProvider, + payload: node.payload || parentNode.payload, + contextValue: node.nodeTypeId + }; + } + + public providerExists(providerId: string): boolean { + return this.oe.providerRegistered(providerId); + } +} diff --git a/src/sql/parts/objectExplorer/common/treeNode.ts b/src/sql/parts/objectExplorer/common/treeNode.ts index a6ab7e7b03..66a2538e76 100644 --- a/src/sql/parts/objectExplorer/common/treeNode.ts +++ b/src/sql/parts/objectExplorer/common/treeNode.ts @@ -25,6 +25,14 @@ export interface ObjectExplorerCallbacks { } export class TreeNode { + /** + * Informs who provides the children to a node, used by data explorer tree view api + */ + public childProvider: string; + /** + * Holds the connection profile for nodes, used by data explorer tree view api + */ + public payload: any; /** * id for TreeNode */ diff --git a/src/sql/platform/connection/common/connectionManagement.ts b/src/sql/platform/connection/common/connectionManagement.ts index 705830a1a1..c0dafc35b5 100644 --- a/src/sql/platform/connection/common/connectionManagement.ts +++ b/src/sql/platform/connection/common/connectionManagement.ts @@ -273,6 +273,7 @@ export interface IConnectionManagementService { */ buildConnectionInfo(connectionString: string, provider?: string): Thenable; + providerRegistered(providerId: string): boolean; /** * Get connection profile by id */ diff --git a/src/sql/platform/connection/common/connectionManagementService.ts b/src/sql/platform/connection/common/connectionManagementService.ts index 489714fc60..1b17a6ed5e 100644 --- a/src/sql/platform/connection/common/connectionManagementService.ts +++ b/src/sql/platform/connection/common/connectionManagementService.ts @@ -143,6 +143,10 @@ export class ConnectionManagementService extends Disposable implements IConnecti this.onDisconnect(() => this.refreshEditorTitles()); } + public providerRegistered(providerId: string): boolean { + return !!this._providers.get(providerId); + } + // Event Emitters public get onAddConnectionProfile(): Event { return this._onAddConnectionProfile.event; diff --git a/src/sql/platform/connection/common/connectionStore.ts b/src/sql/platform/connection/common/connectionStore.ts index 0f0730e79f..44c7fb2cb0 100644 --- a/src/sql/platform/connection/common/connectionStore.ts +++ b/src/sql/platform/connection/common/connectionStore.ts @@ -18,9 +18,6 @@ import { ConnectionProfileGroup, IConnectionProfileGroup } from './connectionPro import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; -import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes'; - -import * as sqlops from 'sqlops'; const MAX_CONNECTIONS_DEFAULT = 25; diff --git a/src/sql/sqlops.d.ts b/src/sql/sqlops.d.ts index 621b805e17..e218f39709 100644 --- a/src/sql/sqlops.d.ts +++ b/src/sql/sqlops.d.ts @@ -234,20 +234,20 @@ declare module 'sqlops' { } /** - * Options for the actions that could happen after connecting is complete - */ + * Options for the actions that could happen after connecting is complete + */ export interface IConnectionCompletionOptions { /** * Save the connection to MRU and settings (only save to setting if profile.saveProfile is set to true) * Default is true. - */ + */ saveConnection: boolean; /** * If true, open the dashboard after connection is complete. * If undefined / false, dashboard won't be opened after connection completes. * Default is false. - */ + */ showDashboard?: boolean; /** @@ -1032,6 +1032,14 @@ declare module 'sqlops' { * will be used instead. */ iconType?: string | SqlThemeIcon; + /** + * Informs who provides the children to a node, used by data explorer tree view api + */ + childProvider?: string; + /** + * Holds the connection profile for nodes, used by data explorer tree view api + */ + payload?: any; } /** @@ -2367,6 +2375,11 @@ declare module 'sqlops' { serverInfo: ServerInfo; } + export class TreeItem extends vscode.TreeItem { + payload?: IConnectionProfile; + childProvider?: string; + } + export namespace tasks { export interface ITaskHandler { diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 374418c0e2..c8be067712 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { nb } from 'sqlops'; -import { TreeItem } from 'vs/workbench/api/node/extHostTypes'; +import { nb, IConnectionProfile } from 'sqlops'; +import * as vsExtTypes from 'vs/workbench/api/node/extHostTypes'; // SQL added extension host types export enum ServiceOptionType { @@ -315,7 +315,7 @@ export interface ToolbarLayout { orientation: Orientation; } -export class TreeComponentItem extends TreeItem { +export class TreeComponentItem extends vsExtTypes.TreeItem { checked?: boolean; } @@ -324,6 +324,11 @@ export enum AzureResource { Sql = 1 } +export class TreeItem extends vsExtTypes.TreeItem { + payload: IConnectionProfile; + providerHandle: string; +} + export interface ServerInfoOption { isBigDataCluster: boolean; clusterEndpoints: ClusterEndpoint; diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index cfd1c877a9..ba5656e18b 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -547,6 +547,7 @@ export function createApiFactory( nb: nb, AzureResource: sqlExtHostTypes.AzureResource, extensions: extensions, + TreeItem: sqlExtHostTypes.TreeItem, }; } }; diff --git a/src/sql/workbench/browser/parts/views/customView.ts b/src/sql/workbench/browser/parts/views/customView.ts new file mode 100644 index 0000000000..71d07b734c --- /dev/null +++ b/src/sql/workbench/browser/parts/views/customView.ts @@ -0,0 +1,583 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * 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, ITreeViewer, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItem as vsITreeItem } from 'vs/workbench/common/views'; +import { IProgressService2 } from 'vs/workbench/services/progress/common/progress'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { FileIconThemableWorkbenchTree } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { Emitter, Event } from 'vs/base/common/event'; +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 { ResourceLabel } from 'vs/workbench/browser/labels'; +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 { equalsIgnoreCase } from 'vs/base/common/strings'; + +import { IOEShimService } from 'sql/parts/objectExplorer/common/objectExplorerViewTreeShim'; +import { ITreeItem } from 'sql/workbench/common/views'; + +class Root implements ITreeItem { + constructor(public readonly childProvider: string) { } + label = 'root'; + handle = '0'; + parentHandle = null; + collapsibleState = TreeItemCollapsibleState.Expanded; + children = void 0; +} + +export class CustomTreeViewer extends Disposable implements ITreeViewer { + + private isVisible: boolean = false; + private activated: boolean = false; + private _hasIconForParentNode = false; + private _hasIconForLeafNode = false; + + private treeContainer: HTMLElement; + private tree: FileIconThemableWorkbenchTree; + private root: ITreeItem; + private elementsToRefresh: ITreeItem[] = []; + + private _dataProvider: ITreeViewDataProvider; + + private _onDidExpandItem: Emitter = this._register(new Emitter()); + readonly onDidExpandItem: Event = this._onDidExpandItem.event; + + private _onDidCollapseItem: Emitter = this._register(new Emitter()); + readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; + + private _onDidChangeSelection: Emitter = this._register(new Emitter()); + readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; + + private _onDidChangeVisibility: Emitter = this._register(new Emitter()); + readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + + constructor( + private id: string, + private container: ViewContainer, + @IExtensionService private extensionService: IExtensionService, + @IWorkbenchThemeService private themeService: IWorkbenchThemeService, + @IInstantiationService private instantiationService: IInstantiationService, + @ICommandService private commandService: ICommandService, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(); + this.root = new Root(id); + 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 => { + if (e.affectsConfiguration('explorer.decorations')) { + this.doRefresh([this.root]); /** soft refresh **/ + } + })); + } + + get dataProvider(): ITreeViewDataProvider { + return this._dataProvider; + } + + set dataProvider(dataProvider: ITreeViewDataProvider) { + } + + get hasIconForParentNode(): boolean { + return this._hasIconForParentNode; + } + + get hasIconForLeafNode(): boolean { + return this._hasIconForLeafNode; + } + + get visible(): boolean { + return this.isVisible; + } + + setVisibility(isVisible: boolean): void { + isVisible = !!isVisible; + if (this.isVisible === isVisible) { + return; + } + + this.isVisible = isVisible; + if (this.isVisible) { + this.activate(); + } + + if (this.tree) { + if (this.isVisible) { + DOM.show(this.tree.getHTMLElement()); + } else { + DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it + } + + if (this.isVisible) { + this.tree.onVisible(); + } else { + this.tree.onHidden(); + } + + if (this.isVisible && this.elementsToRefresh.length) { + this.doRefresh(this.elementsToRefresh); + this.elementsToRefresh = []; + } + } + + this._onDidChangeVisibility.fire(this.isVisible); + } + + focus(): void { + if (this.tree) { + // Make sure the current selected element is revealed + const selectedElement = this.tree.getSelection()[0]; + if (selectedElement) { + this.tree.reveal(selectedElement, 0.5).done(null, errors.onUnexpectedError); + } + + // Pass Focus to Viewer + this.tree.domFocus(); + } + } + + show(container: HTMLElement): void { + if (!this.tree) { + this.createTree(); + } + DOM.append(container, this.treeContainer); + } + + 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 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 }, {}); + this.tree.contextKeyService.createKey(this.id, true); + this._register(this.tree); + this._register(this.tree.onDidChangeSelection(e => this.onSelection(e))); + this._register(this.tree.onDidExpandItem(e => this._onDidExpandItem.fire(e.item.getElement()))); + this._register(this.tree.onDidCollapseItem(e => this._onDidCollapseItem.fire(e.item.getElement()))); + this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.selection))); + this.tree.setInput(this.root); + } + + layout(size: number) { + if (this.tree) { + this.treeContainer.style.height = size + 'px'; + this.tree.layout(size); + } + } + + getOptimalWidth(): number { + if (this.tree) { + const parentNode = this.tree.getHTMLElement(); + const childNodes = [].slice.call(parentNode.querySelectorAll('.outline-item-label > a')); + return DOM.getLargestChildWidth(parentNode, childNodes); + } + return 0; + } + + refresh(elements?: ITreeItem[]): TPromise { + if (this.tree) { + elements = elements || [this.root]; + for (const element of elements) { + element.children = null; // reset children + } + if (this.isVisible) { + return this.doRefresh(elements); + } else { + this.elementsToRefresh.push(...elements); + } + } + return TPromise.as(null); + } + + reveal(item: ITreeItem, parentChain: ITreeItem[], options?: { select?: boolean, focus?: boolean }): TPromise { + 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 ? TPromise.as(null) : this.refresh(); // Refresh if root is not populated + return promise.then(() => { + var result = TPromise.as(null); + 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); + } + }); + }); + } + return TPromise.as(null); + } + + private activate() { + if (!this.activated) { + this.extensionService.activateByEvent(`onView:${this.id}`); + this.activated = true; + } + } + + private doRefresh(elements: ITreeItem[]): TPromise { + if (this.tree) { + return TPromise.join(elements.map(e => this.tree.refresh(e))).then(() => null); + } + return TPromise.as(null); + } + + private onSelection({ payload }: any): void { + if (payload && (!!payload.didClickOnTwistie || payload.source === 'api')) { + return; + } + const selection: ITreeItem = this.tree.getSelection()[0]; + if (selection) { + if (selection.command) { + const originalEvent: KeyboardEvent | MouseEvent = payload && payload.originalEvent; + const isMouseEvent = payload && payload.origin === 'mouse'; + const isDoubleClick = isMouseEvent && originalEvent && originalEvent.detail === 2; + + if (!isMouseEvent || this.tree.openOnSingleClick || isDoubleClick) { + this.commandService.executeCommand(selection.command.id, ...(selection.command.arguments || [])); + } + } + } + } +} + +class TreeDataSource implements IDataSource { + + constructor( + private container: ViewContainer, + @IProgressService2 private progressService: IProgressService2, + @IOEShimService private objectExplorerService: IOEShimService + ) { + } + + getId(tree: ITree, node: ITreeItem): string { + return node.handle; + } + + hasChildren(tree: ITree, node: ITreeItem): boolean { + return this.objectExplorerService.providerExists(node.childProvider) && 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 }, () => { + // 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); + })); + } + return TPromise.as([]); + } + + shouldAutoexpand(tree: ITree, node: ITreeItem): boolean { + return node.collapsibleState === TreeItemCollapsibleState.Expanded; + } + + getParent(tree: ITree, node: any): TPromise { + return TPromise.as(null); + } +} + +interface ITreeExplorerTemplateData { + resourceLabel: ResourceLabel; + icon: HTMLElement; + actionBar: ActionBar; + aligner: Aligner; +} + +class TreeRenderer implements IRenderer { + + private static readonly ITEM_HEIGHT = 22; + private static readonly TREE_TEMPLATE_ID = 'treeExplorer'; + private static readonly MSSQL_TREE_TEMPLATE_ID = 'mssqltreeExplorer'; + + constructor( + private treeViewId: string, + private menus: TreeMenus, + private actionItemProvider: IActionItemProvider, + @IInstantiationService private instantiationService: IInstantiationService, + @IWorkbenchThemeService private themeService: IWorkbenchThemeService, + @IConfigurationService private configurationService: IConfigurationService, + ) { + } + + getHeight(tree: ITree, element: any): number { + return TreeRenderer.ITEM_HEIGHT; + } + + getTemplateId(tree: ITree, element: ITreeItem): string { + return equalsIgnoreCase(element.providerHandle, 'mssql') ? TreeRenderer.MSSQL_TREE_TEMPLATE_ID : TreeRenderer.TREE_TEMPLATE_ID; + } + + renderTemplate(tree: ITree, templateId: string, container: HTMLElement): ITreeExplorerTemplateData { + DOM.addClass(container, 'custom-view-tree-node-item'); + + const icon = DOM.append(container, DOM.$('.custom-view-tree-node-item-icon')); + const resourceLabel = this.instantiationService.createInstance(ResourceLabel, container, {}); + DOM.addClass(resourceLabel.element, 'custom-view-tree-node-item-resourceLabel'); + const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions')); + const actionBar = new ActionBar(actionsContainer, { + actionItemProvider: this.actionItemProvider, + actionRunner: new MultipleSelectionActionRunner(() => tree.getSelection()) + }); + + return { resourceLabel, icon, actionBar, aligner: new Aligner(container, tree, this.themeService) }; + } + + 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 : resource ? basename(resource.path) : ''; + let icon = this.themeService.getTheme().type === LIGHT ? node.icon : node.iconDark; + const title = node.tooltip ? node.tooltip : resource ? void 0 : label; + + // reset + templateData.resourceLabel.clear(); + templateData.actionBar.clear(); + + if ((resource || node.themeIcon) && !icon) { + 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'] }); + } else { + templateData.resourceLabel.setLabel({ name: label }, { title, hideIcon: true, extraClasses: ['custom-view-tree-node-item-resourceLabel'] }); + } + + 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.actionBar.context = ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }); + templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); + + templateData.aligner.treeItem = node; + } + + private getFileKind(node: ITreeItem): FileKind { + if (node.themeIcon) { + switch (node.themeIcon.id) { + case FileThemeIcon.id: + return FileKind.FILE; + case FolderThemeIcon.id: + return FileKind.FOLDER; + } + } + return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE; + } + + disposeTemplate(tree: ITree, templateId: string, templateData: ITreeExplorerTemplateData): void { + templateData.resourceLabel.dispose(); + templateData.actionBar.dispose(); + templateData.aligner.dispose(); + } +} + +class Aligner extends Disposable { + + private _treeItem: ITreeItem; + + constructor( + private container: HTMLElement, + private tree: ITree, + private themeService: IWorkbenchThemeService + ) { + super(); + this._register(this.themeService.onDidFileIconThemeChange(() => this.render())); + } + + set treeItem(treeItem: ITreeItem) { + this._treeItem = treeItem; + this.render(); + } + + private render(): void { + if (this._treeItem) { + DOM.toggleClass(this.container, 'align-icon-with-twisty', this.hasToAlignIconWithTwisty()); + } + } + + private hasToAlignIconWithTwisty(): boolean { + if (this._treeItem.collapsibleState !== TreeItemCollapsibleState.None) { + return false; + } + if (!this.hasIcon(this._treeItem)) { + return false; + + } + const parent: ITreeItem = this.tree.getNavigator(this._treeItem).parent() || this.tree.getInput(); + if (this.hasIcon(parent)) { + return false; + } + return parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c)); + } + + private hasIcon(node: vsITreeItem): boolean { + const icon = this.themeService.getTheme().type === LIGHT ? node.icon : node.iconDark; + if (icon) { + return true; + } + if (node.resourceUri || node.themeIcon) { + const fileIconTheme = this.themeService.getFileIconTheme(); + const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None; + if (isFolder) { + return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons; + } + return fileIconTheme.hasFileIcons; + } + return false; + } +} + +class TreeController extends WorkbenchTreeController { + + constructor( + private treeViewId: string, + private menus: TreeMenus, + @IContextMenuService private contextMenuService: IContextMenuService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IConfigurationService configurationService: IConfigurationService + ) { + super({}, configurationService); + } + + protected shouldToggleExpansion(element: ITreeItem, event: IMouseEvent, origin: string): boolean { + return element.command ? this.isClickOnTwistie(event) : super.shouldToggleExpansion(element, event, origin); + } + + onContextMenu(tree: ITree, node: ITreeItem, event: ContextMenuEvent): boolean { + event.preventDefault(); + event.stopPropagation(); + + tree.setFocus(node); + const actions = this.menus.getResourceContextActions(node); + if (!actions.length) { + return true; + } + const anchor = { x: event.posx, y: event.posy }; + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + + getActions: () => { + return TPromise.as(actions); + }, + + getActionItem: (action) => { + const keybinding = this._keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionItem(action, action, { label: true, keybinding: keybinding.getLabel() }); + } + return null; + }, + + onHide: (wasCancelled?: boolean) => { + if (wasCancelled) { + tree.domFocus(); + } + }, + + getActionsContext: () => ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }), + + actionRunner: new MultipleSelectionActionRunner(() => tree.getSelection()) + }); + + return true; + } +} + +class MultipleSelectionActionRunner extends ActionRunner { + + constructor(private getSelectedResources: () => any[]) { + super(); + } + + runAction(action: IAction, context: any): TPromise { + if (action instanceof MenuItemAction) { + const selection = this.getSelectedResources(); + const filteredSelection = selection.filter(s => s !== context); + + if (selection.length === filteredSelection.length || selection.length === 1) { + return action.run(context); + } + + return action.run(context, ...filteredSelection); + } + + return super.runAction(action, context); + } +} + +class TreeMenus extends Disposable implements IDisposable { + + constructor( + private id: string, + @IContextKeyService private contextKeyService: IContextKeyService, + @IMenuService private menuService: IMenuService, + @IContextMenuService private contextMenuService: IContextMenuService + ) { + super(); + } + + getResourceActions(element: ITreeItem): IAction[] { + return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).primary; + } + + getResourceContextActions(element: ITreeItem): IAction[] { + return this.getActions(MenuId.ViewItemContext, { key: 'viewItem', value: element.contextValue }).secondary; + } + + private getActions(menuId: MenuId, context: { key: string, value: string }): { primary: IAction[]; secondary: IAction[]; } { + const contextKeyService = this.contextKeyService.createScoped(); + contextKeyService.createKey('view', this.id); + contextKeyService.createKey(context.key, context.value); + + const menu = this.menuService.createMenu(menuId, contextKeyService); + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + fillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + + menu.dispose(); + contextKeyService.dispose(); + + return result; + } +} diff --git a/src/sql/workbench/common/views.ts b/src/sql/workbench/common/views.ts index b42b3a2723..5e8f0adc67 100644 --- a/src/sql/workbench/common/views.ts +++ b/src/sql/workbench/common/views.ts @@ -3,9 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITreeViewDataProvider, ITreeItem } from 'vs/workbench/common/views'; +import { ITreeViewDataProvider, ITreeItem as vsITreeItem } from 'vs/workbench/common/views'; +import { IConnectionProfile } from 'sqlops'; -export interface ITreeComponentItem extends ITreeItem { +export interface ITreeComponentItem extends vsITreeItem { checked?: boolean; enabled?: boolean; onCheckedChanged?: (checked: boolean) => void; @@ -15,3 +16,9 @@ export interface ITreeComponentItem extends ITreeItem { export interface IModelViewTreeViewDataProvider extends ITreeViewDataProvider { refresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeComponentItem }); } + +export interface ITreeItem extends vsITreeItem { + providerHandle?: string; + childProvider?: string; + payload?: IConnectionProfile; // its possible we will want this to be more generic +} diff --git a/src/sql/workbench/services/connection/browser/connectionDialogService.ts b/src/sql/workbench/services/connection/browser/connectionDialogService.ts index 5d51b0ecc7..380c601d94 100644 --- a/src/sql/workbench/services/connection/browser/connectionDialogService.ts +++ b/src/sql/workbench/services/connection/browser/connectionDialogService.ts @@ -76,6 +76,10 @@ export class ConnectionDialogService implements IConnectionDialogService { private _connectionErrorTitle = localize('connectionError', 'Connection error'); private _dialogDeferredPromise: Deferred; + /** + * This is used to work around the interconnectedness of this code + */ + private ignoreNextConnect = false; private _connectionManagementService: IConnectionManagementService; constructor( @@ -87,6 +91,7 @@ export class ConnectionDialogService implements IConnectionDialogService { @IClipboardService private _clipboardService: IClipboardService, @ICommandService private _commandService: ICommandService ) { } + /** * Gets the default provider with the following actions * 1. Checks if master provider(map) has data @@ -161,6 +166,13 @@ export class ConnectionDialogService implements IConnectionDialogService { } private handleOnCancel(params: INewConnectionParams): void { + if (this.ignoreNextConnect) { + this._connectionDialog.resetConnection(); + this._connectionDialog.close(); + this.ignoreNextConnect = false; + this._dialogDeferredPromise.resolve(undefined); + return; + } if (this.uiController.databaseDropdownExpanded) { this.uiController.closeDatabaseDropdown(); } else { @@ -182,6 +194,14 @@ export class ConnectionDialogService implements IConnectionDialogService { } private handleDefaultOnConnect(params: INewConnectionParams, connection: IConnectionProfile): Thenable { + if (this.ignoreNextConnect) { + this._connectionDialog.resetConnection(); + this._connectionDialog.close(); + this.ignoreNextConnect = false; + this._connecting = false; + this._dialogDeferredPromise.resolve(connection); + return Promise.resolve(); + } let fromEditor = params && params.connectionType === ConnectionType.editor; let uri: string = undefined; if (fromEditor && params && params.input) { @@ -303,7 +323,12 @@ export class ConnectionDialogService implements IConnectionDialogService { connectionManagementService: IConnectionManagementService, params?: INewConnectionParams, model?: IConnectionProfile, - connectionResult?: IConnectionResult): Thenable { + connectionResult?: IConnectionResult, + doConnect: boolean = true): Thenable { + + if (!doConnect) { + this.ignoreNextConnect = true; + } this._dialogDeferredPromise = new Deferred(); this.showDialog(connectionManagementService, params, diff --git a/src/sql/workbench/services/connection/common/connectionDialogService.ts b/src/sql/workbench/services/connection/common/connectionDialogService.ts index 059a1f8711..87a0b09cdf 100644 --- a/src/sql/workbench/services/connection/common/connectionDialogService.ts +++ b/src/sql/workbench/services/connection/common/connectionDialogService.ts @@ -28,6 +28,6 @@ export interface IConnectionDialogService { * @param model * @param connectionResult */ - openDialogAndWait(connectionManagementService: IConnectionManagementService, params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Thenable; + openDialogAndWait(connectionManagementService: IConnectionManagementService, params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult, doConnect?: boolean): Thenable; } diff --git a/src/sql/workbench/services/objectExplorer/common/objectExplorerService.ts b/src/sql/workbench/services/objectExplorer/common/objectExplorerService.ts index a958b921d3..71250cdce6 100644 --- a/src/sql/workbench/services/objectExplorer/common/objectExplorerService.ts +++ b/src/sql/workbench/services/objectExplorer/common/objectExplorerService.ts @@ -90,6 +90,10 @@ export interface IObjectExplorerService { getNodeActions(connectionId: string, nodePath: string): Thenable; getSessionConnectionProfile(sessionId: string): sqlops.IConnectionProfile; + + getSession(sessionId: string): sqlops.ObjectExplorerSession; + + providerRegistered(providerId: string): boolean; } interface SessionStatus { @@ -152,6 +156,19 @@ export class ObjectExplorerService implements IObjectExplorerService { this._onSelectionOrFocusChange = new Emitter(); } + public getSession(sessionId: string): sqlops.ObjectExplorerSession { + let session = this._sessions[sessionId]; + if (!session) { + return undefined; + } + let node = this._activeObjectExplorerNodes[session.connection.id]; + return node ? node.getSession() : undefined; + } + + public providerRegistered(providerId: string): boolean { + return !!this._providers[providerId]; + } + public get onUpdateObjectExplorerNodes(): Event { return this._onUpdateObjectExplorerNodes.event; } @@ -576,13 +593,16 @@ export class ObjectExplorerService implements IObjectExplorerService { } } - return new TreeNode(nodeInfo.nodeType, nodeInfo.label, isLeaf, nodeInfo.nodePath, + let node = new TreeNode(nodeInfo.nodeType, nodeInfo.label, isLeaf, nodeInfo.nodePath, nodeInfo.nodeSubType, nodeInfo.nodeStatus, parent, nodeInfo.metadata, nodeInfo.iconType, { 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) }); + node.childProvider = nodeInfo.childProvider; + node.payload = nodeInfo.payload; + return node; } public registerServerTreeView(view: ServerTreeView): void { @@ -683,6 +703,9 @@ export class ObjectExplorerService implements IObjectExplorerService { private async setNodeExpandedState(treeNode: TreeNode, expandedState: TreeItemCollapsibleState): Promise { treeNode = await this.getUpdatedTreeNode(treeNode); + if (!treeNode) { + return Promise.resolve(); + } let expandNode = this.getTreeItem(treeNode); if (expandedState === TreeItemCollapsibleState.Expanded) { await this._serverTreeView.reveal(expandNode); @@ -692,6 +715,9 @@ export class ObjectExplorerService implements IObjectExplorerService { private async setNodeSelected(treeNode: TreeNode, selected: boolean, clearOtherSelections: boolean = undefined): Promise { treeNode = await this.getUpdatedTreeNode(treeNode); + if (!treeNode) { + return Promise.resolve(); + } let selectNode = this.getTreeItem(treeNode); if (selected) { await this._serverTreeView.reveal(selectNode); @@ -701,6 +727,9 @@ export class ObjectExplorerService implements IObjectExplorerService { private async getChildren(treeNode: TreeNode): Promise { treeNode = await this.getUpdatedTreeNode(treeNode); + if (!treeNode) { + return Promise.resolve([]); + } if (treeNode.isAlwaysLeaf) { return []; } @@ -712,6 +741,9 @@ export class ObjectExplorerService implements IObjectExplorerService { private async isExpanded(treeNode: TreeNode): Promise { treeNode = await this.getUpdatedTreeNode(treeNode); + if (!treeNode) { + return false; + } do { let expandNode = this.getTreeItem(treeNode); if (!this._serverTreeView.isExpanded(expandNode)) { @@ -734,7 +766,8 @@ export class ObjectExplorerService implements IObjectExplorerService { 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')); + // throw new Error(nls.localize('treeNodeNoLongerExists', 'The given tree node no longer exists')); + return undefined; } return treeNode; }); diff --git a/src/sqltest/stubs/connectionDialogTestService.ts b/src/sqltest/stubs/connectionDialogTestService.ts index 32576b500f..bb1d03a9a6 100644 --- a/src/sqltest/stubs/connectionDialogTestService.ts +++ b/src/sqltest/stubs/connectionDialogTestService.ts @@ -24,4 +24,9 @@ export class ConnectionDialogTestService implements IConnectionDialogService { params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): TPromise { return TPromise.as(undefined); } -} \ No newline at end of file + + public openDialogAndWaitButDontConnect(connectionManagementService: IConnectionManagementService, + params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): TPromise { + return TPromise.as(undefined); + } +} diff --git a/src/sqltest/stubs/connectionManagementService.test.ts b/src/sqltest/stubs/connectionManagementService.test.ts index 50fbe7c6eb..7682fc9372 100644 --- a/src/sqltest/stubs/connectionManagementService.test.ts +++ b/src/sqltest/stubs/connectionManagementService.test.ts @@ -266,6 +266,10 @@ export class TestConnectionManagementService implements IConnectionManagementSer return undefined; } + providerRegistered(providerId: string): boolean { + return undefined; + } + getConnectionProfileById(profileId: string): IConnectionProfile { return undefined; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts index d19a04b35d..bc3f5791d6 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts @@ -7,11 +7,17 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, IExtHostContext } from '../node/extHost.protocol'; -import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeViewer, ViewsRegistry, ICustomViewDescriptor } from 'vs/workbench/common/views'; +import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeViewer, ViewsRegistry, ICustomViewDescriptor, TreeItemCollapsibleState } 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'; +// {{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 { @@ -21,7 +27,9 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie constructor( extHostContext: IExtHostContext, @IViewsService private viewsService: IViewsService, - @INotificationService private notificationService: INotificationService + @INotificationService private notificationService: INotificationService, + // {{SQL CARBON EDIT}} + @IObjectExplorerService private objectExplorerService: IObjectExplorerService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); @@ -30,6 +38,10 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie $registerTreeViewDataProvider(treeViewId: string): 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.getTreeViewer(treeViewId); if (viewer) { viewer.dataProvider = dataProvider; @@ -70,6 +82,25 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie return viewDescriptor ? viewDescriptor.treeViewer : 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.treeViewer.refresh(); + } + return false; + } + dispose(): void { this._dataProviders.forEach((dataProvider, treeViewId) => { const treeViewer = this.getTreeViewer(treeViewId); @@ -158,3 +189,100 @@ 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 TPromise.as({ 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, + iconType: e.icon, + // this is just needed since we don't have this + nodeSubType: e.iconDark, + isLeaf: e.collapsibleState === TreeItemCollapsibleState.None, + childProvider: e.childProvider, + payload: e.payload, + nodeType: e.contextValue + }; + }), + sessionId: this.sessionId + }); + }); + return TPromise.as(true); + } + + public refreshNode(nodeInfo: sqlops.ExpandNodeInfo): Thenable { + // no op + return TPromise.as(true); + } + + public closeSession(closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): Thenable { + // no op + return TPromise.as({ sessionId: undefined, success: true }); + } + + public findNodes(findNodesInfo: sqlops.FindNodesInfo): Thenable { + // no op + return TPromise.as({ 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; + } +} diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index f719205b8c..af6906918b 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -42,6 +42,9 @@ import { IProgressOptions, IProgressStep } from 'vs/workbench/services/progress/ import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as vscode from 'vscode'; +// {{SQL CARBON EDIT}} +import { ITreeItem as sqlITreeItem } from 'sql/workbench/common/views'; + export interface IEnvironment { isExtensionDevelopmentDebug: boolean; appRoot: string; @@ -668,7 +671,8 @@ export interface ExtHostDocumentsAndEditorsShape { } export interface ExtHostTreeViewsShape { - $getChildren(treeViewId: string, treeItemHandle?: string): TPromise; + // {{SQL CARBON EDIT}} + $getChildren(treeViewId: string, treeItemHandle?: string): TPromise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; diff --git a/src/vs/workbench/api/node/extHostTreeViews.ts b/src/vs/workbench/api/node/extHostTreeViews.ts index f997dd6ff1..1e19a8aa11 100644 --- a/src/vs/workbench/api/node/extHostTreeViews.ts +++ b/src/vs/workbench/api/node/extHostTreeViews.ts @@ -21,6 +21,8 @@ import { equals } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; // {{SQL CARBON EDIT}} +import * as sqlops from 'sqlops'; +import { ITreeItem as sqlITreeItem } from 'sql/workbench/common/views'; export type TreeItemHandle = string; export class ExtHostTreeViews implements ExtHostTreeViewsShape { @@ -392,7 +394,7 @@ export class ExtHostTreeView extends Disposable { } // {{SQL CARBON EDIT}} - protected createTreeItem(element: T, extensionTreeItem: vscode.TreeItem, parent?: TreeNode): ITreeItem { + protected createTreeItem(element: T, extensionTreeItem: sqlops.TreeItem, parent?: TreeNode): sqlITreeItem { const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); @@ -407,7 +409,10 @@ export class ExtHostTreeView extends Disposable { icon, iconDark: this.getDarkIconPath(extensionTreeItem) || icon, themeIcon: extensionTreeItem.iconPath instanceof ThemeIcon ? { id: extensionTreeItem.iconPath.id } : void 0, - collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState + collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState, + // {{SQL CARBON EDIT}} + payload: extensionTreeItem.payload, + childProvider: extensionTreeItem.childProvider }; return item; diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 41ff478731..f67e226384 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -184,6 +184,7 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService' import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IUriDisplayService, UriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay'; +import { OEShimService, IOEShimService } from 'sql/parts/objectExplorer/common/objectExplorerViewTreeShim'; interface WorkbenchParams { configuration: IWindowConfiguration; @@ -575,6 +576,7 @@ export class Workbench extends Disposable implements IPartService { serviceCollection.set(ITaskService, this.instantiationService.createInstance(TaskService)); serviceCollection.set(IMetadataService, this.instantiationService.createInstance(MetadataService)); serviceCollection.set(IObjectExplorerService, this.instantiationService.createInstance(ObjectExplorerService)); + serviceCollection.set(IOEShimService, this.instantiationService.createInstance(OEShimService)); serviceCollection.set(IScriptingService, this.instantiationService.createInstance(ScriptingService)); serviceCollection.set(IAdminService, this.instantiationService.createInstance(AdminService)); serviceCollection.set(IJobManagementService, this.instantiationService.createInstance(JobManagementService));