Merge from master
507
src/vs/base/browser/ui/tree/abstractTree.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/tree';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IListOptions, List, IMultipleSelectionController, IListStyles, IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { append, $, toggleClass } from 'vs/base/browser/dom';
|
||||
import { Event, Relay, chain, mapEvent } from 'vs/base/common/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
|
||||
function asListOptions<T, TFilterData>(options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let identityProvider: IIdentityProvider<ITreeNode<T, TFilterData>> | undefined = undefined;
|
||||
|
||||
if (options.identityProvider) {
|
||||
const ip = options.identityProvider;
|
||||
identityProvider = {
|
||||
getId(el) {
|
||||
return ip.getId(el.element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let multipleSelectionController: IMultipleSelectionController<ITreeNode<T, TFilterData>> | undefined = undefined;
|
||||
|
||||
if (options.multipleSelectionController) {
|
||||
const msc = options.multipleSelectionController;
|
||||
multipleSelectionController = {
|
||||
isSelectionSingleChangeEvent(e) {
|
||||
return msc.isSelectionSingleChangeEvent({ ...e, element: e.element } as any);
|
||||
},
|
||||
isSelectionRangeChangeEvent(e) {
|
||||
return msc.isSelectionRangeChangeEvent({ ...e, element: e.element } as any);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let accessibilityProvider: IAccessibilityProvider<ITreeNode<T, TFilterData>> | undefined = undefined;
|
||||
|
||||
if (options.accessibilityProvider) {
|
||||
const ap = options.accessibilityProvider;
|
||||
accessibilityProvider = {
|
||||
getAriaLabel(e) {
|
||||
return ap.getAriaLabel(e.element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
identityProvider,
|
||||
multipleSelectionController,
|
||||
accessibilityProvider
|
||||
};
|
||||
}
|
||||
|
||||
export class ComposedTreeDelegate<T, N extends { element: T }> implements IListVirtualDelegate<N> {
|
||||
|
||||
constructor(private delegate: IListVirtualDelegate<T>) { }
|
||||
|
||||
getHeight(element: N): number {
|
||||
return this.delegate.getHeight(element.element);
|
||||
}
|
||||
|
||||
getTemplateId(element: N): string {
|
||||
return this.delegate.getTemplateId(element.element);
|
||||
}
|
||||
|
||||
hasDynamicHeight(element: N): boolean {
|
||||
return !!this.delegate.hasDynamicHeight && this.delegate.hasDynamicHeight(element.element);
|
||||
}
|
||||
}
|
||||
|
||||
interface ITreeListTemplateData<T> {
|
||||
twistie: HTMLElement;
|
||||
templateData: T;
|
||||
}
|
||||
|
||||
class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>> {
|
||||
|
||||
readonly templateId: string;
|
||||
private renderedElements = new Map<T, ITreeNode<T, TFilterData>>();
|
||||
private renderedNodes = new Map<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>>();
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private renderer: ITreeRenderer<T, TFilterData, TTemplateData>,
|
||||
onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>
|
||||
) {
|
||||
this.templateId = renderer.templateId;
|
||||
|
||||
onDidChangeCollapseState(this.onDidChangeNodeTwistieState, this, this.disposables);
|
||||
|
||||
if (renderer.onDidChangeTwistieState) {
|
||||
renderer.onDidChangeTwistieState(this.onDidChangeTwistieState, this, this.disposables);
|
||||
}
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): ITreeListTemplateData<TTemplateData> {
|
||||
const el = append(container, $('.monaco-tl-row'));
|
||||
const twistie = append(el, $('.monaco-tl-twistie'));
|
||||
const contents = append(el, $('.monaco-tl-contents'));
|
||||
const templateData = this.renderer.renderTemplate(contents);
|
||||
|
||||
return { twistie, templateData };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
|
||||
this.renderedNodes.set(node, templateData);
|
||||
this.renderedElements.set(node.element, node);
|
||||
|
||||
templateData.twistie.style.width = `${10 + node.depth * 10}px`;
|
||||
this.renderTwistie(node, templateData.twistie);
|
||||
|
||||
this.renderer.renderElement(node, index, templateData.templateData);
|
||||
}
|
||||
|
||||
disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.disposeElement(node, index, templateData.templateData);
|
||||
this.renderedNodes.delete(node);
|
||||
this.renderedElements.set(node.element);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.disposeTemplate(templateData.templateData);
|
||||
}
|
||||
|
||||
private onDidChangeTwistieState(element: T): void {
|
||||
const node = this.renderedElements.get(element);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onDidChangeNodeTwistieState(node);
|
||||
}
|
||||
|
||||
private onDidChangeNodeTwistieState(node: ITreeNode<T, TFilterData>): void {
|
||||
const templateData = this.renderedNodes.get(node);
|
||||
|
||||
if (!templateData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderTwistie(node, templateData.twistie);
|
||||
}
|
||||
|
||||
private renderTwistie(node: ITreeNode<T, TFilterData>, twistieElement: HTMLElement) {
|
||||
if (this.renderer.renderTwistie) {
|
||||
this.renderer.renderTwistie(node.element, twistieElement);
|
||||
}
|
||||
|
||||
toggleClass(twistieElement, 'collapsible', node.collapsible);
|
||||
toggleClass(twistieElement, 'collapsed', node.collapsible && node.collapsed);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.renderedNodes.clear();
|
||||
this.renderedElements.clear();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
|
||||
function isInputElement(e: HTMLElement): boolean {
|
||||
return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA';
|
||||
}
|
||||
|
||||
function asTreeEvent<T>(event: IListEvent<ITreeNode<T, any>>): ITreeEvent<T> {
|
||||
return {
|
||||
elements: event.elements.map(node => node.element),
|
||||
browserEvent: event.browserEvent
|
||||
};
|
||||
}
|
||||
|
||||
function asTreeMouseEvent<T>(event: IListMouseEvent<ITreeNode<T, any>>): ITreeMouseEvent<T> {
|
||||
return {
|
||||
browserEvent: event.browserEvent,
|
||||
element: event.element ? event.element.element : null
|
||||
};
|
||||
}
|
||||
|
||||
function asTreeContextMenuEvent<T>(event: IListContextMenuEvent<ITreeNode<T, any>>): ITreeContextMenuEvent<T> {
|
||||
return {
|
||||
element: event.element ? event.element.element : null,
|
||||
browserEvent: event.browserEvent,
|
||||
anchor: event.anchor
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAbstractTreeOptions<T, TFilterData = void> extends IListOptions<T> {
|
||||
filter?: ITreeFilter<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[] = [];
|
||||
|
||||
get onDidChangeFocus(): Event<ITreeEvent<T>> { return mapEvent(this.view.onFocusChange, asTreeEvent); }
|
||||
get onDidChangeSelection(): Event<ITreeEvent<T>> { return mapEvent(this.view.onSelectionChange, asTreeEvent); }
|
||||
|
||||
get onMouseClick(): Event<ITreeMouseEvent<T>> { return mapEvent(this.view.onMouseClick, asTreeMouseEvent); }
|
||||
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return mapEvent(this.view.onMouseDblClick, asTreeMouseEvent); }
|
||||
get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return mapEvent(this.view.onContextMenu, asTreeContextMenuEvent); }
|
||||
get onDidFocus(): Event<void> { return this.view.onDidFocus; }
|
||||
get onDidBlur(): Event<void> { return this.view.onDidBlur; }
|
||||
|
||||
get onDidChangeCollapseState(): Event<ITreeNode<T, TFilterData>> { return this.model.onDidChangeCollapseState; }
|
||||
get onDidChangeRenderNodeCount(): Event<ITreeNode<T, TFilterData>> { return this.model.onDidChangeRenderNodeCount; }
|
||||
|
||||
get onDidDispose(): Event<void> { return this.view.onDidDispose; }
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
renderers: ITreeRenderer<any /* TODO@joao */, TFilterData, any>[],
|
||||
options: IAbstractTreeOptions<T, TFilterData> = {}
|
||||
) {
|
||||
const treeDelegate = new ComposedTreeDelegate<T, ITreeNode<T, TFilterData>>(delegate);
|
||||
|
||||
const onDidChangeCollapseStateRelay = new Relay<ITreeNode<T, TFilterData>>();
|
||||
const treeRenderers = renderers.map(r => new TreeRenderer<T, TFilterData, any>(r, onDidChangeCollapseStateRelay.event));
|
||||
this.disposables.push(...treeRenderers);
|
||||
|
||||
this.view = new List(container, treeDelegate, treeRenderers, asListOptions(options));
|
||||
|
||||
this.model = this.createModel(this.view, options);
|
||||
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
|
||||
|
||||
this.view.onMouseClick(this.reactOnMouseClick, this, this.disposables);
|
||||
|
||||
if (options.keyboardSupport !== false) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Widget
|
||||
|
||||
getHTMLElement(): HTMLElement {
|
||||
return this.view.getHTMLElement();
|
||||
}
|
||||
|
||||
get contentHeight(): number {
|
||||
return this.view.contentHeight;
|
||||
}
|
||||
|
||||
get onDidChangeContentHeight(): Event<number> {
|
||||
return this.view.onDidChangeContentHeight;
|
||||
}
|
||||
|
||||
get scrollTop(): number {
|
||||
return this.view.scrollTop;
|
||||
}
|
||||
|
||||
set scrollTop(scrollTop: number) {
|
||||
this.view.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
get scrollHeight(): number {
|
||||
return this.view.scrollHeight;
|
||||
}
|
||||
|
||||
get renderHeight(): number {
|
||||
return this.view.renderHeight;
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
this.view.domFocus();
|
||||
}
|
||||
|
||||
layout(height?: number): void {
|
||||
this.view.layout(height);
|
||||
}
|
||||
|
||||
layoutWidth(width: number): void {
|
||||
this.view.layoutWidth(width);
|
||||
}
|
||||
|
||||
style(styles: IListStyles): void {
|
||||
this.view.style(styles);
|
||||
}
|
||||
|
||||
// Tree navigation
|
||||
|
||||
getParentElement(location: TRef): T {
|
||||
return this.model.getParentElement(location);
|
||||
}
|
||||
|
||||
getFirstElementChild(location: TRef): T | undefined {
|
||||
return this.model.getFirstElementChild(location);
|
||||
}
|
||||
|
||||
getLastElementAncestor(location?: TRef): T | undefined {
|
||||
return this.model.getLastElementAncestor(location);
|
||||
}
|
||||
|
||||
// Tree
|
||||
|
||||
getNode(location?: TRef): ITreeNode<T, TFilterData> {
|
||||
return this.model.getNode(location);
|
||||
}
|
||||
|
||||
collapse(location: TRef): boolean {
|
||||
return this.model.setCollapsed(location, true);
|
||||
}
|
||||
|
||||
expand(location: TRef): boolean {
|
||||
return this.model.setCollapsed(location, false);
|
||||
}
|
||||
|
||||
toggleCollapsed(location: TRef): void {
|
||||
this.model.toggleCollapsed(location);
|
||||
}
|
||||
|
||||
collapseAll(): void {
|
||||
this.model.collapseAll();
|
||||
}
|
||||
|
||||
isCollapsible(location: TRef): boolean {
|
||||
return this.model.isCollapsible(location);
|
||||
}
|
||||
|
||||
isCollapsed(location: TRef): boolean {
|
||||
return this.model.isCollapsed(location);
|
||||
}
|
||||
|
||||
isExpanded(location: TRef): boolean {
|
||||
return !this.isCollapsed(location);
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
this.model.refilter();
|
||||
}
|
||||
|
||||
setSelection(elements: TRef[], browserEvent?: UIEvent): void {
|
||||
const indexes = elements.map(e => this.model.getListIndex(e));
|
||||
this.view.setSelection(indexes, browserEvent);
|
||||
}
|
||||
|
||||
getSelection(): T[] {
|
||||
const nodes = this.view.getSelectedElements();
|
||||
return nodes.map(n => n.element);
|
||||
}
|
||||
|
||||
setFocus(elements: TRef[], browserEvent?: UIEvent): void {
|
||||
const indexes = elements.map(e => this.model.getListIndex(e));
|
||||
this.view.setFocus(indexes, browserEvent);
|
||||
}
|
||||
|
||||
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
|
||||
this.view.focusNext(n, loop, browserEvent);
|
||||
}
|
||||
|
||||
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void {
|
||||
this.view.focusPrevious(n, loop, browserEvent);
|
||||
}
|
||||
|
||||
focusNextPage(browserEvent?: UIEvent): void {
|
||||
this.view.focusNextPage(browserEvent);
|
||||
}
|
||||
|
||||
focusPreviousPage(browserEvent?: UIEvent): void {
|
||||
this.view.focusPreviousPage(browserEvent);
|
||||
}
|
||||
|
||||
focusLast(browserEvent?: UIEvent): void {
|
||||
this.view.focusLast(browserEvent);
|
||||
}
|
||||
|
||||
focusFirst(browserEvent?: UIEvent): void {
|
||||
this.view.focusFirst(browserEvent);
|
||||
}
|
||||
|
||||
getFocus(): T[] {
|
||||
const nodes = this.view.getFocusedElements();
|
||||
return nodes.map(n => n.element);
|
||||
}
|
||||
|
||||
open(elements: TRef[]): void {
|
||||
const indexes = elements.map(e => this.model.getListIndex(e));
|
||||
this.view.open(indexes);
|
||||
}
|
||||
|
||||
reveal(location: TRef, relativeTop?: number): void {
|
||||
const index = this.model.getListIndex(location);
|
||||
this.view.reveal(index, relativeTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative position of an element rendered in the list.
|
||||
* Returns `null` if the element isn't *entirely* in the visible viewport.
|
||||
*/
|
||||
getRelativeTop(location: TRef): number | null {
|
||||
const index = this.model.getListIndex(location);
|
||||
return this.view.getRelativeTop(index);
|
||||
}
|
||||
|
||||
// List
|
||||
|
||||
get visibleNodeCount(): number {
|
||||
return this.view.length;
|
||||
}
|
||||
|
||||
private reactOnMouseClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
|
||||
const node = e.element;
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected abstract createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IAbstractTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, TRef>;
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
this.view.dispose();
|
||||
}
|
||||
}
|
||||
588
src/vs/base/browser/ui/tree/asyncDataTree.ts
Normal file
@@ -0,0 +1,588 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ComposedTreeDelegate, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
|
||||
import { ObjectTree, IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree';
|
||||
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event, mapEvent } from 'vs/base/common/event';
|
||||
import { timeout, always } from 'vs/base/common/async';
|
||||
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { toggleClass } from 'vs/base/browser/dom';
|
||||
import { Iterator } from 'vs/base/common/iterator';
|
||||
|
||||
export interface IDataSource<T extends NonNullable<any>> {
|
||||
hasChildren(element: T | null): boolean;
|
||||
getChildren(element: T | null): Thenable<T[]>;
|
||||
}
|
||||
|
||||
enum AsyncDataTreeNodeState {
|
||||
Uninitialized,
|
||||
Loaded,
|
||||
Loading,
|
||||
Slow
|
||||
}
|
||||
|
||||
interface IAsyncDataTreeNode<T extends NonNullable<any>> {
|
||||
element: T | null;
|
||||
readonly parent: IAsyncDataTreeNode<T> | null;
|
||||
readonly id?: string | null;
|
||||
readonly children?: IAsyncDataTreeNode<T>[];
|
||||
state: AsyncDataTreeNodeState;
|
||||
}
|
||||
|
||||
interface IDataTreeListTemplateData<T> {
|
||||
templateData: T;
|
||||
}
|
||||
|
||||
class AsyncDataTreeNodeWrapper<T, TFilterData> implements ITreeNode<T, TFilterData> {
|
||||
|
||||
get element(): T { return this.node.element!.element!; }
|
||||
get parent(): ITreeNode<T, TFilterData> | undefined { return this.node.parent && new AsyncDataTreeNodeWrapper(this.node.parent); }
|
||||
get children(): ITreeNode<T, TFilterData>[] { return this.node.children.map(node => new AsyncDataTreeNodeWrapper(node)); }
|
||||
get depth(): number { return this.node.depth; }
|
||||
get collapsible(): boolean { return this.node.collapsible; }
|
||||
get collapsed(): boolean { return this.node.collapsed; }
|
||||
get visible(): boolean { return this.node.visible; }
|
||||
get filterData(): TFilterData | undefined { return this.node.filterData; }
|
||||
|
||||
constructor(private node: ITreeNode<IAsyncDataTreeNode<T> | null, TFilterData>) { }
|
||||
}
|
||||
|
||||
class DataTreeRenderer<T, TFilterData, TTemplateData> implements ITreeRenderer<IAsyncDataTreeNode<T>, TFilterData, IDataTreeListTemplateData<TTemplateData>> {
|
||||
|
||||
readonly templateId: string;
|
||||
private renderedNodes = new Map<IAsyncDataTreeNode<T>, IDataTreeListTemplateData<TTemplateData>>();
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private renderer: ITreeRenderer<T, TFilterData, TTemplateData>,
|
||||
readonly onDidChangeTwistieState: Event<IAsyncDataTreeNode<T>>
|
||||
) {
|
||||
this.templateId = renderer.templateId;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): IDataTreeListTemplateData<TTemplateData> {
|
||||
const templateData = this.renderer.renderTemplate(container);
|
||||
return { templateData };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<IAsyncDataTreeNode<T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.renderElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData);
|
||||
}
|
||||
|
||||
renderTwistie(element: IAsyncDataTreeNode<T>, twistieElement: HTMLElement): boolean {
|
||||
toggleClass(twistieElement, 'loading', element.state === AsyncDataTreeNodeState.Slow);
|
||||
return false;
|
||||
}
|
||||
|
||||
disposeElement(node: ITreeNode<IAsyncDataTreeNode<T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.disposeElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: IDataTreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.disposeTemplate(templateData.templateData);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.renderedNodes.clear();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
|
||||
function asTreeEvent<T>(e: ITreeEvent<IAsyncDataTreeNode<T>>): ITreeEvent<T> {
|
||||
return {
|
||||
browserEvent: e.browserEvent,
|
||||
elements: e.elements.map(e => e.element!)
|
||||
};
|
||||
}
|
||||
|
||||
function asTreeMouseEvent<T>(e: ITreeMouseEvent<IAsyncDataTreeNode<T>>): ITreeMouseEvent<T> {
|
||||
return {
|
||||
browserEvent: e.browserEvent,
|
||||
element: e.element && e.element.element!
|
||||
};
|
||||
}
|
||||
|
||||
function asTreeContextMenuEvent<T>(e: ITreeContextMenuEvent<IAsyncDataTreeNode<T>>): ITreeContextMenuEvent<T> {
|
||||
return {
|
||||
browserEvent: e.browserEvent,
|
||||
element: e.element && e.element.element!,
|
||||
anchor: e.anchor
|
||||
};
|
||||
}
|
||||
|
||||
export enum ChildrenResolutionReason {
|
||||
Refresh,
|
||||
Expand
|
||||
}
|
||||
|
||||
export interface IChildrenResolutionEvent<T> {
|
||||
readonly element: T | null;
|
||||
readonly reason: ChildrenResolutionReason;
|
||||
}
|
||||
|
||||
function asObjectTreeOptions<T, TFilterData>(options?: IAsyncDataTreeOptions<T, TFilterData>): IObjectTreeOptions<IAsyncDataTreeNode<T>, TFilterData> | undefined {
|
||||
return options && {
|
||||
...options,
|
||||
identityProvider: options.identityProvider && {
|
||||
getId(el) {
|
||||
return options.identityProvider!.getId(el.element!);
|
||||
}
|
||||
},
|
||||
multipleSelectionController: options.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);
|
||||
}
|
||||
},
|
||||
accessibilityProvider: options.accessibilityProvider && {
|
||||
getAriaLabel(e) {
|
||||
return options.accessibilityProvider!.getAriaLabel(e.element!);
|
||||
}
|
||||
},
|
||||
filter: options.filter && {
|
||||
filter(element, parentVisibility) {
|
||||
return options.filter!.filter(element.element!, parentVisibility);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function asTreeElement<T>(node: IAsyncDataTreeNode<T>): ITreeElement<IAsyncDataTreeNode<T>> {
|
||||
return {
|
||||
element: node,
|
||||
children: Iterator.map(Iterator.fromArray(node.children!), asTreeElement)
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAsyncDataTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
|
||||
identityProvider?: IIdentityProvider<T>;
|
||||
}
|
||||
|
||||
export class AsyncDataTree<T extends NonNullable<any>, TFilterData = void> implements IDisposable {
|
||||
|
||||
private readonly tree: ObjectTree<IAsyncDataTreeNode<T>, TFilterData>;
|
||||
private readonly root: IAsyncDataTreeNode<T>;
|
||||
private readonly nodes = new Map<T | null, IAsyncDataTreeNode<T>>();
|
||||
private readonly refreshPromises = new Map<IAsyncDataTreeNode<T>, Thenable<void>>();
|
||||
private readonly identityProvider?: IIdentityProvider<T>;
|
||||
|
||||
private readonly _onDidChangeNodeState = new Emitter<IAsyncDataTreeNode<T>>();
|
||||
|
||||
protected readonly disposables: IDisposable[] = [];
|
||||
|
||||
get onDidChangeFocus(): Event<ITreeEvent<T>> { return mapEvent(this.tree.onDidChangeFocus, asTreeEvent); }
|
||||
get onDidChangeSelection(): Event<ITreeEvent<T>> { return mapEvent(this.tree.onDidChangeSelection, asTreeEvent); }
|
||||
get onDidChangeCollapseState(): Event<T> { return mapEvent(this.tree.onDidChangeCollapseState, e => e.element!.element!); }
|
||||
|
||||
private readonly _onDidResolveChildren = new Emitter<IChildrenResolutionEvent<T>>();
|
||||
readonly onDidResolveChildren: Event<IChildrenResolutionEvent<T>> = this._onDidResolveChildren.event;
|
||||
|
||||
get onMouseClick(): Event<ITreeMouseEvent<T>> { return mapEvent(this.tree.onMouseClick, asTreeMouseEvent); }
|
||||
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return mapEvent(this.tree.onMouseDblClick, asTreeMouseEvent); }
|
||||
get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return mapEvent(this.tree.onContextMenu, asTreeContextMenuEvent); }
|
||||
get onDidFocus(): Event<void> { return this.tree.onDidFocus; }
|
||||
get onDidBlur(): Event<void> { return this.tree.onDidBlur; }
|
||||
|
||||
get onDidDispose(): Event<void> { return this.tree.onDidDispose; }
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
renderers: ITreeRenderer<any /* TODO@joao */, TFilterData, any>[],
|
||||
private dataSource: IDataSource<T>,
|
||||
options?: IAsyncDataTreeOptions<T, TFilterData>
|
||||
) {
|
||||
this.identityProvider = options && options.identityProvider;
|
||||
|
||||
const objectTreeDelegate = new ComposedTreeDelegate<T | null, IAsyncDataTreeNode<T>>(delegate);
|
||||
const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this._onDidChangeNodeState.event));
|
||||
const objectTreeOptions = asObjectTreeOptions(options) || {};
|
||||
objectTreeOptions.collapseByDefault = true;
|
||||
|
||||
this.tree = new ObjectTree(container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
|
||||
|
||||
this.root = {
|
||||
element: null,
|
||||
parent: null,
|
||||
state: AsyncDataTreeNodeState.Uninitialized,
|
||||
};
|
||||
|
||||
if (this.identityProvider) {
|
||||
this.root = {
|
||||
...this.root,
|
||||
id: null,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
this.nodes.set(null, this.root);
|
||||
|
||||
this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables);
|
||||
}
|
||||
|
||||
// Widget
|
||||
|
||||
getHTMLElement(): HTMLElement {
|
||||
return this.tree.getHTMLElement();
|
||||
}
|
||||
|
||||
get contentHeight(): number {
|
||||
return this.tree.contentHeight;
|
||||
}
|
||||
|
||||
get onDidChangeContentHeight(): Event<number> {
|
||||
return this.tree.onDidChangeContentHeight;
|
||||
}
|
||||
|
||||
get scrollTop(): number {
|
||||
return this.tree.scrollTop;
|
||||
}
|
||||
|
||||
set scrollTop(scrollTop: number) {
|
||||
this.tree.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
get scrollHeight(): number {
|
||||
return this.tree.scrollHeight;
|
||||
}
|
||||
|
||||
get renderHeight(): number {
|
||||
return this.tree.renderHeight;
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
this.tree.domFocus();
|
||||
}
|
||||
|
||||
layout(height?: number): void {
|
||||
this.tree.layout(height);
|
||||
}
|
||||
|
||||
style(styles: IListStyles): void {
|
||||
this.tree.style(styles);
|
||||
}
|
||||
|
||||
// Data Tree
|
||||
|
||||
refresh(element: T | null, recursive = true): Thenable<void> {
|
||||
return this.refreshNode(this.getDataNode(element), recursive, ChildrenResolutionReason.Refresh);
|
||||
}
|
||||
|
||||
// Tree
|
||||
|
||||
getNode(element: T | null): ITreeNode<T | null, TFilterData> {
|
||||
const dataNode = this.getDataNode(element);
|
||||
const node = this.tree.getNode(dataNode === this.root ? null : dataNode);
|
||||
return new AsyncDataTreeNodeWrapper<T | null, TFilterData>(node);
|
||||
}
|
||||
|
||||
collapse(element: T): boolean {
|
||||
return this.tree.collapse(this.getDataNode(element));
|
||||
}
|
||||
|
||||
async expand(element: T): Promise<boolean> {
|
||||
const node = this.getDataNode(element);
|
||||
|
||||
if (!this.tree.isCollapsed(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.tree.expand(node);
|
||||
|
||||
if (node.state !== AsyncDataTreeNodeState.Loaded) {
|
||||
await this.refreshNode(node, false, ChildrenResolutionReason.Expand);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
toggleCollapsed(element: T): void {
|
||||
this.tree.toggleCollapsed(this.getDataNode(element));
|
||||
}
|
||||
|
||||
collapseAll(): void {
|
||||
this.tree.collapseAll();
|
||||
}
|
||||
|
||||
isCollapsible(element: T): boolean {
|
||||
return this.tree.isCollapsible(this.getDataNode(element));
|
||||
}
|
||||
|
||||
isCollapsed(element: T): boolean {
|
||||
return this.tree.isCollapsed(this.getDataNode(element));
|
||||
}
|
||||
|
||||
isExpanded(element: T): boolean {
|
||||
return this.tree.isExpanded(this.getDataNode(element));
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
this.tree.refilter();
|
||||
}
|
||||
|
||||
setSelection(elements: T[], browserEvent?: UIEvent): void {
|
||||
const nodes = elements.map(e => this.getDataNode(e));
|
||||
this.tree.setSelection(nodes, browserEvent);
|
||||
}
|
||||
|
||||
getSelection(): T[] {
|
||||
const nodes = this.tree.getSelection();
|
||||
return nodes.map(n => n!.element!);
|
||||
}
|
||||
|
||||
setFocus(elements: T[], browserEvent?: UIEvent): void {
|
||||
const nodes = elements.map(e => this.getDataNode(e));
|
||||
this.tree.setFocus(nodes, browserEvent);
|
||||
}
|
||||
|
||||
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
|
||||
this.tree.focusNext(n, loop, browserEvent);
|
||||
}
|
||||
|
||||
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void {
|
||||
this.tree.focusPrevious(n, loop, browserEvent);
|
||||
}
|
||||
|
||||
focusNextPage(browserEvent?: UIEvent): void {
|
||||
this.tree.focusNextPage(browserEvent);
|
||||
}
|
||||
|
||||
focusPreviousPage(browserEvent?: UIEvent): void {
|
||||
this.tree.focusPreviousPage(browserEvent);
|
||||
}
|
||||
|
||||
focusLast(browserEvent?: UIEvent): void {
|
||||
this.tree.focusLast(browserEvent);
|
||||
}
|
||||
|
||||
focusFirst(browserEvent?: UIEvent): void {
|
||||
this.tree.focusFirst(browserEvent);
|
||||
}
|
||||
|
||||
getFocus(): T[] {
|
||||
const nodes = this.tree.getFocus();
|
||||
return nodes.map(n => n!.element!);
|
||||
}
|
||||
|
||||
open(elements: T[]): void {
|
||||
const nodes = elements.map(e => this.getDataNode(e));
|
||||
this.tree.open(nodes);
|
||||
}
|
||||
|
||||
reveal(element: T, relativeTop?: number): void {
|
||||
this.tree.reveal(this.getDataNode(element), relativeTop);
|
||||
}
|
||||
|
||||
getRelativeTop(element: T): number | null {
|
||||
return this.tree.getRelativeTop(this.getDataNode(element));
|
||||
}
|
||||
|
||||
// Tree navigation
|
||||
|
||||
getParentElement(element: T): T | null {
|
||||
const node = this.tree.getParentElement(this.getDataNode(element));
|
||||
return node && node.element;
|
||||
}
|
||||
|
||||
getFirstElementChild(element: T | null = null): T | null | undefined {
|
||||
const dataNode = this.getDataNode(element);
|
||||
const node = this.tree.getFirstElementChild(dataNode === this.root ? null : dataNode);
|
||||
return node && node.element;
|
||||
}
|
||||
|
||||
getLastElementAncestor(element: T | null = null): T | null | undefined {
|
||||
const dataNode = this.getDataNode(element);
|
||||
const node = this.tree.getLastElementAncestor(dataNode === this.root ? null : dataNode);
|
||||
return node && node.element;
|
||||
}
|
||||
|
||||
// List
|
||||
|
||||
get visibleNodeCount(): number {
|
||||
return this.tree.visibleNodeCount;
|
||||
}
|
||||
|
||||
// Implementation
|
||||
|
||||
private getDataNode(element: T | null): IAsyncDataTreeNode<T> {
|
||||
const node: IAsyncDataTreeNode<T> = this.nodes.get(element);
|
||||
|
||||
if (typeof node === 'undefined') {
|
||||
throw new Error(`Data tree node not found: ${element}`);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private async refreshNode(node: IAsyncDataTreeNode<T>, recursive: boolean, reason: ChildrenResolutionReason): Promise<void> {
|
||||
await this._refreshNode(node, recursive, reason);
|
||||
|
||||
if (recursive && node.children) {
|
||||
await Promise.all(node.children.map(child => this.refreshNode(child, recursive, reason)));
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshNode(node: IAsyncDataTreeNode<T>, recursive: boolean, reason: ChildrenResolutionReason): Thenable<void> {
|
||||
let result = this.refreshPromises.get(node);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = this.doRefresh(node, recursive, reason);
|
||||
this.refreshPromises.set(node, result);
|
||||
return always(result, () => this.refreshPromises.delete(node));
|
||||
}
|
||||
|
||||
private doRefresh(node: IAsyncDataTreeNode<T>, recursive: boolean, reason: ChildrenResolutionReason): Thenable<void> {
|
||||
const hasChildren = !!this.dataSource.hasChildren(node.element);
|
||||
|
||||
if (!hasChildren) {
|
||||
this.setChildren(node, [], recursive);
|
||||
return Promise.resolve();
|
||||
} else if (node !== this.root && (!this.tree.isCollapsible(node) || this.tree.isCollapsed(node))) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
node.state = AsyncDataTreeNodeState.Loading;
|
||||
this._onDidChangeNodeState.fire(node);
|
||||
|
||||
const slowTimeout = timeout(800);
|
||||
|
||||
slowTimeout.then(() => {
|
||||
node.state = AsyncDataTreeNodeState.Slow;
|
||||
this._onDidChangeNodeState.fire(node);
|
||||
}, _ => null);
|
||||
|
||||
return this.dataSource.getChildren(node.element)
|
||||
.then(children => {
|
||||
slowTimeout.cancel();
|
||||
node.state = AsyncDataTreeNodeState.Loaded;
|
||||
this._onDidChangeNodeState.fire(node);
|
||||
|
||||
this.setChildren(node, children, recursive);
|
||||
this._onDidResolveChildren.fire({ element: node.element, reason });
|
||||
}, err => {
|
||||
slowTimeout.cancel();
|
||||
node.state = AsyncDataTreeNodeState.Uninitialized;
|
||||
this._onDidChangeNodeState.fire(node);
|
||||
|
||||
if (node !== this.root) {
|
||||
this.tree.collapse(node);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _onDidChangeCollapseState(treeNode: ITreeNode<IAsyncDataTreeNode<T>, any>): void {
|
||||
if (!treeNode.collapsed && treeNode.element.state === AsyncDataTreeNodeState.Uninitialized) {
|
||||
this.refreshNode(treeNode.element, false, ChildrenResolutionReason.Expand);
|
||||
}
|
||||
}
|
||||
|
||||
private setChildren(node: IAsyncDataTreeNode<T>, childrenElements: T[], recursive: boolean): void {
|
||||
const children = childrenElements.map<ITreeElement<IAsyncDataTreeNode<T>>>(element => {
|
||||
if (!this.identityProvider) {
|
||||
return {
|
||||
element: {
|
||||
element,
|
||||
parent: node,
|
||||
state: AsyncDataTreeNodeState.Uninitialized
|
||||
},
|
||||
collapsible: !!this.dataSource.hasChildren(element),
|
||||
collapsed: true
|
||||
};
|
||||
}
|
||||
|
||||
const nodeChildren = new Map<string, IAsyncDataTreeNode<T>>();
|
||||
|
||||
for (const child of node.children!) {
|
||||
nodeChildren.set(child.id!, child);
|
||||
}
|
||||
|
||||
const id = this.identityProvider.getId(element).toString();
|
||||
const asyncDataTreeNode = nodeChildren.get(id);
|
||||
|
||||
if (!asyncDataTreeNode) {
|
||||
return {
|
||||
element: {
|
||||
element,
|
||||
parent: node,
|
||||
id,
|
||||
children: [],
|
||||
state: AsyncDataTreeNodeState.Uninitialized
|
||||
},
|
||||
collapsible: !!this.dataSource.hasChildren(element),
|
||||
collapsed: true
|
||||
};
|
||||
}
|
||||
|
||||
asyncDataTreeNode.element = element;
|
||||
|
||||
const collapsible = !!this.dataSource.hasChildren(element);
|
||||
const collapsed = !collapsible || this.tree.isCollapsed(asyncDataTreeNode);
|
||||
|
||||
if (recursive) {
|
||||
asyncDataTreeNode.state = AsyncDataTreeNodeState.Uninitialized;
|
||||
|
||||
if (this.tree.isCollapsed(asyncDataTreeNode)) {
|
||||
asyncDataTreeNode.children!.length = 0;
|
||||
|
||||
return {
|
||||
element: asyncDataTreeNode,
|
||||
collapsible,
|
||||
collapsed
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let children: Iterator<ITreeElement<IAsyncDataTreeNode<T>>> | undefined = undefined;
|
||||
|
||||
if (collapsible) {
|
||||
children = Iterator.map(Iterator.fromArray(asyncDataTreeNode.children!), asTreeElement);
|
||||
}
|
||||
|
||||
return {
|
||||
element: asyncDataTreeNode,
|
||||
children,
|
||||
collapsible,
|
||||
collapsed
|
||||
};
|
||||
});
|
||||
|
||||
const insertedElements = new Set<T>();
|
||||
|
||||
const onDidCreateNode = (treeNode: ITreeNode<IAsyncDataTreeNode<T>, TFilterData>) => {
|
||||
if (treeNode.element.element) {
|
||||
insertedElements.add(treeNode.element.element);
|
||||
this.nodes.set(treeNode.element.element, treeNode.element);
|
||||
}
|
||||
};
|
||||
|
||||
const onDidDeleteNode = (treeNode: ITreeNode<IAsyncDataTreeNode<T>, TFilterData>) => {
|
||||
if (treeNode.element.element) {
|
||||
if (!insertedElements.has(treeNode.element.element)) {
|
||||
this.nodes.delete(treeNode.element.element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.tree.setChildren(node === this.root ? null : node, children, onDidCreateNode, onDidDeleteNode);
|
||||
|
||||
if (this.identityProvider) {
|
||||
node.children!.splice(0, node.children!.length, ...children.map(c => c.element));
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
39
src/vs/base/browser/ui/tree/indexTree.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/tree';
|
||||
import { Iterator, ISequence } from 'vs/base/common/iterator';
|
||||
import { AbstractTree, IAbstractTreeOptions } 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, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
|
||||
export interface IIndexTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
|
||||
collapseByDefault?: boolean; // defaults to false
|
||||
}
|
||||
|
||||
export class IndexTree<T, TFilterData = void> extends AbstractTree<T, TFilterData, number[]> {
|
||||
|
||||
protected model: IndexTreeModel<T, TFilterData>;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
renderers: ITreeRenderer<any /* TODO@joao */, TFilterData, any>[],
|
||||
private rootElement: T,
|
||||
options: IIndexTreeOptions<T, TFilterData> = {}
|
||||
) {
|
||||
super(container, delegate, renderers, options);
|
||||
}
|
||||
|
||||
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: IIndexTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, number[]> {
|
||||
return new IndexTreeModel(view, this.rootElement, options);
|
||||
}
|
||||
}
|
||||
467
src/vs/base/browser/ui/tree/indexTreeModel.ts
Normal file
@@ -0,0 +1,467 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { Iterator, ISequence } from 'vs/base/common/iterator';
|
||||
import { Emitter, Event, EventBufferer } from 'vs/base/common/event';
|
||||
import { tail2 } from 'vs/base/common/arrays';
|
||||
import { ITreeFilterDataResult, TreeVisibility, ITreeFilter, ITreeModel, ITreeNode, ITreeElement } from 'vs/base/browser/ui/tree/tree';
|
||||
|
||||
interface IMutableTreeNode<T, TFilterData> extends ITreeNode<T, TFilterData> {
|
||||
readonly parent: IMutableTreeNode<T, TFilterData> | undefined;
|
||||
readonly children: IMutableTreeNode<T, TFilterData>[];
|
||||
collapsible: boolean;
|
||||
collapsed: boolean;
|
||||
renderNodeCount: number;
|
||||
visible: boolean;
|
||||
filterData: TFilterData | undefined;
|
||||
}
|
||||
|
||||
function isFilterResult<T>(obj: any): obj is ITreeFilterDataResult<T> {
|
||||
return typeof obj === 'object' && 'visibility' in obj && 'data' in obj;
|
||||
}
|
||||
|
||||
function treeNodeToElement<T>(node: IMutableTreeNode<T, any>): ITreeElement<T> {
|
||||
const { element, collapsed } = node;
|
||||
const children = Iterator.map(Iterator.fromArray(node.children), treeNodeToElement);
|
||||
|
||||
return { element, children, collapsed };
|
||||
}
|
||||
|
||||
function getVisibleState(visibility: boolean | TreeVisibility): TreeVisibility {
|
||||
switch (visibility) {
|
||||
case true: return TreeVisibility.Visible;
|
||||
case false: return TreeVisibility.Hidden;
|
||||
default: return visibility;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IIndexTreeModelOptions<T, TFilterData> {
|
||||
collapseByDefault?: boolean; // defaults to false
|
||||
filter?: ITreeFilter<T, TFilterData>;
|
||||
}
|
||||
|
||||
export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = void> implements ITreeModel<T, TFilterData, number[]> {
|
||||
|
||||
private root: IMutableTreeNode<T, TFilterData>;
|
||||
private eventBufferer = new EventBufferer();
|
||||
|
||||
private _onDidChangeCollapseState = new Emitter<ITreeNode<T, TFilterData>>();
|
||||
readonly onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>> = this.eventBufferer.wrapEvent(this._onDidChangeCollapseState.event);
|
||||
|
||||
private _onDidChangeRenderNodeCount = new Emitter<ITreeNode<T, TFilterData>>();
|
||||
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>> = this.eventBufferer.wrapEvent(this._onDidChangeRenderNodeCount.event);
|
||||
|
||||
private collapseByDefault: boolean;
|
||||
private filter?: ITreeFilter<T, TFilterData>;
|
||||
|
||||
constructor(private list: ISpliceable<ITreeNode<T, TFilterData>>, rootElement: T, options: IIndexTreeModelOptions<T, TFilterData> = {}) {
|
||||
this.collapseByDefault = typeof options.collapseByDefault === 'undefined' ? false : options.collapseByDefault;
|
||||
this.filter = options.filter;
|
||||
|
||||
this.root = {
|
||||
parent: undefined,
|
||||
element: rootElement,
|
||||
children: [],
|
||||
depth: 0,
|
||||
collapsible: false,
|
||||
collapsed: false,
|
||||
renderNodeCount: 0,
|
||||
visible: true,
|
||||
filterData: undefined
|
||||
};
|
||||
}
|
||||
|
||||
splice(
|
||||
location: number[],
|
||||
deleteCount: number,
|
||||
toInsert?: ISequence<ITreeElement<T>>,
|
||||
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
|
||||
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
|
||||
): Iterator<ITreeElement<T>> {
|
||||
if (location.length === 0) {
|
||||
throw new Error('Invalid tree location');
|
||||
}
|
||||
|
||||
const { parentNode, listIndex, revealed } = this.getParentNodeWithListIndex(location);
|
||||
const treeListElementsToInsert: ITreeNode<T, TFilterData>[] = [];
|
||||
const nodesToInsertIterator = Iterator.map(Iterator.from(toInsert), el => this.createTreeNode(el, parentNode, parentNode.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, revealed, treeListElementsToInsert, onDidCreateNode));
|
||||
|
||||
const nodesToInsert: IMutableTreeNode<T, TFilterData>[] = [];
|
||||
let renderNodeCount = 0;
|
||||
|
||||
Iterator.forEach(nodesToInsertIterator, node => {
|
||||
nodesToInsert.push(node);
|
||||
renderNodeCount += node.renderNodeCount;
|
||||
});
|
||||
|
||||
const lastIndex = location[location.length - 1];
|
||||
const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert);
|
||||
|
||||
if (revealed) {
|
||||
const visibleDeleteCount = deletedNodes.reduce((r, node) => r + node.renderNodeCount, 0);
|
||||
|
||||
this._updateAncestorsRenderNodeCount(parentNode, renderNodeCount - visibleDeleteCount);
|
||||
this.list.splice(listIndex, visibleDeleteCount, treeListElementsToInsert);
|
||||
}
|
||||
|
||||
if (deletedNodes.length > 0 && onDidDeleteNode) {
|
||||
const visit = (node: ITreeNode<T, TFilterData>) => {
|
||||
onDidDeleteNode(node);
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
|
||||
deletedNodes.forEach(visit);
|
||||
}
|
||||
|
||||
return Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement);
|
||||
}
|
||||
|
||||
getListIndex(location: number[]): number {
|
||||
return this.getTreeNodeWithListIndex(location).listIndex;
|
||||
}
|
||||
|
||||
setCollapsed(location: number[], collapsed: boolean): boolean {
|
||||
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
|
||||
return this.eventBufferer.bufferEvents(() => this._setCollapsed(node, listIndex, revealed, collapsed));
|
||||
}
|
||||
|
||||
toggleCollapsed(location: number[]): void {
|
||||
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
|
||||
this.eventBufferer.bufferEvents(() => this._setCollapsed(node, listIndex, revealed));
|
||||
}
|
||||
|
||||
collapseAll(): void {
|
||||
const queue = [...this.root.children];
|
||||
let listIndex = 0;
|
||||
|
||||
this.eventBufferer.bufferEvents(() => {
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift()!;
|
||||
const revealed = listIndex < this.root.children.length;
|
||||
this._setCollapsed(node, listIndex, revealed, true);
|
||||
|
||||
queue.push(...node.children);
|
||||
listIndex++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isCollapsible(location: number[]): boolean {
|
||||
return this.getTreeNode(location).collapsible;
|
||||
}
|
||||
|
||||
isCollapsed(location: number[]): boolean {
|
||||
return this.getTreeNode(location).collapsed;
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
const previousRenderNodeCount = this.root.renderNodeCount;
|
||||
const toInsert = this.updateNodeAfterFilterChange(this.root);
|
||||
this.list.splice(0, previousRenderNodeCount, toInsert);
|
||||
}
|
||||
|
||||
private _setCollapsed(node: IMutableTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, collapsed?: boolean | undefined): boolean {
|
||||
if (!node.collapsible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof collapsed === 'undefined') {
|
||||
collapsed = !node.collapsed;
|
||||
}
|
||||
|
||||
if (node.collapsed === collapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
node.collapsed = collapsed;
|
||||
|
||||
if (revealed) {
|
||||
const previousRenderNodeCount = node.renderNodeCount;
|
||||
const toInsert = this.updateNodeAfterCollapseChange(node);
|
||||
|
||||
this.list.splice(listIndex + 1, previousRenderNodeCount - 1, toInsert.slice(1));
|
||||
this._onDidChangeCollapseState.fire(node);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private createTreeNode(
|
||||
treeElement: ITreeElement<T>,
|
||||
parent: IMutableTreeNode<T, TFilterData>,
|
||||
parentVisibility: TreeVisibility,
|
||||
revealed: boolean,
|
||||
treeListElements: ITreeNode<T, TFilterData>[],
|
||||
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void
|
||||
): IMutableTreeNode<T, TFilterData> {
|
||||
const node: IMutableTreeNode<T, TFilterData> = {
|
||||
parent,
|
||||
element: treeElement.element,
|
||||
children: [],
|
||||
depth: parent.depth + 1,
|
||||
collapsible: typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : (typeof treeElement.collapsed !== 'undefined'),
|
||||
collapsed: typeof treeElement.collapsed === 'undefined' ? this.collapseByDefault : treeElement.collapsed,
|
||||
renderNodeCount: 1,
|
||||
visible: true,
|
||||
filterData: undefined
|
||||
};
|
||||
|
||||
const visibility = this._filterNode(node, parentVisibility);
|
||||
|
||||
if (revealed) {
|
||||
treeListElements.push(node);
|
||||
}
|
||||
|
||||
const childElements = Iterator.from(treeElement.children);
|
||||
const childRevealed = revealed && visibility !== TreeVisibility.Hidden && !node.collapsed;
|
||||
const childNodes = Iterator.map(childElements, el => this.createTreeNode(el, node, visibility, childRevealed, treeListElements, onDidCreateNode));
|
||||
|
||||
let hasVisibleDescendants = false;
|
||||
let renderNodeCount = 1;
|
||||
|
||||
Iterator.forEach(childNodes, child => {
|
||||
node.children.push(child);
|
||||
hasVisibleDescendants = hasVisibleDescendants || child.visible;
|
||||
renderNodeCount += child.renderNodeCount;
|
||||
});
|
||||
|
||||
node.collapsible = node.collapsible || node.children.length > 0;
|
||||
node.visible = visibility === TreeVisibility.Recurse ? hasVisibleDescendants : (visibility === TreeVisibility.Visible);
|
||||
|
||||
if (!node.visible) {
|
||||
node.renderNodeCount = 0;
|
||||
|
||||
if (revealed) {
|
||||
treeListElements.pop();
|
||||
}
|
||||
} else if (!node.collapsed) {
|
||||
node.renderNodeCount = renderNodeCount;
|
||||
}
|
||||
|
||||
if (onDidCreateNode) {
|
||||
onDidCreateNode(node);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private updateNodeAfterCollapseChange(node: IMutableTreeNode<T, TFilterData>): ITreeNode<T, TFilterData>[] {
|
||||
const previousRenderNodeCount = node.renderNodeCount;
|
||||
const result: ITreeNode<T, TFilterData>[] = [];
|
||||
|
||||
this._updateNodeAfterCollapseChange(node, result);
|
||||
this._updateAncestorsRenderNodeCount(node.parent, result.length - previousRenderNodeCount);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _updateNodeAfterCollapseChange(node: IMutableTreeNode<T, TFilterData>, result: ITreeNode<T, TFilterData>[]): number {
|
||||
if (node.visible === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
result.push(node);
|
||||
node.renderNodeCount = 1;
|
||||
|
||||
if (!node.collapsed) {
|
||||
for (const child of node.children) {
|
||||
node.renderNodeCount += this._updateNodeAfterCollapseChange(child, result);
|
||||
}
|
||||
}
|
||||
|
||||
this._onDidChangeRenderNodeCount.fire(node);
|
||||
return node.renderNodeCount;
|
||||
}
|
||||
|
||||
private updateNodeAfterFilterChange(node: IMutableTreeNode<T, TFilterData>): ITreeNode<T, TFilterData>[] {
|
||||
const previousRenderNodeCount = node.renderNodeCount;
|
||||
const result: ITreeNode<T, TFilterData>[] = [];
|
||||
|
||||
this._updateNodeAfterFilterChange(node, node.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, result);
|
||||
this._updateAncestorsRenderNodeCount(node.parent, result.length - previousRenderNodeCount);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _updateNodeAfterFilterChange(node: IMutableTreeNode<T, TFilterData>, parentVisibility: TreeVisibility, result: ITreeNode<T, TFilterData>[], revealed = true): boolean {
|
||||
let visibility: TreeVisibility;
|
||||
|
||||
if (node !== this.root) {
|
||||
visibility = this._filterNode(node, parentVisibility);
|
||||
|
||||
if (visibility === TreeVisibility.Hidden) {
|
||||
node.visible = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (revealed) {
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const resultStartLength = result.length;
|
||||
node.renderNodeCount = node === this.root ? 0 : 1;
|
||||
|
||||
let hasVisibleDescendants = false;
|
||||
if (!node.collapsed || visibility! !== TreeVisibility.Hidden) {
|
||||
for (const child of node.children) {
|
||||
hasVisibleDescendants = this._updateNodeAfterFilterChange(child, visibility!, result, revealed && !node.collapsed) || hasVisibleDescendants;
|
||||
}
|
||||
}
|
||||
|
||||
if (node !== this.root) {
|
||||
node.visible = visibility! === TreeVisibility.Recurse ? hasVisibleDescendants : (visibility! === TreeVisibility.Visible);
|
||||
}
|
||||
|
||||
if (!node.visible) {
|
||||
node.renderNodeCount = 0;
|
||||
|
||||
if (revealed) {
|
||||
result.pop();
|
||||
}
|
||||
} else if (!node.collapsed) {
|
||||
node.renderNodeCount += result.length - resultStartLength;
|
||||
}
|
||||
|
||||
this._onDidChangeRenderNodeCount.fire(node);
|
||||
return node.visible;
|
||||
}
|
||||
|
||||
private _updateAncestorsRenderNodeCount(node: IMutableTreeNode<T, TFilterData> | undefined, diff: number): void {
|
||||
if (diff === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (node) {
|
||||
node.renderNodeCount += diff;
|
||||
this._onDidChangeRenderNodeCount.fire(node);
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
|
||||
private _filterNode(node: IMutableTreeNode<T, TFilterData>, parentVisibility: TreeVisibility): TreeVisibility {
|
||||
const result = this.filter ? this.filter.filter(node.element, parentVisibility) : TreeVisibility.Visible;
|
||||
|
||||
if (typeof result === 'boolean') {
|
||||
node.filterData = undefined;
|
||||
return result ? TreeVisibility.Visible : TreeVisibility.Hidden;
|
||||
} else if (isFilterResult<TFilterData>(result)) {
|
||||
node.filterData = result.data;
|
||||
return getVisibleState(result.visibility);
|
||||
} else {
|
||||
node.filterData = undefined;
|
||||
return getVisibleState(result);
|
||||
}
|
||||
}
|
||||
|
||||
// cheap
|
||||
private getTreeNode(location: number[], node: IMutableTreeNode<T, TFilterData> = this.root): IMutableTreeNode<T, TFilterData> {
|
||||
if (!location || location.length === 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const [index, ...rest] = location;
|
||||
|
||||
if (index < 0 || index > node.children.length) {
|
||||
throw new Error('Invalid tree location');
|
||||
}
|
||||
|
||||
return this.getTreeNode(rest, node.children[index]);
|
||||
}
|
||||
|
||||
// expensive
|
||||
private getTreeNodeWithListIndex(location: number[]): { node: IMutableTreeNode<T, TFilterData>, listIndex: number, revealed: boolean } {
|
||||
const { parentNode, listIndex, revealed } = this.getParentNodeWithListIndex(location);
|
||||
const index = location[location.length - 1];
|
||||
|
||||
if (index < 0 || index > parentNode.children.length) {
|
||||
throw new Error('Invalid tree location');
|
||||
}
|
||||
|
||||
const node = parentNode.children[index];
|
||||
|
||||
return { node, listIndex, revealed };
|
||||
}
|
||||
|
||||
private getParentNodeWithListIndex(location: number[], node: IMutableTreeNode<T, TFilterData> = this.root, listIndex: number = 0, revealed = true): { parentNode: IMutableTreeNode<T, TFilterData>; listIndex: number; revealed: boolean; } {
|
||||
const [index, ...rest] = location;
|
||||
|
||||
if (index < 0 || index > node.children.length) {
|
||||
throw new Error('Invalid tree location');
|
||||
}
|
||||
|
||||
// TODO@joao perf!
|
||||
for (let i = 0; i < index; i++) {
|
||||
listIndex += node.children[i].renderNodeCount;
|
||||
}
|
||||
|
||||
revealed = revealed && !node.collapsed;
|
||||
|
||||
if (rest.length === 0) {
|
||||
return { parentNode: node, listIndex, revealed };
|
||||
}
|
||||
|
||||
return this.getParentNodeWithListIndex(rest, node.children[index], listIndex + 1, revealed);
|
||||
}
|
||||
|
||||
getNode(location: number[] = []): ITreeNode<T, TFilterData> {
|
||||
return this.getTreeNode(location);
|
||||
}
|
||||
|
||||
// TODO@joao perf!
|
||||
getNodeLocation(node: ITreeNode<T, TFilterData>): number[] {
|
||||
const location: number[] = [];
|
||||
|
||||
while (node.parent) {
|
||||
location.push(node.parent.children.indexOf(node));
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return location.reverse();
|
||||
}
|
||||
|
||||
getParentNodeLocation(location: number[]): number[] {
|
||||
if (location.length <= 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tail2(location)[0];
|
||||
}
|
||||
|
||||
getParentElement(location: number[]): T {
|
||||
const parentLocation = this.getParentNodeLocation(location);
|
||||
const node = this.getTreeNode(parentLocation);
|
||||
return node.element;
|
||||
}
|
||||
|
||||
getFirstElementChild(location: number[]): T | undefined {
|
||||
const node = this.getTreeNode(location);
|
||||
|
||||
if (node.children.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return node.children[0].element;
|
||||
}
|
||||
|
||||
getLastElementAncestor(location: number[] = []): T | undefined {
|
||||
const node = this.getTreeNode(location);
|
||||
|
||||
if (node.children.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._getLastElementAncestor(node);
|
||||
}
|
||||
|
||||
private _getLastElementAncestor(node: ITreeNode<T, TFilterData>): T {
|
||||
if (node.children.length === 0) {
|
||||
return node.element;
|
||||
}
|
||||
|
||||
return this._getLastElementAncestor(node.children[node.children.length - 1]);
|
||||
}
|
||||
}
|
||||
1
src/vs/base/browser/ui/tree/media/collapsed-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z"/></svg>
|
||||
|
After Width: | Height: | Size: 139 B |
1
src/vs/base/browser/ui/tree/media/collapsed-hc.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>
|
||||
|
After Width: | Height: | Size: 148 B |
1
src/vs/base/browser/ui/tree/media/collapsed.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#646465" d="M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z"/></svg>
|
||||
|
After Width: | Height: | Size: 139 B |
1
src/vs/base/browser/ui/tree/media/expanded-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M11 10H5.344L11 4.414V10z"/></svg>
|
||||
|
After Width: | Height: | Size: 118 B |
1
src/vs/base/browser/ui/tree/media/expanded-hc.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
src/vs/base/browser/ui/tree/media/expanded.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#646465" d="M11 10H5.344L11 4.414V10z"/></svg>
|
||||
|
After Width: | Height: | Size: 118 B |
31
src/vs/base/browser/ui/tree/media/loading-dark.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' standalone='no' ?>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||
<style>
|
||||
circle {
|
||||
animation: ball 0.6s linear infinite;
|
||||
}
|
||||
|
||||
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||
|
||||
@keyframes ball {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
<g style="fill:grey;">
|
||||
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
31
src/vs/base/browser/ui/tree/media/loading-hc.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' standalone='no' ?>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||
<style>
|
||||
circle {
|
||||
animation: ball 0.6s linear infinite;
|
||||
}
|
||||
|
||||
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||
|
||||
@keyframes ball {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
<g style="fill:white;">
|
||||
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
31
src/vs/base/browser/ui/tree/media/loading.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version='1.0' standalone='no' ?>
|
||||
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
|
||||
<style>
|
||||
circle {
|
||||
animation: ball 0.6s linear infinite;
|
||||
}
|
||||
|
||||
circle:nth-child(2) { animation-delay: 0.075s; }
|
||||
circle:nth-child(3) { animation-delay: 0.15s; }
|
||||
circle:nth-child(4) { animation-delay: 0.225s; }
|
||||
circle:nth-child(5) { animation-delay: 0.3s; }
|
||||
circle:nth-child(6) { animation-delay: 0.375s; }
|
||||
circle:nth-child(7) { animation-delay: 0.45s; }
|
||||
circle:nth-child(8) { animation-delay: 0.525s; }
|
||||
|
||||
@keyframes ball {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0.3; }
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
|
||||
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
|
||||
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -3,14 +3,11 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-tl-row {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
.monaco-panel-view .panel > .panel-header h3.title {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
-webkit-margin-before: 0;
|
||||
-webkit-margin-after: 0;
|
||||
}
|
||||
|
||||
.monaco-tl-row > .tl-twistie {
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
67
src/vs/base/browser/ui/tree/media/tree.css
Normal file
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-tl-row {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-tl-twistie,
|
||||
.monaco-tl-contents {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-tl-twistie {
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.monaco-tl-contents {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-tl-twistie.collapsible {
|
||||
background-size: 16px;
|
||||
background-position: 100% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("expanded.svg");
|
||||
}
|
||||
|
||||
.monaco-tl-twistie.collapsible.collapsed:not(.loading) {
|
||||
display: inline-block;
|
||||
background-image: url("collapsed.svg");
|
||||
}
|
||||
|
||||
.vs-dark .monaco-tl-twistie.collapsible:not(.loading) {
|
||||
background-image: url("expanded-dark.svg");
|
||||
}
|
||||
|
||||
.vs-dark .monaco-tl-twistie.collapsible.collapsed:not(.loading) {
|
||||
background-image: url("collapsed-dark.svg");
|
||||
}
|
||||
|
||||
.hc-black .monaco-tl-twistie.collapsible:not(.loading) {
|
||||
background-image: url("expanded-hc.svg");
|
||||
}
|
||||
|
||||
.hc-black .monaco-tl-twistie.collapsible.collapsed:not(.loading) {
|
||||
background-image: url("collapsed-hc.svg");
|
||||
}
|
||||
|
||||
.monaco-tl-twistie.loading {
|
||||
background-image: url("loading.svg");
|
||||
}
|
||||
|
||||
.vs-dark .monaco-tl-twistie.loading {
|
||||
background-image: url("loading-dark.svg");
|
||||
}
|
||||
|
||||
.hc-black .monaco-tl-twistie.loading {
|
||||
background-image: url("loading-hc.svg");
|
||||
}
|
||||
42
src/vs/base/browser/ui/tree/objectTree.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Iterator, ISequence } from 'vs/base/common/iterator';
|
||||
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
|
||||
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
|
||||
export interface IObjectTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
|
||||
collapseByDefault?: boolean; // defaults to false
|
||||
}
|
||||
|
||||
export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
|
||||
|
||||
protected model: ObjectTreeModel<T, TFilterData>;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
renderers: ITreeRenderer<any /* TODO@joao */, TFilterData, any>[],
|
||||
options: IObjectTreeOptions<T, TFilterData> = {}
|
||||
) {
|
||||
super(container, delegate, renderers, options);
|
||||
}
|
||||
|
||||
setChildren(
|
||||
element: T | null,
|
||||
children?: ISequence<ITreeElement<T>>,
|
||||
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
|
||||
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
|
||||
): Iterator<ITreeElement<T | null>> {
|
||||
return this.model.setChildren(element, children, onDidCreateNode, onDidDeleteNode);
|
||||
}
|
||||
|
||||
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
return new ObjectTreeModel(view, options);
|
||||
}
|
||||
}
|
||||
169
src/vs/base/browser/ui/tree/objectTreeModel.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
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';
|
||||
|
||||
export interface IObjectTreeModelOptions<T, TFilterData> extends IIndexTreeModelOptions<T, TFilterData> { }
|
||||
|
||||
export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements ITreeModel<T | null, TFilterData, T | null> {
|
||||
|
||||
private model: IndexTreeModel<T | null, TFilterData>;
|
||||
private nodes = new Map<T | null, ITreeNode<T, TFilterData>>();
|
||||
|
||||
readonly onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>;
|
||||
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
|
||||
|
||||
get size(): number { return this.nodes.size; }
|
||||
|
||||
constructor(list: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeModelOptions<T, TFilterData> = {}) {
|
||||
this.model = new IndexTreeModel(list, null, options);
|
||||
this.onDidChangeCollapseState = this.model.onDidChangeCollapseState as Event<ITreeNode<T, TFilterData>>;
|
||||
this.onDidChangeRenderNodeCount = this.model.onDidChangeRenderNodeCount as Event<ITreeNode<T, TFilterData>>;
|
||||
}
|
||||
|
||||
setChildren(
|
||||
element: T | null,
|
||||
children: ISequence<ITreeElement<T>> | undefined,
|
||||
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
|
||||
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
|
||||
): Iterator<ITreeElement<T | null>> {
|
||||
const location = this.getElementLocation(element);
|
||||
const insertedElements = new Set<T | null>();
|
||||
|
||||
const _onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
insertedElements.add(node.element);
|
||||
this.nodes.set(node.element, node);
|
||||
|
||||
if (onDidCreateNode) {
|
||||
onDidCreateNode(node);
|
||||
}
|
||||
};
|
||||
|
||||
const _onDidDeleteNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
if (!insertedElements.has(node.element)) {
|
||||
this.nodes.delete(node.element);
|
||||
}
|
||||
|
||||
if (onDidDeleteNode) {
|
||||
onDidDeleteNode(node);
|
||||
}
|
||||
};
|
||||
|
||||
return this.model.splice(
|
||||
[...location, 0],
|
||||
Number.MAX_VALUE,
|
||||
this.preserveCollapseState(children),
|
||||
_onDidCreateNode,
|
||||
_onDidDeleteNode
|
||||
);
|
||||
}
|
||||
|
||||
private preserveCollapseState(elements: ISequence<ITreeElement<T | null>> | undefined): ISequence<ITreeElement<T | null>> {
|
||||
const iterator = elements ? getSequenceIterator(elements) : Iterator.empty<ITreeElement<T>>();
|
||||
|
||||
return Iterator.map(iterator, treeElement => {
|
||||
const node = this.nodes.get(treeElement.element);
|
||||
|
||||
if (!node) {
|
||||
return treeElement;
|
||||
}
|
||||
|
||||
const collapsible = typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : node.collapsible;
|
||||
const collapsed = typeof treeElement.collapsed !== 'undefined' ? treeElement.collapsed : (collapsible && node.collapsed);
|
||||
|
||||
return {
|
||||
...treeElement,
|
||||
collapsible,
|
||||
collapsed,
|
||||
children: this.preserveCollapseState(treeElement.children)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getParentElement(ref: T | null = null): T | null {
|
||||
const location = this.getElementLocation(ref);
|
||||
return this.model.getParentElement(location);
|
||||
}
|
||||
|
||||
getFirstElementChild(ref: T | null = null): T | null | undefined {
|
||||
const location = this.getElementLocation(ref);
|
||||
return this.model.getFirstElementChild(location);
|
||||
}
|
||||
|
||||
getLastElementAncestor(ref: T | null = null): T | null | undefined {
|
||||
const location = this.getElementLocation(ref);
|
||||
return this.model.getLastElementAncestor(location);
|
||||
}
|
||||
|
||||
getListIndex(element: T): number {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.getListIndex(location);
|
||||
}
|
||||
|
||||
setCollapsed(element: T, collapsed: boolean): boolean {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.setCollapsed(location, collapsed);
|
||||
}
|
||||
|
||||
toggleCollapsed(element: T): void {
|
||||
const location = this.getElementLocation(element);
|
||||
this.model.toggleCollapsed(location);
|
||||
}
|
||||
|
||||
collapseAll(): void {
|
||||
this.model.collapseAll();
|
||||
}
|
||||
|
||||
isCollapsible(element: T): boolean {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.isCollapsible(location);
|
||||
}
|
||||
|
||||
isCollapsed(element: T): boolean {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.isCollapsed(location);
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
this.model.refilter();
|
||||
}
|
||||
|
||||
getNode(element: T | null = null): ITreeNode<T | null, TFilterData> {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.getNode(location);
|
||||
}
|
||||
|
||||
getNodeLocation(node: ITreeNode<T, TFilterData>): T {
|
||||
return node.element;
|
||||
}
|
||||
|
||||
getParentNodeLocation(element: T): T | null {
|
||||
const node = this.nodes.get(element);
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Tree element not found: ${element}`);
|
||||
}
|
||||
|
||||
return node.parent!.element;
|
||||
}
|
||||
|
||||
private getElementLocation(element: T | null): number[] {
|
||||
if (element === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const node = this.nodes.get(element);
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Tree element not found: ${element}`);
|
||||
}
|
||||
|
||||
return this.model.getNodeLocation(node);
|
||||
}
|
||||
}
|
||||
@@ -3,250 +3,127 @@
|
||||
* Licensed under the Source EULA. 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, getNodeLocation } 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';
|
||||
import { IListRenderer } from 'vs/base/browser/ui/list/list';
|
||||
|
||||
function toTreeListOptions<T>(options?: IListOptions<T>): IListOptions<ITreeNode<T>> {
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
export const enum TreeVisibility {
|
||||
|
||||
let identityProvider: IIdentityProvider<ITreeNode<T>> | undefined = undefined;
|
||||
let multipleSelectionController: IMultipleSelectionController<ITreeNode<T>> | undefined = undefined;
|
||||
/**
|
||||
* The tree node should be hidden.
|
||||
*/
|
||||
Hidden,
|
||||
|
||||
if (options.identityProvider) {
|
||||
identityProvider = el => options.identityProvider(el.element);
|
||||
}
|
||||
/**
|
||||
* The tree node should be visible.
|
||||
*/
|
||||
Visible,
|
||||
|
||||
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
|
||||
};
|
||||
/**
|
||||
* The tree node should be visible if any of its descendants is visible.
|
||||
*/
|
||||
Recurse
|
||||
}
|
||||
|
||||
class TreeDelegate<T> implements IVirtualDelegate<ITreeNode<T>> {
|
||||
/**
|
||||
* A composed filter result containing the visibility result as well as
|
||||
* metadata.
|
||||
*/
|
||||
export interface ITreeFilterDataResult<TFilterData> {
|
||||
|
||||
constructor(private delegate: IVirtualDelegate<T>) { }
|
||||
/**
|
||||
* Whether the node should be visibile.
|
||||
*/
|
||||
visibility: boolean | TreeVisibility;
|
||||
|
||||
getHeight(element: ITreeNode<T>): number {
|
||||
return this.delegate.getHeight(element.element);
|
||||
}
|
||||
|
||||
getTemplateId(element: ITreeNode<T>): string {
|
||||
return this.delegate.getTemplateId(element.element);
|
||||
}
|
||||
/**
|
||||
* Metadata about the element's visibility which gets forwarded to the
|
||||
* renderer once the element gets rendered.
|
||||
*/
|
||||
data: TFilterData;
|
||||
}
|
||||
|
||||
interface ITreeListTemplateData<T> {
|
||||
twistie: HTMLElement;
|
||||
templateData: T;
|
||||
/**
|
||||
* The result of a filter call can be a boolean value indicating whether
|
||||
* the element should be visible or not, a value of type `TreeVisibility` or
|
||||
* an object composed of the visibility result as well as additional metadata
|
||||
* which gets forwarded to the renderer once the element gets rendered.
|
||||
*/
|
||||
export type TreeFilterResult<TFilterData> = boolean | TreeVisibility | ITreeFilterDataResult<TFilterData>;
|
||||
|
||||
/**
|
||||
* A tree filter is responsible for controlling the visibility of
|
||||
* elements in a tree.
|
||||
*/
|
||||
export interface ITreeFilter<T, TFilterData = void> {
|
||||
|
||||
/**
|
||||
* Returns whether this elements should be visible and, if affirmative,
|
||||
* additional metadata which gets forwarded to the renderer once the element
|
||||
* gets rendered.
|
||||
*
|
||||
* @param element The tree element.
|
||||
*/
|
||||
filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult<TFilterData>;
|
||||
}
|
||||
|
||||
function renderTwistie<T>(node: ITreeNode<T>, twistie: HTMLElement): void {
|
||||
if (node.children.length === 0 && !node.collapsible) {
|
||||
twistie.innerText = '';
|
||||
} else {
|
||||
twistie.innerText = node.collapsed ? '▹' : '◢';
|
||||
}
|
||||
export interface ITreeElement<T> {
|
||||
readonly element: T;
|
||||
readonly children?: Iterator<ITreeElement<T>> | ITreeElement<T>[];
|
||||
readonly collapsible?: boolean;
|
||||
readonly collapsed?: boolean;
|
||||
}
|
||||
|
||||
class TreeRenderer<T, TTemplateData> implements IRenderer<ITreeNode<T>, ITreeListTemplateData<TTemplateData>> {
|
||||
|
||||
readonly templateId: string;
|
||||
private renderedNodes = new Map<ITreeNode<T>, ITreeListTemplateData<TTemplateData>>();
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private renderer: IRenderer<T, TTemplateData>,
|
||||
onDidChangeCollapseState: Event<ITreeNode<T>>
|
||||
) {
|
||||
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 templateData = this.renderer.renderTemplate(contents);
|
||||
|
||||
return { twistie, templateData };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<T>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
|
||||
this.renderedNodes.set(node, templateData);
|
||||
|
||||
templateData.twistie.style.width = `${10 + node.depth * 10}px`;
|
||||
renderTwistie(node, templateData.twistie);
|
||||
|
||||
this.renderer.renderElement(node.element, index, templateData.templateData);
|
||||
}
|
||||
|
||||
disposeElement(node: ITreeNode<T>): void {
|
||||
this.renderedNodes.delete(node);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.disposeTemplate(templateData.templateData);
|
||||
}
|
||||
|
||||
private onDidChangeCollapseState(node: ITreeNode<T>): void {
|
||||
const templateData = this.renderedNodes.get(node);
|
||||
|
||||
if (!templateData) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderTwistie(node, templateData.twistie);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.renderedNodes.clear();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
export interface ITreeNode<T, TFilterData = void> {
|
||||
readonly element: T;
|
||||
readonly parent: ITreeNode<T, TFilterData> | undefined;
|
||||
readonly children: ITreeNode<T, TFilterData>[];
|
||||
readonly depth: number;
|
||||
readonly collapsible: boolean;
|
||||
readonly collapsed: boolean;
|
||||
readonly visible: boolean;
|
||||
readonly filterData: TFilterData | undefined;
|
||||
}
|
||||
|
||||
function isInputElement(e: HTMLElement): boolean {
|
||||
return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA';
|
||||
export interface ITreeModel<T, TFilterData, TRef> {
|
||||
readonly onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>;
|
||||
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
|
||||
|
||||
getListIndex(location: TRef): number;
|
||||
getNode(location?: TRef): ITreeNode<T, any>;
|
||||
getNodeLocation(node: ITreeNode<T, any>): TRef;
|
||||
getParentNodeLocation(location: TRef): TRef;
|
||||
|
||||
getParentElement(location: TRef): T;
|
||||
getFirstElementChild(location: TRef): T | undefined;
|
||||
getLastElementAncestor(location?: TRef): T | undefined;
|
||||
|
||||
isCollapsible(location: TRef): boolean;
|
||||
isCollapsed(location: TRef): boolean;
|
||||
setCollapsed(location: TRef, collapsed: boolean): boolean;
|
||||
toggleCollapsed(location: TRef): void;
|
||||
collapseAll(): void;
|
||||
|
||||
refilter(): void;
|
||||
}
|
||||
|
||||
export interface ITreeOptions<T> extends IListOptions<T> { }
|
||||
export interface ITreeRenderer<T, TFilterData = void, TTemplateData = void> extends IListRenderer<ITreeNode<T, TFilterData>, TTemplateData> {
|
||||
renderTwistie?(element: T, twistieElement: HTMLElement): void;
|
||||
onDidChangeTwistieState?: Event<T>;
|
||||
}
|
||||
|
||||
export class Tree<T> implements IDisposable {
|
||||
export interface ITreeEvent<T> {
|
||||
elements: T[];
|
||||
browserEvent?: UIEvent;
|
||||
}
|
||||
|
||||
private view: List<ITreeNode<T>>;
|
||||
private model: TreeModel<T>;
|
||||
private disposables: IDisposable[] = [];
|
||||
export interface ITreeMouseEvent<T> {
|
||||
browserEvent: MouseEvent;
|
||||
element: T | null;
|
||||
}
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
delegate: IVirtualDelegate<T>,
|
||||
renderers: IRenderer<T, any>[],
|
||||
options?: ITreeOptions<T>
|
||||
) {
|
||||
const treeDelegate = new TreeDelegate(delegate);
|
||||
|
||||
const onDidChangeCollapseStateRelay = new Relay<ITreeNode<T>>();
|
||||
const treeRenderers = renderers.map(r => new TreeRenderer(r, onDidChangeCollapseStateRelay.event));
|
||||
this.disposables.push(...treeRenderers);
|
||||
|
||||
const treeOptions = toTreeListOptions(options);
|
||||
|
||||
this.view = new List(container, treeDelegate, treeRenderers, treeOptions);
|
||||
this.model = new TreeModel<T>(this.view);
|
||||
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);
|
||||
}
|
||||
|
||||
private onMouseClick(e: IListMouseEvent<ITreeNode<T>>): void {
|
||||
const node = e.element;
|
||||
const location = 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 = 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.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 = getNodeLocation(node);
|
||||
const didChange = this.model.setCollapsed(location, false);
|
||||
|
||||
if (!didChange) {
|
||||
if (node.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [focusedIndex] = this.view.getFocus();
|
||||
this.view.setFocus([focusedIndex + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
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 = getNodeLocation(node);
|
||||
this.model.toggleCollapsed(location);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
this.view.dispose();
|
||||
this.view = null;
|
||||
this.model = null;
|
||||
}
|
||||
}
|
||||
export interface ITreeContextMenuEvent<T> {
|
||||
browserEvent: UIEvent;
|
||||
element: T | null;
|
||||
anchor: HTMLElement | { x: number; y: number; } | undefined;
|
||||
}
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
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> {
|
||||
readonly parent: IMutableTreeNode<T> | undefined;
|
||||
readonly element: T;
|
||||
readonly children: IMutableTreeNode<T>[];
|
||||
readonly depth: number;
|
||||
readonly collapsible: boolean;
|
||||
readonly collapsed: boolean;
|
||||
readonly visibleCount: number;
|
||||
}
|
||||
|
||||
interface IMutableTreeNode<T> extends ITreeNode<T> {
|
||||
collapsed: boolean;
|
||||
visibleCount: number;
|
||||
}
|
||||
|
||||
function visibleCountReducer<T>(result: number, node: IMutableTreeNode<T>): number {
|
||||
return result + (node.collapsed ? 1 : node.visibleCount);
|
||||
}
|
||||
|
||||
function getVisibleCount<T>(nodes: IMutableTreeNode<T>[]): number {
|
||||
return nodes.reduce(visibleCountReducer, 0);
|
||||
}
|
||||
|
||||
function getVisibleNodes<T>(nodes: IMutableTreeNode<T>[], result: ITreeNode<T>[] = []): ITreeNode<T>[] {
|
||||
for (const node of nodes) {
|
||||
result.push(node);
|
||||
|
||||
if (!node.collapsed) {
|
||||
getVisibleNodes(node.children, result);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getTreeElementIterator<T>(elements: Iterator<ITreeElement<T>> | ITreeElement<T>[] | undefined): Iterator<ITreeElement<T>> {
|
||||
if (!elements) {
|
||||
return Iterator.empty();
|
||||
} else if (Array.isArray(elements)) {
|
||||
return Iterator.iterate(elements);
|
||||
} else {
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
||||
function treeElementToNode<T>(treeElement: ITreeElement<T>, parent: IMutableTreeNode<T>, visible: boolean, treeListElements: ITreeNode<T>[]): IMutableTreeNode<T> {
|
||||
const depth = parent.depth + 1;
|
||||
const { element, collapsible, collapsed } = treeElement;
|
||||
const node = { parent, element, children: [], depth, collapsible: !!collapsible, collapsed: !!collapsed, visibleCount: 0 };
|
||||
|
||||
if (visible) {
|
||||
treeListElements.push(node);
|
||||
}
|
||||
|
||||
const children = getTreeElementIterator(treeElement.children);
|
||||
node.children = Iterator.collect(Iterator.map(children, el => treeElementToNode(el, node, visible && !treeElement.collapsed, treeListElements)));
|
||||
node.collapsible = node.collapsible || node.children.length > 0;
|
||||
node.visibleCount = 1 + getVisibleCount(node.children);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function treeNodeToElement<T>(node: IMutableTreeNode<T>): ITreeElement<T> {
|
||||
const { element, collapsed } = node;
|
||||
const children = Iterator.map(Iterator.iterate(node.children), treeNodeToElement);
|
||||
|
||||
return { element, children, collapsed };
|
||||
}
|
||||
|
||||
export function getNodeLocation<T>(node: ITreeNode<T>): number[] {
|
||||
const location = [];
|
||||
|
||||
while (node.parent) {
|
||||
location.push(node.parent.children.indexOf(node));
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return location.reverse();
|
||||
}
|
||||
|
||||
export class TreeModel<T> {
|
||||
|
||||
private root: IMutableTreeNode<T> = {
|
||||
parent: undefined,
|
||||
element: undefined,
|
||||
children: [],
|
||||
depth: 0,
|
||||
collapsible: false,
|
||||
collapsed: false,
|
||||
visibleCount: 1
|
||||
};
|
||||
|
||||
private _onDidChangeCollapseState = new Emitter<ITreeNode<T>>();
|
||||
readonly onDidChangeCollapseState: Event<ITreeNode<T>> = this._onDidChangeCollapseState.event;
|
||||
|
||||
constructor(private list: ISpliceable<ITreeNode<T>>) { }
|
||||
|
||||
splice(location: number[], deleteCount: number, toInsert?: ISequence<ITreeElement<T>>): Iterator<ITreeElement<T>> {
|
||||
if (location.length === 0) {
|
||||
throw new Error('Invalid tree location');
|
||||
}
|
||||
|
||||
const { parentNode, listIndex, visible } = this.findParentNode(location);
|
||||
const treeListElementsToInsert: ITreeNode<T>[] = [];
|
||||
const elementsToInsert = getTreeElementIterator(toInsert);
|
||||
const nodesToInsert = Iterator.collect(Iterator.map(elementsToInsert, el => treeElementToNode(el, parentNode, visible, treeListElementsToInsert)));
|
||||
const lastIndex = location[location.length - 1];
|
||||
const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert);
|
||||
const visibleDeleteCount = getVisibleCount(deletedNodes);
|
||||
|
||||
parentNode.visibleCount += getVisibleCount(nodesToInsert) - visibleDeleteCount;
|
||||
|
||||
if (visible) {
|
||||
this.list.splice(listIndex, visibleDeleteCount, treeListElementsToInsert);
|
||||
}
|
||||
|
||||
return Iterator.map(Iterator.iterate(deletedNodes), treeNodeToElement);
|
||||
}
|
||||
|
||||
getListIndex(location: number[]): number {
|
||||
return this.findNode(location).listIndex;
|
||||
}
|
||||
|
||||
setCollapsed(location: number[], collapsed: boolean): boolean {
|
||||
return this._setCollapsed(location, collapsed);
|
||||
}
|
||||
|
||||
toggleCollapsed(location: number[]): void {
|
||||
this._setCollapsed(location);
|
||||
}
|
||||
|
||||
private _setCollapsed(location: number[], collapsed?: boolean | undefined): boolean {
|
||||
const { node, listIndex, visible } = this.findNode(location);
|
||||
|
||||
if (!node.collapsible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof collapsed === 'undefined') {
|
||||
collapsed = !node.collapsed;
|
||||
}
|
||||
|
||||
if (node.collapsed === collapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
node.collapsed = collapsed;
|
||||
|
||||
if (visible) {
|
||||
this._onDidChangeCollapseState.fire(node);
|
||||
|
||||
let visibleCountDiff: number;
|
||||
|
||||
if (collapsed) {
|
||||
const deleteCount = getVisibleCount(node.children);
|
||||
|
||||
this.list.splice(listIndex + 1, deleteCount, []);
|
||||
visibleCountDiff = -deleteCount;
|
||||
} else {
|
||||
const toInsert = getVisibleNodes(node.children);
|
||||
|
||||
this.list.splice(listIndex + 1, 0, toInsert);
|
||||
visibleCountDiff = toInsert.length;
|
||||
}
|
||||
|
||||
let mutableNode = node;
|
||||
|
||||
while (mutableNode) {
|
||||
mutableNode.visibleCount += visibleCountDiff;
|
||||
mutableNode = mutableNode.parent;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
isCollapsed(location: number[]): boolean {
|
||||
return this.findNode(location).node.collapsed;
|
||||
}
|
||||
|
||||
private findNode(location: number[]): { node: IMutableTreeNode<T>, listIndex: number, visible: boolean } {
|
||||
const { parentNode, listIndex, visible } = this.findParentNode(location);
|
||||
const index = location[location.length - 1];
|
||||
|
||||
if (index < 0 || index > parentNode.children.length) {
|
||||
throw new Error('Invalid tree location');
|
||||
}
|
||||
|
||||
const node = parentNode.children[index];
|
||||
|
||||
return { node, listIndex, visible };
|
||||
}
|
||||
|
||||
private findParentNode(location: number[], node: IMutableTreeNode<T> = this.root, listIndex: number = 0, visible = true): { parentNode: IMutableTreeNode<T>; listIndex: number; visible: boolean; } {
|
||||
const [index, ...rest] = location;
|
||||
|
||||
if (index < 0 || index > node.children.length) {
|
||||
throw new Error('Invalid tree location');
|
||||
}
|
||||
|
||||
// TODO@joao perf!
|
||||
for (let i = 0; i < index; i++) {
|
||||
listIndex += node.children[i].visibleCount;
|
||||
}
|
||||
|
||||
visible = visible && !node.collapsed;
|
||||
|
||||
if (rest.length === 0) {
|
||||
return { parentNode: node, listIndex, visible };
|
||||
}
|
||||
|
||||
return this.findParentNode(rest, node.children[index], listIndex + 1, visible);
|
||||
}
|
||||
}
|
||||