Skip to content

Commit

Permalink
#30288 Refactor: Hold treeItems in the node
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed Feb 23, 2018
1 parent fae1d64 commit a4cfd33
Show file tree
Hide file tree
Showing 2 changed files with 210 additions and 107 deletions.
224 changes: 141 additions & 83 deletions src/vs/workbench/api/node/extHostTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.proto
import { ITreeItem, TreeViewItemHandleArg } from 'vs/workbench/common/views';
import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands';
import { asWinJsPromise } from 'vs/base/common/async';
import { coalesce } from 'vs/base/common/arrays';
import { TreeItemCollapsibleState } from 'vs/workbench/api/node/extHostTypes';
import { isUndefinedOrNull } from 'vs/base/common/types';

Expand All @@ -39,9 +38,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
});
}

registerTreeDataProvider<T>(id: string, treeDataProvider: vscode.TreeDataProvider<T>): vscode.Disposable {
const treeView = new ExtHostTreeView<T>(id, treeDataProvider, this._proxy, this.commands.converter);
this.treeViews.set(id, treeView);
registerTreeDataProvider<T>(id: string, dataProvider: vscode.TreeDataProvider<T>): vscode.Disposable {
const treeView = this.createExtHostTreeViewer(id, dataProvider);
return {
dispose: () => {
this.treeViews.delete(id);
Expand All @@ -58,32 +56,38 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return treeView.getChildren(treeItemHandle);
}

private createExtHostTreeViewer<T>(id: string, dataProvider: vscode.TreeDataProvider<T>): ExtHostTreeView<T> {
const treeView = new ExtHostTreeView<T>(id, dataProvider, this._proxy, this.commands.converter);
this.treeViews.set(id, treeView);
return treeView;
}

private convertArgument(arg: TreeViewItemHandleArg): any {
const treeView = this.treeViews.get(arg.$treeViewId);
return treeView ? treeView.getExtensionElement(arg.$treeItemHandle) : null;
}
}

interface TreeNode {
handle: TreeItemHandle;
parentHandle: TreeItemHandle;
childrenHandles: TreeItemHandle[];
item: ITreeItem;
parent: TreeNode;
children: TreeNode[];
}

class ExtHostTreeView<T> extends Disposable {

private static LABEL_HANDLE_PREFIX = '0';
private static ID_HANDLE_PREFIX = '1';

private rootHandles: TreeItemHandle[] = [];
private roots: TreeNode[] = null;
private elements: Map<TreeItemHandle, T> = new Map<TreeItemHandle, T>();
private nodes: Map<T, TreeNode> = new Map<T, TreeNode>();

constructor(private viewId: string, private dataProvider: vscode.TreeDataProvider<T>, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter) {
super();
this.proxy.$registerTreeViewDataProvider(viewId);
if (dataProvider.onDidChangeTreeData) {
this._register(debounceEvent<T, T[]>(dataProvider.onDidChangeTreeData, (last, current) => last ? [...last, current] : [current], 200)(elements => this.refresh(elements)));
if (this.dataProvider.onDidChangeTreeData) {
this._register(debounceEvent<T, T[]>(this.dataProvider.onDidChangeTreeData, (last, current) => last ? [...last, current] : [current], 200)(elements => this.refresh(elements)));
}
}

Expand All @@ -94,30 +98,47 @@ class ExtHostTreeView<T> extends Disposable {
return TPromise.as([]);
}

this.clearChildren(parentElement);
return asWinJsPromise(() => this.dataProvider.getChildren(parentElement))
.then(elements => TPromise.join(
coalesce(elements || []).map(element =>
asWinJsPromise(() => this.dataProvider.getTreeItem(element))
.then(extTreeItem => {
if (extTreeItem) {
if (extTreeItem.id && this.elements.has(this.createHandle(element, extTreeItem))) {
throw new Error(localize('treeView.duplicateElement', 'Element with id {0} is already registered', extTreeItem.id));
}
return { element, extTreeItem };
}
return null;
})
))).then(extTreeItems => coalesce(extTreeItems).map((({ element, extTreeItem }) => this.createTreeItem(element, extTreeItem, parentHandle))));
const childrenNodes = this.getChildrenNodes(parentHandle); // Get it from cache
return (childrenNodes ? TPromise.as(childrenNodes) : this.fetchChildrenNodes(parentElement))
.then(nodes => nodes.map(n => n.item));
}

getExtensionElement(treeItemHandle: TreeItemHandle): T {
return this.elements.get(treeItemHandle);
}

private getChildrenNodes(parentNodeOrHandle?: TreeNode | TreeItemHandle): TreeNode[] {
if (parentNodeOrHandle) {
let parentNode: TreeNode;
if (typeof parentNodeOrHandle === 'string') {
const parentElement = this.getExtensionElement(parentNodeOrHandle);
parentNode = parentElement ? this.nodes.get(parentElement) : null;
} else {
parentNode = parentNodeOrHandle;
}
return parentNode ? parentNode.children : null;
}
return this.roots;
}

private fetchChildrenNodes(parentElement?: T): TPromise<TreeNode[]> {
// clear children cache
this.clearChildren(parentElement);

const parentNode = parentElement ? this.nodes.get(parentElement) : void 0;
return asWinJsPromise(() => this.dataProvider.getChildren(parentElement))
.then(elements => TPromise.join(
(elements || [])
.filter(element => !!element)
.map(element => asWinJsPromise(() => this.dataProvider.getTreeItem(element))
.then(extTreeItem => extTreeItem ? this.createAndRegisterTreeNode(element, extTreeItem, parentNode) : null))))
.then(nodes => nodes.filter(n => !!n));
}

private refresh(elements: T[]): void {
const hasRoot = elements.some(element => !element);
if (hasRoot) {
this.clearAll(); // clear cache
this.proxy.$refresh(this.viewId);
} else {
const handlesToRefresh = this.getHandlesToRefresh(elements);
Expand All @@ -131,15 +152,15 @@ class ExtHostTreeView<T> extends Disposable {
const elementsToUpdate = new Set<TreeItemHandle>();
for (const element of elements) {
let elementNode = this.nodes.get(element);
if (elementNode && !elementsToUpdate.has(elementNode.handle)) {
if (elementNode && !elementsToUpdate.has(elementNode.item.handle)) {
// check if an ancestor of extElement is already in the elements to update list
let currentNode = elementNode;
while (currentNode && currentNode.parentHandle && !elementsToUpdate.has(currentNode.parentHandle)) {
const parentElement = this.elements.get(currentNode.parentHandle);
while (currentNode && currentNode.parent && !elementsToUpdate.has(currentNode.parent.item.handle)) {
const parentElement = this.elements.get(currentNode.parent.item.handle);
currentNode = this.nodes.get(parentElement);
}
if (!currentNode.parentHandle) {
elementsToUpdate.add(elementNode.handle);
if (!currentNode.parent) {
elementsToUpdate.add(elementNode.item.handle);
}
}
}
Expand All @@ -149,7 +170,7 @@ class ExtHostTreeView<T> extends Disposable {
elementsToUpdate.forEach((handle) => {
const element = this.elements.get(handle);
let node = this.nodes.get(element);
if (node && !elementsToUpdate.has(node.parentHandle)) {
if (node && (!node.parent || !elementsToUpdate.has(node.parent.item.handle))) {
handlesToUpdate.push(handle);
}
});
Expand All @@ -158,31 +179,57 @@ class ExtHostTreeView<T> extends Disposable {
}

private refreshHandles(itemHandles: TreeItemHandle[]): TPromise<void> {
const itemsToRefresh: { [handle: string]: ITreeItem } = {};
const promises: TPromise<void>[] = [];
itemHandles.forEach(treeItemHandle => {
const extElement = this.getExtensionElement(treeItemHandle);
const node = this.nodes.get(extElement);
promises.push(asWinJsPromise(() => this.dataProvider.getTreeItem(extElement))
.then(extTreeItem => {
if (extTreeItem) {
itemsToRefresh[treeItemHandle] = this.createTreeItem(extElement, extTreeItem, node.parentHandle);
const itemsToRefresh: { [treeItemHandle: string]: ITreeItem } = {};
return TPromise.join(itemHandles.map(treeItemHandle =>
this.refreshNode(treeItemHandle)
.then(node => {
if (node) {
itemsToRefresh[treeItemHandle] = node.item;
}
}));
});
return TPromise.join(promises)
.then(treeItems => this.proxy.$refresh(this.viewId, itemsToRefresh));
})))
.then(() => Object.keys(itemsToRefresh).length ? this.proxy.$refresh(this.viewId, itemsToRefresh) : null);
}

private createTreeItem(element: T, extensionTreeItem: vscode.TreeItem, parentHandle: TreeItemHandle): ITreeItem {
private refreshNode(treeItemHandle: TreeItemHandle): TPromise<TreeNode> {
const extElement = this.getExtensionElement(treeItemHandle);
const existing = this.nodes.get(extElement);
this.clearChildren(extElement); // clear children cache
return asWinJsPromise(() => this.dataProvider.getTreeItem(extElement))
.then(extTreeItem => {
if (extTreeItem) {
const newNode = this.createTreeNode(extElement, extTreeItem, existing.parent);
this.updateNodeCache(extElement, newNode, existing, existing.parent);
return newNode;
}
return null;
});
}

const handle = this.createHandle(element, extensionTreeItem, parentHandle);
const icon = this.getLightIconPath(extensionTreeItem);
this.update(element, handle, parentHandle);
private createAndRegisterTreeNode(element: T, extTreeItem: vscode.TreeItem, parentNode: TreeNode): TreeNode {
const node = this.createTreeNode(element, extTreeItem, parentNode);
if (extTreeItem.id && this.elements.has(node.item.handle)) {
throw new Error(localize('treeView.duplicateElement', 'Element with id {0} is already registered', extTreeItem.id));
}
this.addNodeToCache(element, node);
this.addNodeToParentCache(node, parentNode);
return node;
}

private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode): TreeNode {
return {
item: this.createTreeItem(element, extensionTreeItem, parent),
parent,
children: void 0
};
}

private createTreeItem(element: T, extensionTreeItem: vscode.TreeItem, parent?: TreeNode): ITreeItem {

const handle = this.createHandle(element, extensionTreeItem, parent);
const icon = this.getLightIconPath(extensionTreeItem);
const item = {
handle,
parentHandle,
parentHandle: parent ? parent.item.handle : void 0,
label: extensionTreeItem.label,
resourceUri: extensionTreeItem.resourceUri,
tooltip: typeof extensionTreeItem.tooltip === 'string' ? extensionTreeItem.tooltip : void 0,
Expand All @@ -192,19 +239,22 @@ class ExtHostTreeView<T> extends Disposable {
iconDark: this.getDarkIconPath(extensionTreeItem) || icon,
collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState
};

return item;
}

private createHandle(element: T, { id, label, resourceUri }: vscode.TreeItem, parentHandle?: TreeItemHandle): TreeItemHandle {
private createHandle(element: T, { id, label, resourceUri }: vscode.TreeItem, parent?: TreeNode): TreeItemHandle {
if (id) {
return `${ExtHostTreeView.ID_HANDLE_PREFIX}/${id}`;
}

const prefix = parentHandle ? parentHandle : ExtHostTreeView.LABEL_HANDLE_PREFIX;
const prefix: string = parent ? parent.item.handle : ExtHostTreeView.LABEL_HANDLE_PREFIX;
let elementId = label ? label : resourceUri ? basename(resourceUri.path) : '';
elementId = elementId.indexOf('/') !== -1 ? elementId.replace('/', '//') : elementId;
const existingHandle = this.nodes.has(element) ? this.nodes.get(element).handle : void 0;
const existingHandle = this.nodes.has(element) ? this.nodes.get(element).item.handle : void 0;
const childrenNodes = (this.getChildrenNodes(parent) || []);

for (let counter = 0; counter <= this.getChildrenHandles(parentHandle).length; counter++) {
for (let counter = 0; counter <= childrenNodes.length; counter++) {
const handle = `${prefix}/${counter}:${elementId}`;
if (!this.elements.has(handle) || existingHandle === handle) {
return handle;
Expand Down Expand Up @@ -238,69 +288,77 @@ class ExtHostTreeView<T> extends Disposable {
return URI.file(iconPath).toString();
}

private getChildrenHandles(parentHandle?: TreeItemHandle): TreeItemHandle[] {
return parentHandle ? this.nodes.get(this.getExtensionElement(parentHandle)).childrenHandles : this.rootHandles;
private addNodeToCache(element: T, node: TreeNode): void {
this.elements.set(node.item.handle, element);
this.nodes.set(element, node);
}

private update(element: T, handle: TreeItemHandle, parentHandle: TreeItemHandle): void {
const node = this.nodes.get(element);
const childrenHandles = this.getChildrenHandles(parentHandle);
private updateNodeCache(element: T, newNode: TreeNode, existing: TreeNode, parentNode: TreeNode): void {
// Remove from the cache
this.elements.delete(newNode.item.handle);
this.nodes.delete(element);
if (newNode.item.handle !== existing.item.handle) {
this.elements.delete(existing.item.handle);
}

// Add the new node to the cache
this.addNodeToCache(element, newNode);

// Update parent node
if (node) {
if (node.handle !== handle) {
// Remove the old handle from the system
this.elements.delete(node.handle);
childrenHandles[childrenHandles.indexOf(node.handle)] = handle;
// Replace the node in parent's children nodes
const childrenNodes = (this.getChildrenNodes(parentNode) || []);
const childNode = childrenNodes.filter(c => c.item.handle === existing.item.handle)[0];
if (childNode) {
childrenNodes.splice(childrenNodes.indexOf(childNode), 1, newNode);
}
}

this.clearChildren(element);
private addNodeToParentCache(node: TreeNode, parentNode: TreeNode): void {
if (parentNode) {
if (!parentNode.children) {
parentNode.children = [];
}
parentNode.children.push(node);
} else {
childrenHandles.push(handle);
if (!this.roots) {
this.roots = [];
}
this.roots.push(node);
}

// Update element maps
this.elements.set(handle, element);
this.nodes.set(element, {
handle,
parentHandle,
childrenHandles: node ? node.childrenHandles : []
});
}

private clearChildren(parentElement?: T): void {
if (parentElement) {
let node = this.nodes.get(parentElement);
if (node.childrenHandles) {
for (const childHandle of node.childrenHandles) {
const childEleement = this.elements.get(childHandle);
if (node.children) {
for (const child of node.children) {
const childEleement = this.elements.get(child.item.handle);
if (childEleement) {
this.clear(childEleement);
}
}
}
node.childrenHandles = [];
node.children = [];
} else {
this.clearAll();
}
}

private clear(element: T): void {
let node = this.nodes.get(element);
if (node.childrenHandles) {
for (const childHandle of node.childrenHandles) {
const childEleement = this.elements.get(childHandle);
if (node.children) {
for (const child of node.children) {
const childEleement = this.elements.get(child.item.handle);
if (childEleement) {
this.clear(childEleement);
}
}
}
this.nodes.delete(element);
this.elements.delete(node.handle);
this.elements.delete(node.item.handle);
}

private clearAll(): void {
this.rootHandles = [];
this.roots = null;
this.elements.clear();
this.nodes.clear();
}
Expand Down
Loading

0 comments on commit a4cfd33

Please sign in to comment.