提交 23fe4acb 编写于 作者: J Joao Moreno

tree: abstract tree

上级 b8f2d15d
/*---------------------------------------------------------------------------------------------
* 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<T>(options?: IListOptions<T>): IListOptions<ITreeNode<T, any>> {
if (!options) {
return undefined;
}
let identityProvider: IIdentityProvider<ITreeNode<T, any>> | undefined = undefined;
let multipleSelectionController: IMultipleSelectionController<ITreeNode<T, any>> | 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<T> implements IVirtualDelegate<ITreeNode<T, any>> {
constructor(private delegate: IVirtualDelegate<T>) { }
getHeight(element: ITreeNode<T, any>): number {
return this.delegate.getHeight(element.element);
}
getTemplateId(element: ITreeNode<T, any>): string {
return this.delegate.getTemplateId(element.element);
}
}
interface ITreeListTemplateData<T> {
twistie: HTMLElement;
count: HTMLElement;
templateData: T;
}
function renderTwistie<T>(node: ITreeNode<T, any>, twistie: HTMLElement): void {
if (node.children.length === 0 && !node.collapsible) {
twistie.innerText = '';
} else {
twistie.innerText = node.collapsed ? '' : '';
}
}
class TreeRenderer<T, TFilterData, TTemplateData> implements IRenderer<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>> {
readonly templateId: string;
private renderedNodes = new Map<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>>();
private disposables: IDisposable[] = [];
constructor(
private renderer: IRenderer<T, TTemplateData>,
onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>
) {
this.templateId = renderer.templateId;
onDidChangeCollapseState(this.onDidChangeCollapseState, this, this.disposables);
}
renderTemplate(container: HTMLElement): ITreeListTemplateData<TTemplateData> {
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<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): 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<T, TFilterData>): void {
this.renderedNodes.delete(node);
}
disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
this.renderer.disposeTemplate(templateData.templateData);
}
private onDidChangeCollapseState(node: ITreeNode<T, TFilterData>): 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<T, TFilterData = void> extends IListOptions<T>, IIndexTreeModelOptions<T, TFilterData> { }
export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
private view: List<ITreeNode<T, TFilterData>>;
protected model: ITreeModel<T, TFilterData, TRef>;
protected disposables: IDisposable[] = [];
constructor(
container: HTMLElement,
delegate: IVirtualDelegate<T>,
renderers: IRenderer<T, any>[],
options?: ITreeOptions<T, TFilterData>
) {
const treeDelegate = new TreeDelegate(delegate);
const onDidChangeCollapseStateRelay = new Relay<ITreeNode<T, TFilterData>>();
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<ITreeNode<T, TFilterData>>, options: ITreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, TRef>;
// collapseAll(): void {
// this.model.setCollapsedAll(true);
// }
refilter(): void {
this.model.refilter();
}
private onMouseClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): 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
/*---------------------------------------------------------------------------------------------
* 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<T, TFilterData = void> extends AbstractTree<T, TFilterData, number[]> {
protected model: IndexTreeModel<T, TFilterData>;
splice(location: number[], deleteCount: number, toInsert: ISequence<ITreeElement<T>> = Iterator.empty()): Iterator<ITreeElement<T>> {
return this.model.splice(location, deleteCount, toInsert);
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: ITreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, number[]> {
return new IndexTreeModel(view, options);
}
}
\ No newline at end of file
......@@ -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<T> {
readonly element: T;
readonly children?: Iterator<ITreeElement<T>> | ITreeElement<T>[];
readonly collapsible?: boolean;
readonly collapsed?: boolean;
}
export interface ITreeNode<T, TFilterData = void> {
readonly parent: ITreeNode<T, TFilterData> | undefined;
readonly element: T;
readonly children: ITreeNode<T, TFilterData>[];
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<T, TFilterData> extends ITreeNode<T, TFilterData> {
readonly parent: IMutableTreeNode<T, TFilterData> | undefined;
......@@ -35,25 +19,10 @@ interface IMutableTreeNode<T, TFilterData> extends ITreeNode<T, TFilterData> {
visible: boolean;
}
export const enum Visibility {
Hidden,
Visible,
Recurse // TODO@joao come up with a better name
}
export interface IFilterResult<TFilterData> {
visibility: Visibility;
data: TFilterData;
}
function isFilterResult<T>(obj: any): obj is IFilterResult<T> {
function isFilterResult<T>(obj: any): obj is ITreeFilterResult<T> {
return typeof obj === 'object' && 'visibility' in obj && 'data' in obj;
}
export interface ITreeFilter<T, TFilterData = void> {
filter(element: T): boolean | Visibility | IFilterResult<TFilterData>;
}
function treeNodeToElement<T>(node: IMutableTreeNode<T, any>): ITreeElement<T> {
const { element, collapsed } = node;
const children = Iterator.map(Iterator.fromArray(node.children), treeNodeToElement);
......@@ -61,31 +30,17 @@ function treeNodeToElement<T>(node: IMutableTreeNode<T, any>): ITreeElement<T> {
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<T, TFilterData = void> {
filter?: ITreeFilter<T, TFilterData>;
}
export class TreeModel<T, TFilterData = void> {
// TODO@joao perf!
static getNodeLocation<T>(node: ITreeNode<T, any>): number[] {
const location = [];
while (node.parent) {
location.push(node.parent.children.indexOf(node));
node = node.parent;
}
export interface IIndexTreeModelOptions<T, TFilterData> extends ITreeOptions<T, TFilterData> { }
return location.reverse();
}
export class IndexTreeModel<T, TFilterData = void> implements ITreeModel<T, TFilterData, number[]> {
private root: IMutableTreeNode<T, TFilterData> = {
parent: undefined,
......@@ -104,7 +59,7 @@ export class TreeModel<T, TFilterData = void> {
private filter?: ITreeFilter<T, TFilterData>;
constructor(private list: ISpliceable<ITreeNode<T, TFilterData>>, options: ITreeModelOptions<T, TFilterData> = {}) {
constructor(private list: ISpliceable<ITreeNode<T, TFilterData>>, options: IIndexTreeModelOptions<T, TFilterData> = {}) {
this.filter = options.filter;
}
......@@ -167,28 +122,28 @@ export class TreeModel<T, TFilterData = void> {
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<T, TFilterData = void> {
}
private _filterNode(node: IMutableTreeNode<T, TFilterData>): 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<T, TFilterData = void> {
return this.findParentNode(rest, node.children[index], listIndex + 1, revealed);
}
// TODO@joao perf!
getNodeLocation(node: ITreeNode<T, TFilterData>): 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
/*---------------------------------------------------------------------------------------------
* 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<T, TFilterData = void> extends AbstractTree<T, TFilterData, ITreeNode<T, TFilterData>> {
protected model: ObjectTreeModel<T, TFilterData>;
setChildren(element: T | null, children?: ISequence<ITreeElement<T>>): Iterator<ITreeElement<T>> {
return this.model.setChildren(element, children);
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: ITreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, ITreeNode<T, TFilterData>> {
return new ObjectTreeModel(view, options);
}
}
\ No newline at end of file
......@@ -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<T extends NonNullable<any>, TFilterData = void> {
export interface IObjectTreeModelOptions<T, TFilterData> extends IIndexTreeModelOptions<T, TFilterData> { }
private model: TreeModel<T, TFilterData>;
export class ObjectTreeModel<T extends NonNullable<any>, TFilterData = void> implements ITreeModel<T, TFilterData, ITreeNode<T, TFilterData>> {
private model: IndexTreeModel<T, TFilterData>;
private nodes = new Map<T, ITreeNode<T, TFilterData>>();
readonly onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>;
get size(): number { return this.nodes.size; }
constructor(list: ISpliceable<ITreeNode<T, TFilterData>>, options: ITreeModelOptions<T, TFilterData> = {}) {
this.model = new TreeModel(list, options);
constructor(list: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeModelOptions<T, TFilterData> = {}) {
this.model = new IndexTreeModel(list, options);
this.onDidChangeCollapseState = this.model.onDidChangeCollapseState;
}
setChildren(element: T | null, children?: ISequence<ITreeElement<T>>): Iterator<ITreeElement<T>> {
const location = this.getLocation(element);
const location = this.getElementLocation(element);
const insertedElements = new Set<T>();
const onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
......@@ -42,23 +45,23 @@ export class TreeObjectModel<T extends NonNullable<any>, 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<T, TFilterData>): 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<T, TFilterData>, 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<T, TFilterData>): void {
const location = this.getElementLocation(node.element);
this.model.toggleCollapsed(location);
}
isCollapsed(element: T): boolean {
const location = this.getLocation(element);
isCollapsed(node: ITreeNode<T, TFilterData>): boolean {
const location = this.getElementLocation(node.element);
return this.model.isCollapsed(location);
}
......@@ -66,7 +69,7 @@ export class TreeObjectModel<T extends NonNullable<any>, 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<T extends NonNullable<any>, TFilterData = void> {
throw new Error(`Tree element not found: ${element}`);
}
return TreeModel.getNodeLocation(node);
return this.model.getNodeLocation(node);
}
getNodeLocation(node: ITreeNode<T, TFilterData>): ITreeNode<T, TFilterData> {
return node;
}
getParentNodeLocation(node: ITreeNode<T, TFilterData>): ITreeNode<T, TFilterData> | null {
return node.parent || null;
}
}
\ No newline at end of file
......@@ -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<T>(options?: IListOptions<T>): IListOptions<ITreeNode<T, any>> {
if (!options) {
return undefined;
}
let identityProvider: IIdentityProvider<ITreeNode<T, any>> | undefined = undefined;
let multipleSelectionController: IMultipleSelectionController<ITreeNode<T, any>> | 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<T> implements IVirtualDelegate<ITreeNode<T, any>> {
constructor(private delegate: IVirtualDelegate<T>) { }
getHeight(element: ITreeNode<T, any>): number {
return this.delegate.getHeight(element.element);
}
getTemplateId(element: ITreeNode<T, any>): string {
return this.delegate.getTemplateId(element.element);
}
export interface ITreeFilterResult<TFilterData> {
visibility: TreeVisibility;
data: TFilterData;
}
interface ITreeListTemplateData<T> {
twistie: HTMLElement;
count: HTMLElement;
templateData: T;
export interface ITreeFilter<T, TFilterData = void> {
filter(element: T): boolean | TreeVisibility | ITreeFilterResult<TFilterData>;
}
function renderTwistie<T>(node: ITreeNode<T, any>, twistie: HTMLElement): void {
if (node.children.length === 0 && !node.collapsible) {
twistie.innerText = '';
} else {
twistie.innerText = node.collapsed ? '' : '';
}
export interface ITreeOptions<T, TFilterData = void> {
filter?: ITreeFilter<T, TFilterData>;
}
class TreeRenderer<T, TFilterData, TTemplateData> implements IRenderer<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>> {
readonly templateId: string;
private renderedNodes = new Map<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>>();
private disposables: IDisposable[] = [];
constructor(
private renderer: IRenderer<T, TTemplateData>,
onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>
) {
this.templateId = renderer.templateId;
onDidChangeCollapseState(this.onDidChangeCollapseState, this, this.disposables);
}
renderTemplate(container: HTMLElement): ITreeListTemplateData<TTemplateData> {
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<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): 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<T, TFilterData>): void {
this.renderedNodes.delete(node);
}
disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
this.renderer.disposeTemplate(templateData.templateData);
}
private onDidChangeCollapseState(node: ITreeNode<T, TFilterData>): 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<T> {
readonly element: T;
readonly children?: Iterator<ITreeElement<T>> | ITreeElement<T>[];
readonly collapsible?: boolean;
readonly collapsed?: boolean;
}
function isInputElement(e: HTMLElement): boolean {
return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA';
export interface ITreeNode<T, TFilterData = void> {
readonly parent: ITreeNode<T, TFilterData> | undefined;
readonly element: T;
readonly children: ITreeNode<T, TFilterData>[];
readonly depth: number;
readonly collapsible: boolean;
readonly collapsed: boolean;
readonly revealedCount: number;
readonly filterData: TFilterData | undefined;
}
export interface ITreeOptions<T, TFilterData = void> extends IListOptions<T>, ITreeModelOptions<T, TFilterData> { }
export class Tree<T, TFilterData = void> implements IDisposable {
private view: List<ITreeNode<T, TFilterData>>;
private model: TreeModel<T, TFilterData>;
private disposables: IDisposable[] = [];
constructor(
container: HTMLElement,
delegate: IVirtualDelegate<T>,
renderers: IRenderer<T, any>[],
options?: ITreeOptions<T, TFilterData>
) {
const treeDelegate = new TreeDelegate(delegate);
const onDidChangeCollapseStateRelay = new Relay<ITreeNode<T, TFilterData>>();
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<T, TFilterData>(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<ITreeElement<T>> = Iterator.empty()): Iterator<ITreeElement<T>> {
return this.model.splice(location, deleteCount, toInsert);
}
collapseAll(): void {
this.model.setCollapsedAll(true);
}
refilter(): void {
this.model.refilter();
}
private onMouseClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): 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<T, TFilterData, TRef> {
onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>;
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<T, any>): TRef;
getParentNodeLocation(location: TRef): TRef | null;
}
\ No newline at end of file
......@@ -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<T>(arr: T[]): ISpliceable<T> {
return {
......@@ -20,18 +21,18 @@ function toArray<T>(list: ITreeNode<T>[]): T[] {
return list.map(i => i.element);
}
suite('TreeModel2', function () {
suite('IndexTreeModel', function () {
test('ctor', () => {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(toSpliceable(list));
assert(model);
assert.equal(list.length, 0);
});
test('insert', () => {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(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<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(toSpliceable(list));
model.splice([0], 0, Iterator.fromArray([
{
......@@ -90,7 +91,7 @@ suite('TreeModel2', function () {
test('deep insert collapsed', function () {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(toSpliceable(list));
model.splice([0], 0, Iterator.fromArray([
{
......@@ -118,7 +119,7 @@ suite('TreeModel2', function () {
test('delete', () => {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(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<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(toSpliceable(list));
model.splice([0], 0, Iterator.fromArray([
{
......@@ -177,7 +178,7 @@ suite('TreeModel2', function () {
test('deep delete', function () {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(toSpliceable(list));
model.splice([0], 0, Iterator.fromArray([
{
......@@ -205,7 +206,7 @@ suite('TreeModel2', function () {
test('hidden delete', function () {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(toSpliceable(list));
model.splice([0], 0, Iterator.fromArray([
{
......@@ -230,7 +231,7 @@ suite('TreeModel2', function () {
test('collapse', () => {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(toSpliceable(list));
model.splice([0], 0, Iterator.fromArray([
{
......@@ -261,7 +262,7 @@ suite('TreeModel2', function () {
test('expand', () => {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(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<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(toSpliceable(list));
model.splice([0], 0, Iterator.fromArray([
{
......@@ -335,12 +336,12 @@ suite('TreeModel2', function () {
test('simple filter', function () {
const list = [] as ITreeNode<number>[];
const filter = new class implements ITreeFilter<number> {
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<number>(toSpliceable(list), { filter });
const model = new IndexTreeModel<number>(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<number>[];
const filter = new class implements ITreeFilter<number> {
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<number>(toSpliceable(list), { filter });
const model = new IndexTreeModel<number>(toSpliceable(list), { filter });
model.splice([0], 0, Iterator.fromArray([
{
......@@ -392,12 +393,12 @@ suite('TreeModel2', function () {
const list = [] as ITreeNode<number>[];
let shouldFilter = false;
const filter = new class implements ITreeFilter<number> {
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<number>(toSpliceable(list), { filter });
const model = new IndexTreeModel<number>(toSpliceable(list), { filter });
model.splice([0], 0, Iterator.fromArray([
{
......@@ -431,12 +432,12 @@ suite('TreeModel2', function () {
const list = [] as ITreeNode<string>[];
let query = new RegExp('');
const filter = new class implements ITreeFilter<string> {
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<string>(toSpliceable(list), { filter });
const model = new IndexTreeModel<string>(toSpliceable(list), { filter });
model.splice([0], 0, Iterator.fromArray([
{
......@@ -477,12 +478,12 @@ suite('TreeModel2', function () {
const list = [] as ITreeNode<string>[];
let query = new RegExp('');
const filter = new class implements ITreeFilter<string> {
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<string>(toSpliceable(list), { filter });
const model = new IndexTreeModel<string>(toSpliceable(list), { filter });
model.splice([0], 0, Iterator.fromArray([
{
......@@ -523,12 +524,12 @@ suite('TreeModel2', function () {
const list = [] as ITreeNode<string>[];
let query = new RegExp('');
const filter = new class implements ITreeFilter<string> {
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<string>(toSpliceable(list), { filter });
const model = new IndexTreeModel<string>(toSpliceable(list), { filter });
model.splice([0], 0, Iterator.fromArray([
{
......@@ -576,7 +577,7 @@ suite('TreeModel2', function () {
test('simple', function () {
const list = [] as ITreeNode<number>[];
const model = new TreeModel<number>(toSpliceable(list));
const model = new IndexTreeModel<number>(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<number>[];
const filter = new class implements ITreeFilter<number> {
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<number>(toSpliceable(list), { filter });
const model = new IndexTreeModel<number>(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
......@@ -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<T>(arr: T[]): ISpliceable<T> {
......@@ -21,11 +21,11 @@ function toArray<T>(list: ITreeNode<T>[]): T[] {
return list.map(i => i.element);
}
suite('TreeObjectModel', function () {
suite('ObjectTreeModel', function () {
test('ctor', () => {
const list = [] as ITreeNode<number>[];
const model = new TreeObjectModel<number>(toSpliceable(list));
const model = new ObjectTreeModel<number>(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<number>[];
const model = new TreeObjectModel<number>(toSpliceable(list));
const model = new ObjectTreeModel<number>(toSpliceable(list));
model.setChildren(null, Iterator.fromArray([
{ element: 0 },
......@@ -60,7 +60,7 @@ suite('TreeObjectModel', function () {
test('nested', () => {
const list = [] as ITreeNode<number>[];
const model = new TreeObjectModel<number>(toSpliceable(list));
const model = new ObjectTreeModel<number>(toSpliceable(list));
model.setChildren(null, Iterator.fromArray([
{
......
......@@ -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');
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册