From b10fc8fc9c9cdd54c746cba210c368304119dbd1 Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Wed, 28 Nov 2018 17:46:56 +0100 Subject: [PATCH] object tree model: preserve collapse with identity provider fixes #63149 --- .../base/browser/ui/tree/objectTreeModel.ts | 58 ++++++++++++++++++- .../browser/ui/tree/objectTreeModel.test.ts | 31 ++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 673dd245000..f466b2534ce 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -4,18 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { ISpliceable } from 'vs/base/common/sequence'; -import { Iterator, ISequence } from 'vs/base/common/iterator'; +import { Iterator, ISequence, getSequenceIterator } from 'vs/base/common/iterator'; import { IndexTreeModel, IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/indexTreeModel'; import { Event } from 'vs/base/common/event'; import { ITreeModel, ITreeNode, ITreeElement } from 'vs/base/browser/ui/tree/tree'; +import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; -export interface IObjectTreeModelOptions extends IIndexTreeModelOptions { } +export interface IObjectTreeModelOptions extends IIndexTreeModelOptions { + identityProvider?: IIdentityProvider; +} export class ObjectTreeModel, TFilterData = void> implements ITreeModel { private model: IndexTreeModel; private nodes = new Map>(); + private identityProvider: IIdentityProvider | undefined = undefined; + private nodesByIdentity: Map> | undefined = undefined; + readonly onDidChangeCollapseState: Event>; readonly onDidChangeRenderNodeCount: Event>; @@ -23,6 +29,12 @@ export class ObjectTreeModel, TFilterData = void> imp constructor(list: ISpliceable>, options: IObjectTreeModelOptions = {}) { this.model = new IndexTreeModel(list, options); + this.identityProvider = options.identityProvider; + + if (this.identityProvider) { + this.nodesByIdentity = new Map(); + } + this.onDidChangeCollapseState = this.model.onDidChangeCollapseState; this.onDidChangeRenderNodeCount = this.model.onDidChangeRenderNodeCount; } @@ -35,11 +47,20 @@ export class ObjectTreeModel, TFilterData = void> imp ): Iterator> { const location = this.getElementLocation(element); const insertedElements = new Set(); + let createdNodesByIdentity: Map> | undefined; + + if (this.identityProvider) { + createdNodesByIdentity = new Map(); + } const _onDidCreateNode = (node: ITreeNode) => { insertedElements.add(node.element); this.nodes.set(node.element, node); + if (createdNodesByIdentity) { + createdNodesByIdentity.set(this.identityProvider!.getId(node.element).toString(), node); + } + if (onDidCreateNode) { onDidCreateNode(node); } @@ -50,12 +71,43 @@ export class ObjectTreeModel, TFilterData = void> imp this.nodes.delete(node.element); } + if (this.nodesByIdentity) { + this.nodesByIdentity.delete(this.identityProvider!.getId(node.element).toString()); + } + if (onDidDeleteNode) { onDidDeleteNode(node); } }; - return this.model.splice([...location, 0], Number.MAX_VALUE, children, _onDidCreateNode, _onDidDeleteNode); + const preserveCollapseState = (elements: ISequence>): ISequence> => { + const iterator = getSequenceIterator(elements); + + return Iterator.map(iterator, treeElement => { + const identity = this.identityProvider!.getId(treeElement.element).toString(); + const node = this.nodesByIdentity!.get(identity); + const collapsible = typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : (typeof treeElement.collapsed !== 'undefined'); + + return { + ...treeElement, + collapsible, + collapsed: typeof treeElement.collapsed === 'undefined' ? (node ? (collapsible && node.collapsed) : undefined) : treeElement.collapsed, + children: treeElement.children && preserveCollapseState(treeElement.children) + }; + }); + }; + + if (children && this.identityProvider) { + children = preserveCollapseState(children); + } + + const result = this.model.splice([...location, 0], Number.MAX_VALUE, children, _onDidCreateNode, _onDidDeleteNode); + + if (createdNodesByIdentity) { + createdNodesByIdentity.forEach((node, identity) => this.nodesByIdentity!.set(identity, node)); + } + + return result; } getParentElement(ref: T | null = null): T | null { diff --git a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts index 6aac4a6ad68..7b7a3647a64 100644 --- a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts @@ -140,4 +140,35 @@ suite('ObjectTreeModel', function () { model.setCollapsed(1, false); assert.deepEqual(toArray(list), [1, 11, 111, 112, 2]); }); + + test('collapse state is lost without an identity provider', () => { + const list: ITreeNode[] = []; + const model = new ObjectTreeModel(toSpliceable(list), { collapseByDefault: true }); + const data = [{ element: 'father', children: [{ element: 'child' }] }]; + + model.setChildren(null, data); + assert.deepEqual(toArray(list), ['father']); + + model.setCollapsed('father', false); + assert.deepEqual(toArray(list), ['father', 'child']); + + model.setChildren(null, data); + assert.deepEqual(toArray(list), ['father']); + }); + + test('collapse state is preserved with an identity provider', () => { + const list: ITreeNode[] = []; + const identityProvider = { getId: (name: string) => name }; + const model = new ObjectTreeModel(toSpliceable(list), { collapseByDefault: true, identityProvider }); + const data = [{ element: 'father', children: [{ element: 'child' }] }]; + + model.setChildren(null, data); + assert.deepEqual(toArray(list), ['father']); + + model.setCollapsed('father', false); + assert.deepEqual(toArray(list), ['father', 'child']); + + model.setChildren(null, data); + assert.deepEqual(toArray(list), ['father', 'child']); + }); }); \ No newline at end of file -- GitLab