提交 d9e9be8b 编写于 作者: S Sandeep Somavarapu

Implement #30288

上级 a4cfd33d
......@@ -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<T>(viewId: string, treeDataProvider: TreeDataProvider<T>): TreeView<T>;
}
/**
* Represents a Tree view
*/
export interface TreeView<T> 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<void>;
}
/**
* A data provider that provides tree data
*/
export interface TreeDataProvider<T> {
/**
* 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<T | undefined | null>;
/**
* 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<TreeItem>;
/**
* 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<T[]>;
/**
* 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<T>;
}
}
......@@ -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<void> {
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) {
......
......@@ -392,8 +392,8 @@ export function createApiFactory(
}
return extHostTerminalService.createTerminal(<string>nameOrOptions, shellPath, shellArgs);
},
registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider<any>): vscode.Disposable {
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider);
registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider<any>): vscode.TreeView<any> {
return extHostTreeViews.registerTreeDataProvider(viewId, treeDataProvider, (fn) => proposedApiFunction(extension, fn));
},
// proposed API
sampleFunction: proposedApiFunction(extension, () => {
......
......@@ -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<void>;
}
export interface MainThreadErrorsShape extends IDisposable {
......
......@@ -38,9 +38,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
});
}
registerTreeDataProvider<T>(id: string, dataProvider: vscode.TreeDataProvider<T>): vscode.Disposable {
registerTreeDataProvider<T>(id: string, dataProvider: vscode.TreeDataProvider<T>, proposedApiFunction: <U>(fn: U) => U): vscode.TreeView<T> {
const treeView = this.createExtHostTreeViewer(id, dataProvider);
return {
reveal: proposedApiFunction((element: T, options?: { donotSelect?: boolean }): Thenable<void> => {
return treeView.reveal(element, options);
}),
dispose: () => {
this.treeViews.delete(id);
treeView.dispose();
......@@ -107,6 +110,54 @@ class ExtHostTreeView<T> extends Disposable {
return this.elements.get(treeItemHandle);
}
reveal(element: T, options?: { donotSelect?: boolean }): TPromise<void> {
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<TreeNode[]> {
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<T> {
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<TreeNode> {
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;
......
......@@ -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<string, CustomTreeViewer> = new Map<string, CustomTreeViewer>();
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<void> {
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 ((<ICustomViewDescriptor>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<any[]> {
getChildren(node?: ITreeItem): TPromise<ITreeItem[]> {
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<void> {
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}`);
......
......@@ -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<void> {
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<void> {
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();
}
});
......
......@@ -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<void>;
}
......@@ -171,6 +171,8 @@ export interface ITreeViewer extends IDisposable {
show(container: HTMLElement);
getOptimalWidth(): number;
reveal(item: ITreeItem, parentChain: ITreeItem[], options: { donotSelect?: boolean }): TPromise<void>;
}
export interface ICustomViewDescriptor extends IViewDescriptor {
......@@ -185,6 +187,8 @@ export interface ICustomViewsService {
_serviceBrand: any;
getTreeViewer(id: string): ITreeViewer;
openView(id: string, focus?: boolean): TPromise<void>;
}
export type TreeViewItemHandleArg = {
......
......@@ -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 => (<IViewsViewlet>viewlet).openView(view.id), errors.onUnexpectedError)));
result.push(new ViewEntry(view.name, viewlet.name, () => this.viewletService.openViewlet(viewlet.id, true).done(viewlet => (<IViewsViewlet>viewlet).openView(view.id, true), errors.onUnexpectedError)));
}
}
}
......
......@@ -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<void> {
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 }], (<Array<any>>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' }
], (<Array<any>>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 }[] => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册