diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index f37c8bc6975..87f0d7c5e3d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -572,4 +572,70 @@ declare module 'vscode' { */ export function createWebview(title: string, column: ViewColumn, options: WebviewOptions): Webview; } + + export namespace window { + + /** + * Register a [TreeDataProvider](#TreeDataProvider) for the view contributed using the extension point `views`. + * @param viewId Id of the view contributed using the extension point `views`. + * @param treeDataProvider A [TreeDataProvider](#TreeDataProvider) that provides tree data for the view + * @return handle to the [treeview](#TreeView) that can be disposable. + */ + export function registerTreeDataProvider(viewId: string, treeDataProvider: TreeDataProvider): TreeView; + + } + + /** + * Represents a Tree view + */ + export interface TreeView extends Disposable { + + /** + * Reveal an element. By default revealed element is selected. + * + * In order to not to select, set the option `donotSelect` to `true`. + * + * **NOTE:** [TreeDataProvider](#TreeDataProvider) is required to implement [getParent](#TreeDataProvider.getParent) method to access this API. + */ + reveal(element: T, options?: { donotSelect?: boolean }): Thenable; + } + + /** + * A data provider that provides tree data + */ + export interface TreeDataProvider { + /** + * An optional event to signal that an element or root has changed. + * This will trigger the view to update the changed element/root and its children recursively (if shown). + * To signal that root has changed, do not pass any argument or pass `undefined` or `null`. + */ + onDidChangeTreeData?: Event; + + /** + * Get [TreeItem](#TreeItem) representation of the `element` + * + * @param element The element for which [TreeItem](#TreeItem) representation is asked for. + * @return [TreeItem](#TreeItem) representation of the element + */ + getTreeItem(element: T): TreeItem | Thenable; + + /** + * Get the children of `element` or root if no element is passed. + * + * @param element The element from which the provider gets children. Can be `undefined`. + * @return Children of `element` or root if no element is passed. + */ + getChildren(element?: T): ProviderResult; + + /** + * Optional method to return the parent of `element`. + * Return `null` or `undefined` if `element` is a child of root. + * + * **NOTE:** This method should be implemented in order to access [reveal](#TreeView.reveal) API. + * + * @param element The element for which the parent has to be returned. + * @return Parent of `element`. + */ + getParent?(element: T): ProviderResult; + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts index 0b19a1ecd1b..d67cd8c3c7e 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts @@ -34,6 +34,14 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this.viewsService.getTreeViewer(treeViewId).dataProvider = dataProvider; } + $reveal(treeViewId: string, item: ITreeItem, parentChain: ITreeItem[], options: { donotSelect?: boolean } = { donotSelect: false }): TPromise { + return this.viewsService.openView(treeViewId) + .then(() => { + const viewer = this.viewsService.getTreeViewer(treeViewId); + return viewer ? viewer.reveal(item, parentChain, options) : null; + }); + } + $refresh(treeViewId: string, itemsToRefresh: { [treeItemHandle: string]: ITreeItem }): void { const dataProvider = this._dataProviders.get(treeViewId); if (dataProvider) { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index a3c9b6685bc..7bfaa74be53 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -392,8 +392,8 @@ export function createApiFactory( } return extHostTerminalService.createTerminal(nameOrOptions, shellPath, shellArgs); }, - registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { - return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider); + registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.TreeView { + return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, (fn) => proposedApiFunction(extension, fn)); }, // proposed API sampleFunction: proposedApiFunction(extension, () => { diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index aa12dcb7b62..95e302f4c60 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -216,6 +216,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { export interface MainThreadTreeViewsShape extends IDisposable { $registerTreeViewDataProvider(treeViewId: string): void; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): void; + $reveal(treeViewId: string, treeItem: ITreeItem, parentChain: ITreeItem[], options?: { donotSelect?: boolean }): TPromise; } export interface MainThreadErrorsShape extends IDisposable { diff --git a/src/vs/workbench/api/node/extHostTreeViews.ts b/src/vs/workbench/api/node/extHostTreeViews.ts index 52592bc2b2a..6a8c297ddfe 100644 --- a/src/vs/workbench/api/node/extHostTreeViews.ts +++ b/src/vs/workbench/api/node/extHostTreeViews.ts @@ -38,9 +38,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { }); } - registerTreeDataProvider(id: string, dataProvider: vscode.TreeDataProvider): vscode.Disposable { + registerTreeDataProvider(id: string, dataProvider: vscode.TreeDataProvider, proposedApiFunction: (fn: U) => U): vscode.TreeView { const treeView = this.createExtHostTreeViewer(id, dataProvider); return { + reveal: proposedApiFunction((element: T, options?: { donotSelect?: boolean }): Thenable => { + return treeView.reveal(element, options); + }), dispose: () => { this.treeViews.delete(id); treeView.dispose(); @@ -107,6 +110,54 @@ class ExtHostTreeView extends Disposable { return this.elements.get(treeItemHandle); } + reveal(element: T, options?: { donotSelect?: boolean }): TPromise { + if (typeof this.dataProvider.getParent !== 'function') { + return TPromise.wrapError(new Error(`Required registered TreeDataProvider to implement 'getParent' method to access 'reveal' mehtod`)); + } + return this.resolveUnknownParentChain(element) + .then(parentChain => this.resolveTreeItem(element, parentChain[parentChain.length - 1]) + .then(treeNode => this.proxy.$reveal(this.viewId, treeNode.item, parentChain.map(p => p.item), options))); + } + + private resolveUnknownParentChain(element: T): TPromise { + return this.resolveParent(element) + .then((parent) => { + if (!parent) { + return TPromise.as([]); + } + return this.resolveUnknownParentChain(parent) + .then(result => this.resolveTreeItem(parent, result[result.length - 1]) + .then(parentNode => { + result.push(parentNode); + return result; + })); + }); + } + + private resolveParent(element: T): TPromise { + const node = this.nodes.get(element); + if (node) { + return TPromise.as(node.parent ? this.elements.get(node.parent.item.handle) : null); + } + return asWinJsPromise(() => this.dataProvider.getParent(element)); + } + + private resolveTreeItem(element: T, parent?: TreeNode): TPromise { + return asWinJsPromise(() => this.dataProvider.getTreeItem(element)) + .then(extTreeItem => this.createHandle(element, extTreeItem, parent)) + .then(handle => this.getChildren(parent ? parent.item.handle : null) + .then(() => { + const cachedElement = this.getExtensionElement(handle); + if (cachedElement) { + const node = this.nodes.get(cachedElement); + if (node) { + return TPromise.as(node); + } + } + throw new Error(`Cannot resolve tree item for element ${handle}`); + })); + } + private getChildrenNodes(parentNodeOrHandle?: TreeNode | TreeItemHandle): TreeNode[] { if (parentNodeOrHandle) { let parentNode: TreeNode; diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index d9a75bd4ba5..9b4bd7b6e8b 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -13,7 +13,7 @@ import * as DOM from 'vs/base/browser/dom'; import { $ } from 'vs/base/browser/builder'; import { LIGHT } from 'vs/platform/theme/common/themeService'; import { ITree, IDataSource, IRenderer, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree'; -import { TreeItemCollapsibleState, ITreeItem, ITreeViewer, ICustomViewsService, ITreeViewDataProvider, ViewsRegistry, IViewDescriptor, TreeViewItemHandleArg, ICustomViewDescriptor } from 'vs/workbench/common/views'; +import { TreeItemCollapsibleState, ITreeItem, ITreeViewer, ICustomViewsService, ITreeViewDataProvider, ViewsRegistry, IViewDescriptor, TreeViewItemHandleArg, ICustomViewDescriptor, IViewsViewlet } from 'vs/workbench/common/views'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IProgressService2, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -32,6 +32,7 @@ import { fillInActions, ContextAwareMenuItemActionItem } from 'vs/platform/actio import { FileKind } from 'vs/platform/files/common/files'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { FileIconThemableWorkbenchTree } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; export class CustomViewsService extends Disposable implements ICustomViewsService { @@ -40,7 +41,8 @@ export class CustomViewsService extends Disposable implements ICustomViewsServic private viewers: Map = new Map(); constructor( - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IViewletService private viewletService: IViewletService ) { super(); this.createViewers(ViewsRegistry.getAllViews()); @@ -52,6 +54,19 @@ export class CustomViewsService extends Disposable implements ICustomViewsServic return this.viewers.get(id); } + openView(id: string, focus: boolean): TPromise { + const viewDescriptor = ViewsRegistry.getView(id); + if (viewDescriptor) { + return this.viewletService.openViewlet(viewDescriptor.id) + .then((viewlet: IViewsViewlet) => { + if (viewlet && viewlet.openView) { + viewlet.openView(id, focus); + } + }); + } + return TPromise.as(null); + } + private createViewers(viewDescriptors: IViewDescriptor[]): void { for (const viewDescriptor of viewDescriptors) { if ((viewDescriptor).treeView) { @@ -122,7 +137,7 @@ class CustomTreeViewer extends Disposable implements ITreeViewer { this._dataProvider = new class implements ITreeViewDataProvider { onDidChange = dataProvider.onDidChange; onDispose = dataProvider.onDispose; - getChildren(node?: ITreeItem): TPromise { + getChildren(node?: ITreeItem): TPromise { if (node.children) { return TPromise.as(node.children); } @@ -247,6 +262,24 @@ class CustomTreeViewer extends Disposable implements ITreeViewer { return TPromise.as(null); } + reveal(item: ITreeItem, parentChain: ITreeItem[], options?: { donotSelect?: boolean }): TPromise { + if (this.tree && this.isVisible) { + options = options ? options : { donotSelect: false }; + const select = !options.donotSelect; + 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]); + } + }); + } + return TPromise.as(null); + } + private activate() { if (!this.activated) { this.extensionService.activateByEvent(`onView:${this.id}`); diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 20db0ad67aa..88874ea907b 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -260,14 +260,19 @@ export class ViewsViewlet extends PanelViewlet implements IViewsViewlet { .then(() => void 0); } - openView(id: string): void { - this.focus(); + openView(id: string, focus?: boolean): TPromise { + if (focus) { + this.focus(); + } const view = this.getView(id); if (view) { view.setExpanded(true); - view.focus(); + if (focus) { + view.focus(); + } + return TPromise.as(null); } else { - this.toggleViewVisibility(id); + return this.toggleViewVisibility(id, focus); } } @@ -294,19 +299,19 @@ export class ViewsViewlet extends PanelViewlet implements IViewsViewlet { super.shutdown(); } - toggleViewVisibility(id: string): void { + toggleViewVisibility(id: string, focus?: boolean): TPromise { let viewState = this.viewsStates.get(id); if (!viewState) { - return; + return TPromise.as(null); } viewState.isHidden = !!this.getView(id); - this.updateViews() + return this.updateViews() .then(() => { this._onDidChangeViewVisibilityState.fire(id); if (!viewState.isHidden) { - this.openView(id); - } else { + this.openView(id, focus); + } else if (focus) { this.focus(); } }); diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 453e722abd9..f9269f651ce 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -16,9 +16,9 @@ import { IDisposable } from 'vs/base/common/lifecycle'; export class ViewLocation { - static readonly Explorer = new ViewLocation('explorer'); - static readonly Debug = new ViewLocation('debug'); - static readonly Extensions = new ViewLocation('extensions'); + static readonly Explorer = new ViewLocation('workbench.view.explorer'); + static readonly Debug = new ViewLocation('workbench.view.debug'); + static readonly Extensions = new ViewLocation('workbench.view.extensions'); constructor(private _id: string) { } @@ -29,8 +29,8 @@ export class ViewLocation { static getContributedViewLocation(value: string): ViewLocation { switch (value) { - case ViewLocation.Explorer.id: return ViewLocation.Explorer; - case ViewLocation.Debug.id: return ViewLocation.Debug; + case 'explorer': return ViewLocation.Explorer; + case 'debug': return ViewLocation.Debug; } return void 0; } @@ -150,7 +150,7 @@ export const ViewsRegistry: IViewsRegistry = new class implements IViewsRegistry export interface IViewsViewlet extends IViewlet { - openView(id: string): void; + openView(id: string, focus?: boolean): TPromise; } @@ -171,6 +171,8 @@ export interface ITreeViewer extends IDisposable { show(container: HTMLElement); getOptimalWidth(): number; + + reveal(item: ITreeItem, parentChain: ITreeItem[], options: { donotSelect?: boolean }): TPromise; } export interface ICustomViewDescriptor extends IViewDescriptor { @@ -185,6 +187,8 @@ export interface ICustomViewsService { _serviceBrand: any; getTreeViewer(id: string): ITreeViewer; + + openView(id: string, focus?: boolean): TPromise; } export type TreeViewItemHandleArg = { diff --git a/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts b/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts index 7b41004a587..b0a5d7c8035 100644 --- a/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/viewPickerHandler.ts @@ -129,7 +129,7 @@ export class ViewPickerHandler extends QuickOpenHandler { if (views.length) { for (const view of views) { if (this.contextKeyService.contextMatchesRules(view.when)) { - result.push(new ViewEntry(view.name, viewlet.name, () => this.viewletService.openViewlet(viewlet.id, true).done(viewlet => (viewlet).openView(view.id), errors.onUnexpectedError))); + result.push(new ViewEntry(view.name, viewlet.name, () => this.viewletService.openViewlet(viewlet.id, true).done(viewlet => (viewlet).openView(view.id, true), errors.onUnexpectedError))); } } } diff --git a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts index 1362dfc2782..2342c3dab8c 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -6,6 +6,7 @@ 'use strict'; import * as assert from 'assert'; +import * as sinon from 'sinon'; import { Emitter } from 'vs/base/common/event'; import { ExtHostTreeViews } from 'vs/workbench/api/node/extHostTreeViews'; import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands'; @@ -33,6 +34,11 @@ suite('ExtHostTreeView', function () { $refresh(viewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): void { this.onRefresh.fire(itemsToRefresh); } + + $reveal(): TPromise { + return null; + } + } let testObject: ExtHostTreeViews; @@ -69,8 +75,8 @@ suite('ExtHostTreeView', function () { testObject = new ExtHostTreeViews(target, new ExtHostCommands(rpcProtocol, new ExtHostHeapService(), new NullLogService())); onDidChangeTreeNode = new Emitter<{ key: string }>(); onDidChangeTreeNodeWithId = new Emitter<{ key: string }>(); - testObject.registerTreeDataProvider('testNodeTreeProvider', aNodeTreeDataProvider()); - testObject.registerTreeDataProvider('testNodeWithIdTreeProvider', aNodeWithIdTreeDataProvider()); + testObject.registerTreeDataProvider('testNodeTreeProvider', aNodeTreeDataProvider(), (fn) => fn); + testObject.registerTreeDataProvider('testNodeWithIdTreeProvider', aNodeWithIdTreeDataProvider(), (fn) => fn); testObject.$getChildren('testNodeTreeProvider').then(elements => { for (const element of elements) { @@ -398,6 +404,61 @@ suite('ExtHostTreeView', function () { }); }); + test('reveal will throw an error if getParent is not implemented', () => { + const treeView = testObject.registerTreeDataProvider('treeDataProvider', aNodeTreeDataProvider(), (fn) => fn); + return treeView.reveal({ key: 'a' }) + .then(() => assert.fail('Reveal should throw an error as getParent is not implemented'), () => null); + }); + + test('reveal will return empty array for root element', () => { + const revealTarget = sinon.spy(target, '$reveal'); + const treeView = testObject.registerTreeDataProvider('treeDataProvider', aCompleteNodeTreeDataProvider(), (fn) => fn); + return treeView.reveal({ key: 'a' }) + .then(() => { + assert.ok(revealTarget.calledOnce); + assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); + assert.deepEqual({ handle: '0/0:a', label: 'a', collapsibleState: TreeItemCollapsibleState.Collapsed }, removeUnsetKeys(revealTarget.args[0][1])); + assert.deepEqual([], revealTarget.args[0][2]); + assert.equal(void 0, revealTarget.args[0][3]); + }); + }); + + test('reveal will return parents array for an element', () => { + const revealTarget = sinon.spy(target, '$reveal'); + const treeView = testObject.registerTreeDataProvider('treeDataProvider', aCompleteNodeTreeDataProvider(), (fn) => fn); + return treeView.reveal({ key: 'aa' }) + .then(() => { + assert.ok(revealTarget.calledOnce); + assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); + assert.deepEqual({ handle: '0/0:a/0:aa', label: 'aa', collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:a' }, removeUnsetKeys(revealTarget.args[0][1])); + assert.deepEqual([{ handle: '0/0:a', label: 'a', collapsibleState: TreeItemCollapsibleState.Collapsed }], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); + assert.equal(void 0, revealTarget.args[0][3]); + }); + }); + + test('reveal will return parents array for deeper element with no selection', () => { + tree = { + 'b': { + 'ba': { + 'bac': {} + } + } + }; + const revealTarget = sinon.spy(target, '$reveal'); + const treeView = testObject.registerTreeDataProvider('treeDataProvider', aCompleteNodeTreeDataProvider(), (fn) => fn); + return treeView.reveal({ key: 'bac' }, { donotSelect: true }) + .then(() => { + assert.ok(revealTarget.calledOnce); + assert.deepEqual('treeDataProvider', revealTarget.args[0][0]); + assert.deepEqual({ handle: '0/0:b/0:ba/0:bac', label: 'bac', collapsibleState: TreeItemCollapsibleState.None, parentHandle: '0/0:b/0:ba' }, removeUnsetKeys(revealTarget.args[0][1])); + assert.deepEqual([ + { handle: '0/0:b', label: 'b', collapsibleState: TreeItemCollapsibleState.Collapsed }, + { handle: '0/0:b/0:ba', label: 'ba', collapsibleState: TreeItemCollapsibleState.Collapsed, parentHandle: '0/0:b' } + ], (>revealTarget.args[0][2]).map(arg => removeUnsetKeys(arg))); + assert.deepEqual({ donotSelect: true }, revealTarget.args[0][3]); + }); + }); + function removeUnsetKeys(obj: any): any { const result = {}; for (const key of Object.keys(obj)) { @@ -420,6 +481,22 @@ suite('ExtHostTreeView', function () { }; } + function aCompleteNodeTreeDataProvider(): TreeDataProvider<{ key: string }> { + return { + getChildren: (element: { key: string }): { key: string }[] => { + return getChildren(element ? element.key : undefined).map(key => getNode(key)); + }, + getTreeItem: (element: { key: string }): TreeItem => { + return getTreeItem(element.key); + }, + getParent: ({ key }: { key: string }): { key: string } => { + const parentKey = key.substring(0, key.length - 1); + return parentKey ? new Key(parentKey) : void 0; + }, + onDidChangeTreeData: onDidChangeTreeNode.event + }; + } + function aNodeWithIdTreeDataProvider(): TreeDataProvider<{ key: string }> { return { getChildren: (element: { key: string }): { key: string }[] => {