diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ae2c48a5ea8166ed877ab867baabd7390b11c2f --- /dev/null +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -0,0 +1,266 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./tree'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IListOptions, List, IIdentityProvider, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget'; +import { IVirtualDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list'; +import { append, $ } from 'vs/base/browser/dom'; +import { Event, Relay, chain } from 'vs/base/common/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { ITreeModel, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { ISpliceable } from 'vs/base/common/sequence'; +import { IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/indexTreeModel'; + +function toTreeListOptions(options?: IListOptions): IListOptions> { + if (!options) { + return undefined; + } + + let identityProvider: IIdentityProvider> | undefined = undefined; + let multipleSelectionController: IMultipleSelectionController> | undefined = undefined; + + if (options.identityProvider) { + identityProvider = el => options.identityProvider(el.element); + } + + if (options.multipleSelectionController) { + multipleSelectionController = { + isSelectionSingleChangeEvent(e) { + return options.multipleSelectionController.isSelectionSingleChangeEvent({ ...e, element: e.element } as any); + }, + isSelectionRangeChangeEvent(e) { + return options.multipleSelectionController.isSelectionRangeChangeEvent({ ...e, element: e.element } as any); + } + }; + } + + return { + ...options, + identityProvider, + multipleSelectionController + }; +} + +class TreeDelegate implements IVirtualDelegate> { + + constructor(private delegate: IVirtualDelegate) { } + + getHeight(element: ITreeNode): number { + return this.delegate.getHeight(element.element); + } + + getTemplateId(element: ITreeNode): string { + return this.delegate.getTemplateId(element.element); + } +} + +interface ITreeListTemplateData { + twistie: HTMLElement; + count: HTMLElement; + templateData: T; +} + +function renderTwistie(node: ITreeNode, twistie: HTMLElement): void { + if (node.children.length === 0 && !node.collapsible) { + twistie.innerText = ''; + } else { + twistie.innerText = node.collapsed ? '▹' : '◢'; + } +} + +class TreeRenderer implements IRenderer, ITreeListTemplateData> { + + readonly templateId: string; + private renderedNodes = new Map, ITreeListTemplateData>(); + private disposables: IDisposable[] = []; + + constructor( + private renderer: IRenderer, + onDidChangeCollapseState: Event> + ) { + this.templateId = renderer.templateId; + onDidChangeCollapseState(this.onDidChangeCollapseState, this, this.disposables); + } + + renderTemplate(container: HTMLElement): ITreeListTemplateData { + const el = append(container, $('.monaco-tl-row')); + const twistie = append(el, $('.tl-twistie')); + const contents = append(el, $('.tl-contents')); + const count = append(el, $('.tl-count')); + const templateData = this.renderer.renderTemplate(contents); + + return { twistie, count, templateData }; + } + + renderElement(node: ITreeNode, index: number, templateData: ITreeListTemplateData): void { + this.renderedNodes.set(node, templateData); + + templateData.twistie.style.width = `${10 + node.depth * 10}px`; + renderTwistie(node, templateData.twistie); + templateData.count.textContent = `${node.revealedCount}`; + + this.renderer.renderElement(node.element, index, templateData.templateData); + } + + disposeElement(node: ITreeNode): void { + this.renderedNodes.delete(node); + } + + disposeTemplate(templateData: ITreeListTemplateData): void { + this.renderer.disposeTemplate(templateData.templateData); + } + + private onDidChangeCollapseState(node: ITreeNode): void { + const templateData = this.renderedNodes.get(node); + + if (!templateData) { + return; + } + + renderTwistie(node, templateData.twistie); + templateData.count.textContent = `${node.revealedCount}`; + } + + dispose(): void { + this.renderedNodes.clear(); + this.disposables = dispose(this.disposables); + } +} + +function isInputElement(e: HTMLElement): boolean { + return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA'; +} + +export interface ITreeOptions extends IListOptions, IIndexTreeModelOptions { } + +export abstract class AbstractTree implements IDisposable { + + private view: List>; + protected model: ITreeModel; + protected disposables: IDisposable[] = []; + + constructor( + container: HTMLElement, + delegate: IVirtualDelegate, + renderers: IRenderer[], + options?: ITreeOptions + ) { + const treeDelegate = new TreeDelegate(delegate); + + const onDidChangeCollapseStateRelay = new Relay>(); + const treeRenderers = renderers.map(r => new TreeRenderer(r, onDidChangeCollapseStateRelay.event)); + this.disposables.push(...treeRenderers); + + this.view = new List(container, treeDelegate, treeRenderers, toTreeListOptions(options)); + this.model = this.createModel(this.view, options); + onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState; + + this.view.onMouseClick(this.onMouseClick, this, this.disposables); + + const onKeyDown = chain(this.view.onKeyDown) + .filter(e => !isInputElement(e.target as HTMLElement)) + .map(e => new StandardKeyboardEvent(e)); + + onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables); + } + + protected abstract createModel(view: ISpliceable>, options: ITreeOptions): ITreeModel; + + // collapseAll(): void { + // this.model.setCollapsedAll(true); + // } + + refilter(): void { + this.model.refilter(); + } + + private onMouseClick(e: IListMouseEvent>): void { + const node = e.element; + const location = this.model.getNodeLocation(node); + + this.model.toggleCollapsed(location); + } + + private onLeftArrow(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + + const nodes = this.view.getFocusedElements(); + + if (nodes.length === 0) { + return; + } + + const node = nodes[0]; + const location = this.model.getNodeLocation(node); + const didChange = this.model.setCollapsed(location, true); + + if (!didChange) { + const parentLocation = this.model.getParentNodeLocation(location); + + if (parentLocation === null) { + return; + } + + const parentListIndex = this.model.getListIndex(parentLocation); + + this.view.reveal(parentListIndex); + this.view.setFocus([parentListIndex]); + } + } + + private onRightArrow(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + + const nodes = this.view.getFocusedElements(); + + if (nodes.length === 0) { + return; + } + + const node = nodes[0]; + const location = this.model.getNodeLocation(node); + const didChange = this.model.setCollapsed(location, false); + + if (!didChange) { + if (node.children.length === 0) { + return; + } + + const [focusedIndex] = this.view.getFocus(); + const firstChildIndex = focusedIndex + 1; + + this.view.reveal(firstChildIndex); + this.view.setFocus([firstChildIndex]); + } + } + + private onSpace(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + + const nodes = this.view.getFocusedElements(); + + if (nodes.length === 0) { + return; + } + + const node = nodes[0]; + const location = this.model.getNodeLocation(node); + this.model.toggleCollapsed(location); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + this.view.dispose(); + this.view = null; + this.model = null; + } +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/tree/indexTree.ts b/src/vs/base/browser/ui/tree/indexTree.ts new file mode 100644 index 0000000000000000000000000000000000000000..86b19455381394b9aa4b8965130e118e5014186c --- /dev/null +++ b/src/vs/base/browser/ui/tree/indexTree.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./tree'; +import { Iterator, ISequence } from 'vs/base/common/iterator'; +import { AbstractTree, ITreeOptions } from 'vs/base/browser/ui/tree/abstractTree'; +import { ISpliceable } from 'vs/base/common/sequence'; +import { IndexTreeModel } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { ITreeElement, ITreeModel, ITreeNode } from 'vs/base/browser/ui/tree/tree'; + +export class IndexTree extends AbstractTree { + + protected model: IndexTreeModel; + + splice(location: number[], deleteCount: number, toInsert: ISequence> = Iterator.empty()): Iterator> { + return this.model.splice(location, deleteCount, toInsert); + } + + protected createModel(view: ISpliceable>, options: ITreeOptions): ITreeModel { + return new IndexTreeModel(view, options); + } +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/tree/treeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts similarity index 84% rename from src/vs/base/browser/ui/tree/treeModel.ts rename to src/vs/base/browser/ui/tree/indexTreeModel.ts index cc1ad0fa8d3be94fff42e6d19e6e7f5d0a5dbd93..0ae88bd661d853203fbfe117b03699948f171260 100644 --- a/src/vs/base/browser/ui/tree/treeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -6,24 +6,8 @@ import { ISpliceable } from 'vs/base/common/sequence'; import { Iterator, ISequence } from 'vs/base/common/iterator'; import { Emitter, Event } from 'vs/base/common/event'; - -export interface ITreeElement { - readonly element: T; - readonly children?: Iterator> | ITreeElement[]; - readonly collapsible?: boolean; - readonly collapsed?: boolean; -} - -export interface ITreeNode { - readonly parent: ITreeNode | undefined; - readonly element: T; - readonly children: ITreeNode[]; - readonly depth: number; - readonly collapsible: boolean; - readonly collapsed: boolean; - readonly revealedCount: number; - readonly filterData: TFilterData | undefined; -} +import { tail2 } from 'vs/base/common/arrays'; +import { ITreeFilterResult, TreeVisibility, ITreeFilter, ITreeOptions, ITreeModel, ITreeNode, ITreeElement } from 'vs/base/browser/ui/tree/tree'; interface IMutableTreeNode extends ITreeNode { readonly parent: IMutableTreeNode | undefined; @@ -35,25 +19,10 @@ interface IMutableTreeNode extends ITreeNode { visible: boolean; } -export const enum Visibility { - Hidden, - Visible, - Recurse // TODO@joao come up with a better name -} - -export interface IFilterResult { - visibility: Visibility; - data: TFilterData; -} - -function isFilterResult(obj: any): obj is IFilterResult { +function isFilterResult(obj: any): obj is ITreeFilterResult { return typeof obj === 'object' && 'visibility' in obj && 'data' in obj; } -export interface ITreeFilter { - filter(element: T): boolean | Visibility | IFilterResult; -} - function treeNodeToElement(node: IMutableTreeNode): ITreeElement { const { element, collapsed } = node; const children = Iterator.map(Iterator.fromArray(node.children), treeNodeToElement); @@ -61,31 +30,17 @@ function treeNodeToElement(node: IMutableTreeNode): ITreeElement { return { element, children, collapsed }; } -function getVisibleState(visibility: Visibility): boolean | undefined { +function getVisibleState(visibility: TreeVisibility): boolean | undefined { switch (visibility) { - case Visibility.Hidden: return false; - case Visibility.Visible: return true; - case Visibility.Recurse: return undefined; + case TreeVisibility.Hidden: return false; + case TreeVisibility.Visible: return true; + case TreeVisibility.Recurse: return undefined; } } -export interface ITreeModelOptions { - filter?: ITreeFilter; -} - -export class TreeModel { - - // TODO@joao perf! - static getNodeLocation(node: ITreeNode): number[] { - const location = []; - - while (node.parent) { - location.push(node.parent.children.indexOf(node)); - node = node.parent; - } +export interface IIndexTreeModelOptions extends ITreeOptions { } - return location.reverse(); - } +export class IndexTreeModel implements ITreeModel { private root: IMutableTreeNode = { parent: undefined, @@ -104,7 +59,7 @@ export class TreeModel { private filter?: ITreeFilter; - constructor(private list: ISpliceable>, options: ITreeModelOptions = {}) { + constructor(private list: ISpliceable>, options: IIndexTreeModelOptions = {}) { this.filter = options.filter; } @@ -167,28 +122,28 @@ export class TreeModel { this._setCollapsed(node, listIndex, revealed); } - // TODO@joao cleanup - setCollapsedAll(collapsed: boolean): void { - if (collapsed) { - const queue = [...this.root.children]; // TODO@joao use a linked list - let listIndex = 0; + // // TODO@joao cleanup + // setCollapsedAll(collapsed: boolean): void { + // if (collapsed) { + // const queue = [...this.root.children]; // TODO@joao use a linked list + // let listIndex = 0; - while (queue.length > 0) { - const node = queue.shift(); - const revealed = listIndex < this.root.children.length; - this._setCollapsed(node, listIndex, revealed, collapsed); + // while (queue.length > 0) { + // const node = queue.shift(); + // const revealed = listIndex < this.root.children.length; + // this._setCollapsed(node, listIndex, revealed, collapsed); - queue.push(...node.children); - listIndex++; - } - } - } + // queue.push(...node.children); + // listIndex++; + // } + // } + // } isCollapsed(location: number[]): boolean { return this.findNode(location).node.collapsed; } - refilter(/* location?: number[] */): void { + refilter(): void { const previousRevealedCount = this.root.revealedCount; const toInsert = this.updateNodeAfterFilterChange(this.root); this.list.splice(0, previousRevealedCount, toInsert); @@ -369,7 +324,7 @@ export class TreeModel { } private _filterNode(node: IMutableTreeNode): boolean | undefined { - const result = this.filter ? this.filter.filter(node.element) : Visibility.Visible; + const result = this.filter ? this.filter.filter(node.element) : TreeVisibility.Visible; if (typeof result === 'boolean') { node.filterData = undefined; @@ -416,4 +371,24 @@ export class TreeModel { return this.findParentNode(rest, node.children[index], listIndex + 1, revealed); } + + // TODO@joao perf! + getNodeLocation(node: ITreeNode): number[] { + const location = []; + + while (node.parent) { + location.push(node.parent.children.indexOf(node)); + node = node.parent; + } + + return location.reverse(); + } + + getParentNodeLocation(location: number[]): number[] | null { + if (location.length <= 1) { + return null; + } + + return tail2(location)[0]; + } } \ No newline at end of file diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba4f2586bc5804809597fd3fc858ce8aca165828 --- /dev/null +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./tree'; +import { Iterator, ISequence } from 'vs/base/common/iterator'; +import { AbstractTree, ITreeOptions } from 'vs/base/browser/ui/tree/abstractTree'; +import { ISpliceable } from 'vs/base/common/sequence'; +import { ITreeNode, ITreeModel, ITreeElement } from 'vs/base/browser/ui/tree/tree'; +import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; + +export class ObjectTree extends AbstractTree> { + + protected model: ObjectTreeModel; + + setChildren(element: T | null, children?: ISequence>): Iterator> { + return this.model.setChildren(element, children); + } + + protected createModel(view: ISpliceable>, options: ITreeOptions): ITreeModel> { + return new ObjectTreeModel(view, options); + } +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 8fb4402bf6c13d1968b00f75478860ace2572505..ba25be7bca7d72d32e2d176d2110c45100b9b421 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -7,25 +7,28 @@ import { ISpliceable } from 'vs/base/common/sequence'; import { Iterator, ISequence } from 'vs/base/common/iterator'; -import { TreeModel, ITreeNode, ITreeModelOptions, ITreeElement } from 'vs/base/browser/ui/tree/treeModel'; +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'; -export class TreeObjectModel, TFilterData = void> { +export interface IObjectTreeModelOptions extends IIndexTreeModelOptions { } - private model: TreeModel; +export class ObjectTreeModel, TFilterData = void> implements ITreeModel> { + + private model: IndexTreeModel; private nodes = new Map>(); readonly onDidChangeCollapseState: Event>; get size(): number { return this.nodes.size; } - constructor(list: ISpliceable>, options: ITreeModelOptions = {}) { - this.model = new TreeModel(list, options); + constructor(list: ISpliceable>, options: IObjectTreeModelOptions = {}) { + this.model = new IndexTreeModel(list, options); this.onDidChangeCollapseState = this.model.onDidChangeCollapseState; } setChildren(element: T | null, children?: ISequence>): Iterator> { - const location = this.getLocation(element); + const location = this.getElementLocation(element); const insertedElements = new Set(); const onDidCreateNode = (node: ITreeNode) => { @@ -42,23 +45,23 @@ export class TreeObjectModel, TFilterData = void> { return this.model.splice([...location, 0], Number.MAX_VALUE, children, onDidCreateNode, onDidDeleteNode); } - getListIndex(element: T): number { - const location = this.getLocation(element); + getListIndex(node: ITreeNode): number { + const location = this.getElementLocation(node.element); return this.model.getListIndex(location); } - setCollapsed(element: T, collapsed: boolean): boolean { - const location = this.getLocation(element); + setCollapsed(node: ITreeNode, collapsed: boolean): boolean { + const location = this.getElementLocation(node.element); return this.model.setCollapsed(location, collapsed); } - toggleCollapsed(element: T): void { - const location = this.getLocation(element); + toggleCollapsed(node: ITreeNode): void { + const location = this.getElementLocation(node.element); this.model.toggleCollapsed(location); } - isCollapsed(element: T): boolean { - const location = this.getLocation(element); + isCollapsed(node: ITreeNode): boolean { + const location = this.getElementLocation(node.element); return this.model.isCollapsed(location); } @@ -66,7 +69,7 @@ export class TreeObjectModel, TFilterData = void> { this.model.refilter(); } - private getLocation(element: T | null): number[] { + private getElementLocation(element: T | null): number[] { if (element === null) { return []; } @@ -77,6 +80,14 @@ export class TreeObjectModel, TFilterData = void> { throw new Error(`Tree element not found: ${element}`); } - return TreeModel.getNodeLocation(node); + return this.model.getNodeLocation(node); + } + + getNodeLocation(node: ITreeNode): ITreeNode { + return node; + } + + getParentNodeLocation(node: ITreeNode): ITreeNode | null { + return node.parent || null; } } \ No newline at end of file diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 70b5083500fc0d3483c595d94d7361cd3ac4c0be..52bf83392acd75e8e82792e6581de355239eca40 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -3,265 +3,55 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./tree'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IListOptions, List, IIdentityProvider, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget'; -import { TreeModel, ITreeNode, ITreeElement, ITreeModelOptions } from 'vs/base/browser/ui/tree/treeModel'; -import { Iterator, ISequence } from 'vs/base/common/iterator'; -import { IVirtualDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list'; -import { append, $ } from 'vs/base/browser/dom'; -import { Event, Relay, chain } from 'vs/base/common/event'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { tail2 } from 'vs/base/common/arrays'; +import { Event } from 'vs/base/common/event'; +import { Iterator } from 'vs/base/common/iterator'; -function toTreeListOptions(options?: IListOptions): IListOptions> { - if (!options) { - return undefined; - } - - let identityProvider: IIdentityProvider> | undefined = undefined; - let multipleSelectionController: IMultipleSelectionController> | undefined = undefined; - - if (options.identityProvider) { - identityProvider = el => options.identityProvider(el.element); - } - - if (options.multipleSelectionController) { - multipleSelectionController = { - isSelectionSingleChangeEvent(e) { - return options.multipleSelectionController.isSelectionSingleChangeEvent({ ...e, element: e.element } as any); - }, - isSelectionRangeChangeEvent(e) { - return options.multipleSelectionController.isSelectionRangeChangeEvent({ ...e, element: e.element } as any); - } - }; - } - - return { - ...options, - identityProvider, - multipleSelectionController - }; +export const enum TreeVisibility { + Hidden, + Visible, + Recurse // TODO@joao come up with a better name } -class TreeDelegate implements IVirtualDelegate> { - - constructor(private delegate: IVirtualDelegate) { } - - getHeight(element: ITreeNode): number { - return this.delegate.getHeight(element.element); - } - - getTemplateId(element: ITreeNode): string { - return this.delegate.getTemplateId(element.element); - } +export interface ITreeFilterResult { + visibility: TreeVisibility; + data: TFilterData; } -interface ITreeListTemplateData { - twistie: HTMLElement; - count: HTMLElement; - templateData: T; +export interface ITreeFilter { + filter(element: T): boolean | TreeVisibility | ITreeFilterResult; } -function renderTwistie(node: ITreeNode, twistie: HTMLElement): void { - if (node.children.length === 0 && !node.collapsible) { - twistie.innerText = ''; - } else { - twistie.innerText = node.collapsed ? '▹' : '◢'; - } +export interface ITreeOptions { + filter?: ITreeFilter; } -class TreeRenderer implements IRenderer, ITreeListTemplateData> { - - readonly templateId: string; - private renderedNodes = new Map, ITreeListTemplateData>(); - private disposables: IDisposable[] = []; - - constructor( - private renderer: IRenderer, - onDidChangeCollapseState: Event> - ) { - this.templateId = renderer.templateId; - onDidChangeCollapseState(this.onDidChangeCollapseState, this, this.disposables); - } - - renderTemplate(container: HTMLElement): ITreeListTemplateData { - const el = append(container, $('.monaco-tl-row')); - const twistie = append(el, $('.tl-twistie')); - const contents = append(el, $('.tl-contents')); - const count = append(el, $('.tl-count')); - const templateData = this.renderer.renderTemplate(contents); - - return { twistie, count, templateData }; - } - - renderElement(node: ITreeNode, index: number, templateData: ITreeListTemplateData): void { - this.renderedNodes.set(node, templateData); - - templateData.twistie.style.width = `${10 + node.depth * 10}px`; - renderTwistie(node, templateData.twistie); - templateData.count.textContent = `${node.revealedCount}`; - - this.renderer.renderElement(node.element, index, templateData.templateData); - } - - disposeElement(node: ITreeNode): void { - this.renderedNodes.delete(node); - } - - disposeTemplate(templateData: ITreeListTemplateData): void { - this.renderer.disposeTemplate(templateData.templateData); - } - - private onDidChangeCollapseState(node: ITreeNode): void { - const templateData = this.renderedNodes.get(node); - - if (!templateData) { - return; - } - - renderTwistie(node, templateData.twistie); - templateData.count.textContent = `${node.revealedCount}`; - } - - dispose(): void { - this.renderedNodes.clear(); - this.disposables = dispose(this.disposables); - } +export interface ITreeElement { + readonly element: T; + readonly children?: Iterator> | ITreeElement[]; + readonly collapsible?: boolean; + readonly collapsed?: boolean; } -function isInputElement(e: HTMLElement): boolean { - return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA'; +export interface ITreeNode { + readonly parent: ITreeNode | undefined; + readonly element: T; + readonly children: ITreeNode[]; + readonly depth: number; + readonly collapsible: boolean; + readonly collapsed: boolean; + readonly revealedCount: number; + readonly filterData: TFilterData | undefined; } -export interface ITreeOptions extends IListOptions, ITreeModelOptions { } - -export class Tree implements IDisposable { - - private view: List>; - private model: TreeModel; - private disposables: IDisposable[] = []; - - constructor( - container: HTMLElement, - delegate: IVirtualDelegate, - renderers: IRenderer[], - options?: ITreeOptions - ) { - const treeDelegate = new TreeDelegate(delegate); - - const onDidChangeCollapseStateRelay = new Relay>(); - const treeRenderers = renderers.map(r => new TreeRenderer(r, onDidChangeCollapseStateRelay.event)); - this.disposables.push(...treeRenderers); - - this.view = new List(container, treeDelegate, treeRenderers, toTreeListOptions(options)); - this.model = new TreeModel(this.view, options); - onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState; - - this.view.onMouseClick(this.onMouseClick, this, this.disposables); - - const onKeyDown = chain(this.view.onKeyDown) - .filter(e => !isInputElement(e.target as HTMLElement)) - .map(e => new StandardKeyboardEvent(e)); - - onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables); - onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables); - onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables); - } - - splice(location: number[], deleteCount: number, toInsert: ISequence> = Iterator.empty()): Iterator> { - return this.model.splice(location, deleteCount, toInsert); - } - - collapseAll(): void { - this.model.setCollapsedAll(true); - } - - refilter(): void { - this.model.refilter(); - } - - private onMouseClick(e: IListMouseEvent>): void { - const node = e.element; - const location = TreeModel.getNodeLocation(node); - - this.model.toggleCollapsed(location); - } - - private onLeftArrow(e: StandardKeyboardEvent): void { - e.preventDefault(); - e.stopPropagation(); - - const nodes = this.view.getFocusedElements(); - - if (nodes.length === 0) { - return; - } - - const node = nodes[0]; - const location = TreeModel.getNodeLocation(node); - const didChange = this.model.setCollapsed(location, true); - - if (!didChange) { - if (location.length === 1) { - return; - } - - const [parentLocation] = tail2(location); - const parentListIndex = this.model.getListIndex(parentLocation); - - this.view.reveal(parentListIndex); - this.view.setFocus([parentListIndex]); - } - } - - private onRightArrow(e: StandardKeyboardEvent): void { - e.preventDefault(); - e.stopPropagation(); - - const nodes = this.view.getFocusedElements(); - - if (nodes.length === 0) { - return; - } - - const node = nodes[0]; - const location = TreeModel.getNodeLocation(node); - const didChange = this.model.setCollapsed(location, false); - - if (!didChange) { - if (node.children.length === 0) { - return; - } - - const [focusedIndex] = this.view.getFocus(); - const firstChildIndex = focusedIndex + 1; - - this.view.reveal(firstChildIndex); - this.view.setFocus([firstChildIndex]); - } - } - - private onSpace(e: StandardKeyboardEvent): void { - e.preventDefault(); - e.stopPropagation(); - - const nodes = this.view.getFocusedElements(); - - if (nodes.length === 0) { - return; - } +export interface ITreeModel { + onDidChangeCollapseState: Event>; - const node = nodes[0]; - const location = TreeModel.getNodeLocation(node); - this.model.toggleCollapsed(location); - } + getListIndex(ref: TRef): number; + setCollapsed(ref: TRef, collapsed: boolean): boolean; + toggleCollapsed(ref: TRef): void; + isCollapsed(ref: TRef): boolean; + refilter(): void; - dispose(): void { - this.disposables = dispose(this.disposables); - this.view.dispose(); - this.view = null; - this.model = null; - } + getNodeLocation(node: ITreeNode): TRef; + getParentNodeLocation(location: TRef): TRef | null; } \ No newline at end of file diff --git a/src/vs/base/test/browser/ui/tree/treeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts similarity index 82% rename from src/vs/base/test/browser/ui/tree/treeModel.test.ts rename to src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index 9215f39d0b010fe1926e8e75f3d07dfa7a7b954f..53d2add5df412b812559dc13879b344c572dd3eb 100644 --- a/src/vs/base/test/browser/ui/tree/treeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TreeModel, ITreeNode, ITreeFilter, Visibility } from 'vs/base/browser/ui/tree/treeModel'; +import { ITreeNode, ITreeFilter, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; import { Iterator } from 'vs/base/common/iterator'; +import { IndexTreeModel } from 'vs/base/browser/ui/tree/indexTreeModel'; function toSpliceable(arr: T[]): ISpliceable { return { @@ -20,18 +21,18 @@ function toArray(list: ITreeNode[]): T[] { return list.map(i => i.element); } -suite('TreeModel2', function () { +suite('IndexTreeModel', function () { test('ctor', () => { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); assert(model); assert.equal(list.length, 0); }); test('insert', () => { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { element: 0 }, @@ -53,7 +54,7 @@ suite('TreeModel2', function () { test('deep insert', function () { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -90,7 +91,7 @@ suite('TreeModel2', function () { test('deep insert collapsed', function () { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -118,7 +119,7 @@ suite('TreeModel2', function () { test('delete', () => { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { element: 0 }, @@ -143,7 +144,7 @@ suite('TreeModel2', function () { test('nested delete', function () { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -177,7 +178,7 @@ suite('TreeModel2', function () { test('deep delete', function () { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -205,7 +206,7 @@ suite('TreeModel2', function () { test('hidden delete', function () { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -230,7 +231,7 @@ suite('TreeModel2', function () { test('collapse', () => { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -261,7 +262,7 @@ suite('TreeModel2', function () { test('expand', () => { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -301,7 +302,7 @@ suite('TreeModel2', function () { test('collapse should recursively adjust visible count', function () { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -335,12 +336,12 @@ suite('TreeModel2', function () { test('simple filter', function () { const list = [] as ITreeNode[]; const filter = new class implements ITreeFilter { - filter(element: number): Visibility { - return element % 2 === 0 ? Visibility.Visible : Visibility.Hidden; + filter(element: number): TreeVisibility { + return element % 2 === 0 ? TreeVisibility.Visible : TreeVisibility.Hidden; } }; - const model = new TreeModel(toSpliceable(list), { filter }); + const model = new IndexTreeModel(toSpliceable(list), { filter }); model.splice([0], 0, Iterator.fromArray([ { @@ -369,12 +370,12 @@ suite('TreeModel2', function () { test('recursive filter on initial model', function () { const list = [] as ITreeNode[]; const filter = new class implements ITreeFilter { - filter(element: number): Visibility { - return element === 0 ? Visibility.Recurse : Visibility.Hidden; + filter(element: number): TreeVisibility { + return element === 0 ? TreeVisibility.Recurse : TreeVisibility.Hidden; } }; - const model = new TreeModel(toSpliceable(list), { filter }); + const model = new IndexTreeModel(toSpliceable(list), { filter }); model.splice([0], 0, Iterator.fromArray([ { @@ -392,12 +393,12 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; let shouldFilter = false; const filter = new class implements ITreeFilter { - filter(element: number): Visibility { - return (!shouldFilter || element % 2 === 0) ? Visibility.Visible : Visibility.Hidden; + filter(element: number): TreeVisibility { + return (!shouldFilter || element % 2 === 0) ? TreeVisibility.Visible : TreeVisibility.Hidden; } }; - const model = new TreeModel(toSpliceable(list), { filter }); + const model = new IndexTreeModel(toSpliceable(list), { filter }); model.splice([0], 0, Iterator.fromArray([ { @@ -431,12 +432,12 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; let query = new RegExp(''); const filter = new class implements ITreeFilter { - filter(element: string): Visibility { - return query.test(element) ? Visibility.Visible : Visibility.Recurse; + filter(element: string): TreeVisibility { + return query.test(element) ? TreeVisibility.Visible : TreeVisibility.Recurse; } }; - const model = new TreeModel(toSpliceable(list), { filter }); + const model = new IndexTreeModel(toSpliceable(list), { filter }); model.splice([0], 0, Iterator.fromArray([ { @@ -477,12 +478,12 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; let query = new RegExp(''); const filter = new class implements ITreeFilter { - filter(element: string): Visibility { - return query.test(element) ? Visibility.Visible : Visibility.Recurse; + filter(element: string): TreeVisibility { + return query.test(element) ? TreeVisibility.Visible : TreeVisibility.Recurse; } }; - const model = new TreeModel(toSpliceable(list), { filter }); + const model = new IndexTreeModel(toSpliceable(list), { filter }); model.splice([0], 0, Iterator.fromArray([ { @@ -523,12 +524,12 @@ suite('TreeModel2', function () { const list = [] as ITreeNode[]; let query = new RegExp(''); const filter = new class implements ITreeFilter { - filter(element: string): Visibility { - return query.test(element) ? Visibility.Visible : Visibility.Recurse; + filter(element: string): TreeVisibility { + return query.test(element) ? TreeVisibility.Visible : TreeVisibility.Recurse; } }; - const model = new TreeModel(toSpliceable(list), { filter }); + const model = new IndexTreeModel(toSpliceable(list), { filter }); model.splice([0], 0, Iterator.fromArray([ { @@ -576,7 +577,7 @@ suite('TreeModel2', function () { test('simple', function () { const list = [] as ITreeNode[]; - const model = new TreeModel(toSpliceable(list)); + const model = new IndexTreeModel(toSpliceable(list)); model.splice([0], 0, Iterator.fromArray([ { @@ -590,23 +591,23 @@ suite('TreeModel2', function () { { element: 2 } ])); - assert.deepEqual(TreeModel.getNodeLocation(list[0]), [0]); - assert.deepEqual(TreeModel.getNodeLocation(list[1]), [0, 0]); - assert.deepEqual(TreeModel.getNodeLocation(list[2]), [0, 1]); - assert.deepEqual(TreeModel.getNodeLocation(list[3]), [0, 2]); - assert.deepEqual(TreeModel.getNodeLocation(list[4]), [1]); - assert.deepEqual(TreeModel.getNodeLocation(list[5]), [2]); + assert.deepEqual(model.getNodeLocation(list[0]), [0]); + assert.deepEqual(model.getNodeLocation(list[1]), [0, 0]); + assert.deepEqual(model.getNodeLocation(list[2]), [0, 1]); + assert.deepEqual(model.getNodeLocation(list[3]), [0, 2]); + assert.deepEqual(model.getNodeLocation(list[4]), [1]); + assert.deepEqual(model.getNodeLocation(list[5]), [2]); }); test('with filter', function () { const list = [] as ITreeNode[]; const filter = new class implements ITreeFilter { - filter(element: number): Visibility { - return element % 2 === 0 ? Visibility.Visible : Visibility.Hidden; + filter(element: number): TreeVisibility { + return element % 2 === 0 ? TreeVisibility.Visible : TreeVisibility.Hidden; } }; - const model = new TreeModel(toSpliceable(list), { filter }); + const model = new IndexTreeModel(toSpliceable(list), { filter }); model.splice([0], 0, Iterator.fromArray([ { @@ -622,10 +623,10 @@ suite('TreeModel2', function () { } ])); - assert.deepEqual(TreeModel.getNodeLocation(list[0]), [0]); - assert.deepEqual(TreeModel.getNodeLocation(list[1]), [0, 1]); - assert.deepEqual(TreeModel.getNodeLocation(list[2]), [0, 3]); - assert.deepEqual(TreeModel.getNodeLocation(list[3]), [0, 5]); + assert.deepEqual(model.getNodeLocation(list[0]), [0]); + assert.deepEqual(model.getNodeLocation(list[1]), [0, 1]); + assert.deepEqual(model.getNodeLocation(list[2]), [0, 3]); + assert.deepEqual(model.getNodeLocation(list[3]), [0, 5]); }); }); }); \ No newline at end of file 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 fc26e930268f19077499af1ad4332084c982d7b0..c1ec64cb8b63478ac25d517d1685994dd2cf13dd 100644 --- a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ITreeNode } from 'vs/base/browser/ui/tree/treeModel'; +import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; -import { TreeObjectModel } from 'vs/base/browser/ui/tree/treeObjectModel'; +import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { Iterator } from 'vs/base/common/iterator'; function toSpliceable(arr: T[]): ISpliceable { @@ -21,11 +21,11 @@ function toArray(list: ITreeNode[]): T[] { return list.map(i => i.element); } -suite('TreeObjectModel', function () { +suite('ObjectTreeModel', function () { test('ctor', () => { const list = [] as ITreeNode[]; - const model = new TreeObjectModel(toSpliceable(list)); + const model = new ObjectTreeModel(toSpliceable(list)); assert(model); assert.equal(list.length, 0); assert.equal(model.size, 0); @@ -33,7 +33,7 @@ suite('TreeObjectModel', function () { test('flat', () => { const list = [] as ITreeNode[]; - const model = new TreeObjectModel(toSpliceable(list)); + const model = new ObjectTreeModel(toSpliceable(list)); model.setChildren(null, Iterator.fromArray([ { element: 0 }, @@ -60,7 +60,7 @@ suite('TreeObjectModel', function () { test('nested', () => { const list = [] as ITreeNode[]; - const model = new TreeObjectModel(toSpliceable(list)); + const model = new ObjectTreeModel(toSpliceable(list)); model.setChildren(null, Iterator.fromArray([ { diff --git a/test/tree/public/index.html b/test/tree/public/index.html index 6fe21424d193ba45e0031a7203ce3ea6f4693634..f8ae14082aa05bd53b01d92753974253e4911248 100644 --- a/test/tree/public/index.html +++ b/test/tree/public/index.html @@ -37,7 +37,7 @@ require.config({ baseUrl: '/static' }); - require(['vs/base/browser/ui/tree/tree', 'vs/base/browser/ui/tree/treeModel', 'vs/base/common/iterator'], ({ Tree }, { Visibility }, { iter }) => { + require(['vs/base/browser/ui/tree/indexTree', 'vs/base/browser/ui/tree/tree', 'vs/base/common/iterator'], ({ IndexTree }, { TreeVisibility }, { iter }) => { const delegate = { getHeight() { return 22; }, getTemplateId() { return 'template'; } @@ -71,11 +71,11 @@ perf('refilter', () => tree.refilter()); } filter(el) { - return (this.pattern ? this.pattern.test(el) : true) ? Visibility.Visible : Visibility.Recurse; + return (this.pattern ? this.pattern.test(el) : true) ? TreeVisibility.Visible : TreeVisibility.Recurse; } }; - const tree = new Tree(container, delegate, [renderer], { filter: treeFilter }); + const tree = new IndexTree(container, delegate, [renderer], { filter: treeFilter }); function setModel(model) { performance.mark('before splice');