diff --git a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts index d055e61652b..73aec9dca84 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts @@ -12,6 +12,7 @@ import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { ViewsRegistry } from 'vs/workbench/browser/parts/views/viewsRegistry'; import { ITreeViewDataProvider, ITreeItem } from 'vs/workbench/common/views'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { assign } from 'vs/base/common/objects'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -94,6 +95,13 @@ class TreeViewDataProvider implements ITreeViewDataProvider { if (treeItems.length) { this._onDidChange.fire(treeItems); } + /*this._proxy.$resolveHandles(this.treeViewId, treeItemHandles) + .then(treeItems => { + treeItems = coalesce(treeItems.map(treeItem => this.updateTreeItem(treeItem))); + if (treeItems.length) { + this._onDidChange.fire(treeItems); + } + }); */ } else { this._onDidChange.fire(); } @@ -110,7 +118,9 @@ class TreeViewDataProvider implements ITreeViewDataProvider { private setElements(parent: TreeItemHandle, children: ITreeItem[]) { if (children && children.length) { for (const child of children) { - this.itemsMap.set(child.handle, child); + if (!this.updateTreeItem(child)) { + this.itemsMap.set(child.handle, child); + } if (child.children && child.children.length) { this.setElements(child.handle, child.children); } @@ -120,4 +130,14 @@ class TreeViewDataProvider implements ITreeViewDataProvider { } } } + + private updateTreeItem(treeItem: ITreeItem): ITreeItem { + const current = this.itemsMap.get(treeItem.handle); + treeItem.children = treeItem.children ? treeItem.children : null; + if (current) { + assign(current, treeItem); + return current; + } + return null; + } } \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 1159a392db9..23d68755c05 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -495,6 +495,7 @@ export interface ExtHostDocumentsAndEditorsShape { export interface ExtHostTreeViewsShape { $getElements(treeViewId: string): TPromise; $getChildren(treeViewId: string, treeItemHandle: string): TPromise; + // $resolveHandles(treeViewId: string, treeItemHandles: string[]): TPromise; } export interface ExtHostWorkspaceShape { diff --git a/src/vs/workbench/api/node/extHostTreeViews.ts b/src/vs/workbench/api/node/extHostTreeViews.ts index 9a169b7a524..fd1ffd55c84 100644 --- a/src/vs/workbench/api/node/extHostTreeViews.ts +++ b/src/vs/workbench/api/node/extHostTreeViews.ts @@ -7,13 +7,11 @@ import { localize } from 'vs/nls'; import * as vscode from 'vscode'; import URI from 'vs/base/common/uri'; -import { distinct } from 'vs/base/common/arrays'; import { debounceEvent } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; import { ITreeItem, TreeViewItemHandleArg } from 'vs/workbench/common/views'; -import { TreeItemCollapsibleState } from './extHostTypes'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; import { asWinJsPromise } from 'vs/base/common/async'; @@ -64,17 +62,30 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.getChildren(treeItemHandle); } + /* $resolveHandles(treeViewId: string, treeItemHandles: string[]): TPromise { + const treeView = this.treeViews.get(treeViewId); + if (!treeView) { + return TPromise.wrapError(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId))); + } + return treeView.resolveHandles(treeItemHandles); + } */ + private convertArgument(arg: TreeViewItemHandleArg): any { const treeView = this.treeViews.get(arg.$treeViewId); return treeView ? treeView.getExtensionElement(arg.$treeItemHandle) : null; } } +interface TreeNode { + handle: TreeItemHandle; + parent: TreeItemHandle; + children: TreeItemHandle[]; +} + class ExtHostTreeView extends Disposable { private extElementsMap: Map = new Map(); - private itemHandlesMap: Map = new Map(); - private extChildrenElementsMap: Map = new Map(); + private nodes: Map = new Map(); constructor(private viewId: string, private dataProvider: vscode.TreeDataProvider, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter) { super(); @@ -85,12 +96,9 @@ class ExtHostTreeView extends Disposable { } getTreeItems(): TPromise { - this.extChildrenElementsMap.clear(); - this.extElementsMap.clear(); - this.itemHandlesMap.clear(); - + this.clearAll(); return asWinJsPromise(() => this.dataProvider.getChildren()) - .then(elements => this.processAndMapElements(elements, '0')); + .then(elements => this.resolveElements(elements, '0')); } getChildren(treeItemHandle: TreeItemHandle): TPromise { @@ -102,9 +110,24 @@ class ExtHostTreeView extends Disposable { } return asWinJsPromise(() => this.dataProvider.getChildren(extElement)) - .then(childrenElements => this.processAndMapElements(childrenElements, treeItemHandle)); + .then(childrenElements => this.resolveElements(childrenElements, treeItemHandle)) + .then(childrenItems => { + this.nodes.get(extElement).children = childrenItems.map(c => c.handle); + return childrenItems; + }); } + /* resolveHandles(treeItemHandles: TreeItemHandle[]): TPromise { + return TPromise.join(treeItemHandles.map(treeItemHandle => { + let extElement = this.getExtensionElement(treeItemHandle); + if (!extElement) { + return TPromise.wrapError(new Error(localize('treeItem.notFound', 'No tree item with id \'{0}\' found.', treeItemHandle))); + } + const node = this.nodes.get(extElement); + return this.resolveElement(extElement, node.handle); + })).then(treeItems => coalesce(treeItems)); + } */ + getExtensionElement(treeItemHandle: TreeItemHandle): T { return this.extElementsMap.get(treeItemHandle); } @@ -114,23 +137,30 @@ class ExtHostTreeView extends Disposable { if (hasRoot) { this.proxy.$refresh(this.viewId, []); } else { - const itemHandles = distinct(elements.map(element => this.itemHandlesMap.get(element)) - .filter(itemHandle => !!itemHandle)); - if (itemHandles.length) { - this.proxy.$refresh(this.viewId, itemHandles); + const handlesToUpdate = this.getElementsToUpdate(elements); + if (handlesToUpdate.length) { + this.proxy.$refresh(this.viewId, handlesToUpdate); } } } - private processAndMapElements(elements: T[], parentHandle: TreeItemHandle): TPromise { + private resolveElements(elements: T[], parentHandle: TreeItemHandle): TPromise { if (elements && elements.length) { return TPromise.join( elements.filter(element => !!element) .map((element, index) => { - if (this.extChildrenElementsMap.has(element)) { - return TPromise.wrapError(new Error(localize('treeView.duplicateElement', 'Element {0} is already registered', element))); - } - return this.resolveElement(element, index, parentHandle); + return this.resolveElement(element, index, parentHandle) + .then(treeItem => { + if (treeItem) { + this.nodes.set(element, { + handle: treeItem.handle, + parent: parentHandle, + children: void 0 + }); + this.extElementsMap.set(treeItem.handle, element); + } + return treeItem; + }); })) .then(treeItems => treeItems.filter(treeItem => !!treeItem)); } @@ -139,22 +169,7 @@ class ExtHostTreeView extends Disposable { private resolveElement(element: T, index: number, parentHandle: TreeItemHandle): TPromise { return asWinJsPromise(() => this.dataProvider.getTreeItem(element)) - .then(extTreeItem => { - const treeItem = this.massageTreeItem(extTreeItem, index, parentHandle); - if (treeItem) { - this.itemHandlesMap.set(element, treeItem.handle); - this.extElementsMap.set(treeItem.handle, element); - if (treeItem.collapsibleState === TreeItemCollapsibleState.Expanded) { - return this.getChildren(treeItem.handle).then(children => { - treeItem.children = children; - return treeItem; - }); - } else { - return treeItem; - } - } - return null; - }); + .then(extTreeItem => this.massageTreeItem(extTreeItem, index, parentHandle)); } private massageTreeItem(extensionTreeItem: vscode.TreeItem, index: number, parentHandle: TreeItemHandle): ITreeItem { @@ -169,7 +184,7 @@ class ExtHostTreeView extends Disposable { contextValue: extensionTreeItem.contextValue, icon, iconDark: this.getDarkIconPath(extensionTreeItem) || icon, - collapsibleState: extensionTreeItem.collapsibleState, + collapsibleState: extensionTreeItem.collapsibleState }; } @@ -197,29 +212,69 @@ class ExtHostTreeView extends Disposable { return URI.file(iconPath).toString(); } - private clearChildren(extElement: T): void { - const children = this.extChildrenElementsMap.get(extElement); - if (children) { - for (const child of children) { - this.clearElement(child); + private getElementsToUpdate(elements: T[]): TreeItemHandle[] { + const elementsToUpdate = new Set(); + for (const element of elements) { + let elementNode = this.nodes.get(element); + if (elementNode && !elementsToUpdate.has(elementNode.handle)) { + // check if an ancestor of extElement is already in the elements to update list + let currentNode = elementNode; + while (currentNode && currentNode.parent && !elementsToUpdate.has(currentNode.parent)) { + const parentElement = this.extElementsMap.get(currentNode.parent); + currentNode = this.nodes.get(parentElement); + } + if (!currentNode) { + elementsToUpdate.add(elementNode.handle); + } } - this.extChildrenElementsMap.delete(extElement); } + + const handlesToUpdate: TreeItemHandle[] = []; + // Take only top level elements + elementsToUpdate.forEach((handle) => { + const element = this.extElementsMap.get(handle); + let node = this.nodes.get(element); + if (node && !elementsToUpdate.has(node.parent)) { + handlesToUpdate.push(handle); + } + }); + + return handlesToUpdate; } - private clearElement(extElement: T): void { - this.clearChildren(extElement); - - const treeItemhandle = this.itemHandlesMap.get(extElement); - this.itemHandlesMap.delete(extElement); - if (treeItemhandle) { - this.extElementsMap.delete(treeItemhandle); + private clearChildren(element: T): void { + let node = this.nodes.get(element); + if (node.children) { + for (const childHandle of node.children) { + const childEleement = this.extElementsMap.get(childHandle); + if (childEleement) { + this.clear(childEleement); + } + } } + node.children = void 0; + } + + private clear(element: T): void { + let node = this.nodes.get(element); + if (node.children) { + for (const childHandle of node.children) { + const childEleement = this.extElementsMap.get(childHandle); + if (childEleement) { + this.clear(childEleement); + } + } + } + this.nodes.delete(element); + this.extElementsMap.delete(node.handle); + } + + private clearAll(): void { + this.extElementsMap.clear(); + this.nodes.clear(); } dispose() { - this.extElementsMap.clear(); - this.itemHandlesMap.clear(); - this.extChildrenElementsMap.clear(); + this.clearAll(); } } \ No newline at end of file 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 5e06e83c94d..815a5832edb 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -100,31 +100,6 @@ suite('ExtHostTreeView', function () { }); }); - test('children are fetched immediately if state is expanded', () => { - tree['c'] = { - 'ca': { - 'caa': { - 'collapsibleState': TreeItemCollapsibleState.None - }, - 'collapsibleState': TreeItemCollapsibleState.Expanded, - }, - 'cb': { - 'cba': {}, - 'collapsibleState': TreeItemCollapsibleState.Collapsed, - }, - 'collapsibleState': TreeItemCollapsibleState.Expanded, - }; - return testObject.$getElements('testDataProvider') - .then(elements => { - const actuals = elements.map(e => e.handle); - assert.deepEqual(actuals, ['0/0:a', '0/1:b', '0/2:c']); - assert.deepEqual(elements[2].children.map(e => e.handle), ['0/2:c/0:ca', '0/2:c/1:cb']); - assert.deepEqual(elements[2].children[0].children.map(e => e.handle), ['0/2:c/0:ca/0:caa']); - assert.deepEqual(elements[2].children[0].children[0].children, undefined); - assert.deepEqual(elements[2].children[1].children, undefined); - }); - }); - test('refresh root', function (done) { target.onRefresh.event(actuals => { assert.equal(0, actuals.length); @@ -149,6 +124,26 @@ suite('ExtHostTreeView', function () { onDidChangeTreeData.fire('bb'); }); + test('refresh parent and child node trigger refresh only on parent - scenario 1', function (done) { + target.onRefresh.event(actuals => { + assert.deepEqual(['0/1:b', '0/0:a/0:aa'], actuals); + done(); + }); + onDidChangeTreeData.fire('b'); + onDidChangeTreeData.fire('aa'); + onDidChangeTreeData.fire('bb'); + }); + + test('refresh parent and child node trigger refresh only on parent - scenario 2', function (done) { + target.onRefresh.event(actuals => { + assert.deepEqual(['0/0:a/0:aa', '0/1:b'], actuals); + done(); + }); + onDidChangeTreeData.fire('bb'); + onDidChangeTreeData.fire('aa'); + onDidChangeTreeData.fire('b'); + }); + test('refresh calls are throttled on roots', function (done) { target.onRefresh.event(actuals => { assert.equal(0, actuals.length);