Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
1750
lib/vscode/src/vs/base/browser/ui/tree/abstractTree.ts
Normal file
1750
lib/vscode/src/vs/base/browser/ui/tree/abstractTree.ts
Normal file
File diff suppressed because it is too large
Load Diff
1285
lib/vscode/src/vs/base/browser/ui/tree/asyncDataTree.ts
Normal file
1285
lib/vscode/src/vs/base/browser/ui/tree/asyncDataTree.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,523 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ITreeModel, ITreeNode, ITreeElement, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError, TreeFilterResult, TreeVisibility, WeakMapper } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
|
||||
import { IList } from 'vs/base/browser/ui/tree/indexTreeModel';
|
||||
|
||||
// Exported only for test reasons, do not use directly
|
||||
export interface ICompressedTreeElement<T> extends ITreeElement<T> {
|
||||
readonly children?: Iterable<ICompressedTreeElement<T>>;
|
||||
readonly incompressible?: boolean;
|
||||
}
|
||||
|
||||
// Exported only for test reasons, do not use directly
|
||||
export interface ICompressedTreeNode<T> {
|
||||
readonly elements: T[];
|
||||
readonly incompressible: boolean;
|
||||
}
|
||||
|
||||
function noCompress<T>(element: ICompressedTreeElement<T>): ITreeElement<ICompressedTreeNode<T>> {
|
||||
const elements = [element.element];
|
||||
const incompressible = element.incompressible || false;
|
||||
|
||||
return {
|
||||
element: { elements, incompressible },
|
||||
children: Iterable.map(Iterable.from(element.children), noCompress),
|
||||
collapsible: element.collapsible,
|
||||
collapsed: element.collapsed
|
||||
};
|
||||
}
|
||||
|
||||
// Exported only for test reasons, do not use directly
|
||||
export function compress<T>(element: ICompressedTreeElement<T>): ITreeElement<ICompressedTreeNode<T>> {
|
||||
const elements = [element.element];
|
||||
const incompressible = element.incompressible || false;
|
||||
|
||||
let childrenIterator: Iterable<ITreeElement<T>>;
|
||||
let children: ITreeElement<T>[];
|
||||
|
||||
while (true) {
|
||||
[children, childrenIterator] = Iterable.consume(Iterable.from(element.children), 2);
|
||||
|
||||
if (children.length !== 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
element = children[0];
|
||||
|
||||
if (element.incompressible) {
|
||||
break;
|
||||
}
|
||||
|
||||
elements.push(element.element);
|
||||
}
|
||||
|
||||
return {
|
||||
element: { elements, incompressible },
|
||||
children: Iterable.map(Iterable.concat(children, childrenIterator), compress),
|
||||
collapsible: element.collapsible,
|
||||
collapsed: element.collapsed
|
||||
};
|
||||
}
|
||||
|
||||
function _decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>, index = 0): ICompressedTreeElement<T> {
|
||||
let children: Iterable<ICompressedTreeElement<T>>;
|
||||
|
||||
if (index < element.element.elements.length - 1) {
|
||||
children = [_decompress(element, index + 1)];
|
||||
} else {
|
||||
children = Iterable.map(Iterable.from(element.children), el => _decompress(el, 0));
|
||||
}
|
||||
|
||||
if (index === 0 && element.element.incompressible) {
|
||||
return {
|
||||
element: element.element.elements[index],
|
||||
children,
|
||||
incompressible: true,
|
||||
collapsible: element.collapsible,
|
||||
collapsed: element.collapsed
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
element: element.element.elements[index],
|
||||
children,
|
||||
collapsible: element.collapsible,
|
||||
collapsed: element.collapsed
|
||||
};
|
||||
}
|
||||
|
||||
// Exported only for test reasons, do not use directly
|
||||
export function decompress<T>(element: ITreeElement<ICompressedTreeNode<T>>): ICompressedTreeElement<T> {
|
||||
return _decompress(element, 0);
|
||||
}
|
||||
|
||||
function splice<T>(treeElement: ICompressedTreeElement<T>, element: T, children: Iterable<ICompressedTreeElement<T>>): ICompressedTreeElement<T> {
|
||||
if (treeElement.element === element) {
|
||||
return { ...treeElement, children };
|
||||
}
|
||||
|
||||
return { ...treeElement, children: Iterable.map(Iterable.from(treeElement.children), e => splice(e, element, children)) };
|
||||
}
|
||||
|
||||
interface ICompressedObjectTreeModelOptions<T, TFilterData> extends IObjectTreeModelOptions<ICompressedTreeNode<T>, TFilterData> {
|
||||
readonly compressionEnabled?: boolean;
|
||||
}
|
||||
|
||||
// Exported only for test reasons, do not use directly
|
||||
export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements ITreeModel<ICompressedTreeNode<T> | null, TFilterData, T | null> {
|
||||
|
||||
readonly rootRef = null;
|
||||
|
||||
get onDidSplice(): Event<ITreeModelSpliceEvent<ICompressedTreeNode<T> | null, TFilterData>> { return this.model.onDidSplice; }
|
||||
get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<ICompressedTreeNode<T>, TFilterData>> { return this.model.onDidChangeCollapseState; }
|
||||
get onDidChangeRenderNodeCount(): Event<ITreeNode<ICompressedTreeNode<T>, TFilterData>> { return this.model.onDidChangeRenderNodeCount; }
|
||||
|
||||
private model: ObjectTreeModel<ICompressedTreeNode<T>, TFilterData>;
|
||||
private nodes = new Map<T | null, ICompressedTreeNode<T>>();
|
||||
private enabled: boolean;
|
||||
|
||||
get size(): number { return this.nodes.size; }
|
||||
|
||||
constructor(
|
||||
private user: string,
|
||||
list: IList<ITreeNode<ICompressedTreeNode<T>, TFilterData>>,
|
||||
options: ICompressedObjectTreeModelOptions<T, TFilterData> = {}
|
||||
) {
|
||||
this.model = new ObjectTreeModel(user, list, options);
|
||||
this.enabled = typeof options.compressionEnabled === 'undefined' ? true : options.compressionEnabled;
|
||||
}
|
||||
|
||||
setChildren(
|
||||
element: T | null,
|
||||
children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()
|
||||
): void {
|
||||
if (element === null) {
|
||||
const compressedChildren = Iterable.map(children, this.enabled ? compress : noCompress);
|
||||
this._setChildren(null, compressedChildren);
|
||||
return;
|
||||
}
|
||||
|
||||
const compressedNode = this.nodes.get(element);
|
||||
|
||||
if (!compressedNode) {
|
||||
throw new Error('Unknown compressed tree node');
|
||||
}
|
||||
|
||||
const node = this.model.getNode(compressedNode) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
|
||||
const compressedParentNode = this.model.getParentNodeLocation(compressedNode);
|
||||
const parent = this.model.getNode(compressedParentNode) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
|
||||
|
||||
const decompressedElement = decompress(node);
|
||||
const splicedElement = splice(decompressedElement, element, children);
|
||||
const recompressedElement = (this.enabled ? compress : noCompress)(splicedElement);
|
||||
|
||||
const parentChildren = parent.children
|
||||
.map(child => child === node ? recompressedElement : child);
|
||||
|
||||
this._setChildren(parent.element, parentChildren);
|
||||
}
|
||||
|
||||
isCompressionEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
setCompressionEnabled(enabled: boolean): void {
|
||||
if (enabled === this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enabled = enabled;
|
||||
|
||||
const root = this.model.getNode();
|
||||
const rootChildren = root.children as ITreeNode<ICompressedTreeNode<T>>[];
|
||||
const decompressedRootChildren = Iterable.map(rootChildren, decompress);
|
||||
const recompressedRootChildren = Iterable.map(decompressedRootChildren, enabled ? compress : noCompress);
|
||||
this._setChildren(null, recompressedRootChildren);
|
||||
}
|
||||
|
||||
private _setChildren(
|
||||
node: ICompressedTreeNode<T> | null,
|
||||
children: Iterable<ITreeElement<ICompressedTreeNode<T>>>
|
||||
): void {
|
||||
const insertedElements = new Set<T | null>();
|
||||
const _onDidCreateNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
|
||||
for (const element of node.element.elements) {
|
||||
insertedElements.add(element);
|
||||
this.nodes.set(element, node.element);
|
||||
}
|
||||
};
|
||||
|
||||
const _onDidDeleteNode = (node: ITreeNode<ICompressedTreeNode<T>, TFilterData>) => {
|
||||
for (const element of node.element.elements) {
|
||||
if (!insertedElements.has(element)) {
|
||||
this.nodes.delete(element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode);
|
||||
}
|
||||
|
||||
has(element: T | null): boolean {
|
||||
return this.nodes.has(element);
|
||||
}
|
||||
|
||||
getListIndex(location: T | null): number {
|
||||
const node = this.getCompressedNode(location);
|
||||
return this.model.getListIndex(node);
|
||||
}
|
||||
|
||||
getListRenderCount(location: T | null): number {
|
||||
const node = this.getCompressedNode(location);
|
||||
return this.model.getListRenderCount(node);
|
||||
}
|
||||
|
||||
getNode(location?: T | null | undefined): ITreeNode<ICompressedTreeNode<T> | null, TFilterData> {
|
||||
if (typeof location === 'undefined') {
|
||||
return this.model.getNode();
|
||||
}
|
||||
|
||||
const node = this.getCompressedNode(location);
|
||||
return this.model.getNode(node);
|
||||
}
|
||||
|
||||
// TODO: review this
|
||||
getNodeLocation(node: ITreeNode<ICompressedTreeNode<T>, TFilterData>): T | null {
|
||||
const compressedNode = this.model.getNodeLocation(node);
|
||||
|
||||
if (compressedNode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return compressedNode.elements[compressedNode.elements.length - 1];
|
||||
}
|
||||
|
||||
// TODO: review this
|
||||
getParentNodeLocation(location: T | null): T | null {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
const parentNode = this.model.getParentNodeLocation(compressedNode);
|
||||
|
||||
if (parentNode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parentNode.elements[parentNode.elements.length - 1];
|
||||
}
|
||||
|
||||
getFirstElementChild(location: T | null): ICompressedTreeNode<T> | null | undefined {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
return this.model.getFirstElementChild(compressedNode);
|
||||
}
|
||||
|
||||
getLastElementAncestor(location?: T | null | undefined): ICompressedTreeNode<T> | null | undefined {
|
||||
const compressedNode = typeof location === 'undefined' ? undefined : this.getCompressedNode(location);
|
||||
return this.model.getLastElementAncestor(compressedNode);
|
||||
}
|
||||
|
||||
isCollapsible(location: T | null): boolean {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
return this.model.isCollapsible(compressedNode);
|
||||
}
|
||||
|
||||
setCollapsible(location: T | null, collapsible?: boolean): boolean {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
return this.model.setCollapsible(compressedNode, collapsible);
|
||||
}
|
||||
|
||||
isCollapsed(location: T | null): boolean {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
return this.model.isCollapsed(compressedNode);
|
||||
}
|
||||
|
||||
setCollapsed(location: T | null, collapsed?: boolean | undefined, recursive?: boolean | undefined): boolean {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
return this.model.setCollapsed(compressedNode, collapsed, recursive);
|
||||
}
|
||||
|
||||
expandTo(location: T | null): void {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
this.model.expandTo(compressedNode);
|
||||
}
|
||||
|
||||
rerender(location: T | null): void {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
this.model.rerender(compressedNode);
|
||||
}
|
||||
|
||||
updateElementHeight(element: T, height: number): void {
|
||||
const compressedNode = this.getCompressedNode(element);
|
||||
|
||||
if (!compressedNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.updateElementHeight(compressedNode, height);
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
this.model.refilter();
|
||||
}
|
||||
|
||||
resort(location: T | null = null, recursive = true): void {
|
||||
const compressedNode = this.getCompressedNode(location);
|
||||
this.model.resort(compressedNode, recursive);
|
||||
}
|
||||
|
||||
getCompressedNode(element: T | null): ICompressedTreeNode<T> | null {
|
||||
if (element === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const node = this.nodes.get(element);
|
||||
|
||||
if (!node) {
|
||||
throw new TreeError(this.user, `Tree element not found: ${element}`);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
// Compressible Object Tree
|
||||
|
||||
export type ElementMapper<T> = (elements: T[]) => T;
|
||||
export const DefaultElementMapper: ElementMapper<any> = elements => elements[elements.length - 1];
|
||||
|
||||
export type CompressedNodeUnwrapper<T> = (node: ICompressedTreeNode<T>) => T;
|
||||
type CompressedNodeWeakMapper<T, TFilterData> = WeakMapper<ITreeNode<ICompressedTreeNode<T> | null, TFilterData>, ITreeNode<T | null, TFilterData>>;
|
||||
|
||||
class CompressedTreeNodeWrapper<T, TFilterData> implements ITreeNode<T | null, TFilterData> {
|
||||
|
||||
get element(): T | null { return this.node.element === null ? null : this.unwrapper(this.node.element); }
|
||||
get children(): ITreeNode<T | null, TFilterData>[] { return this.node.children.map(node => new CompressedTreeNodeWrapper(this.unwrapper, node)); }
|
||||
get depth(): number { return this.node.depth; }
|
||||
get visibleChildrenCount(): number { return this.node.visibleChildrenCount; }
|
||||
get visibleChildIndex(): number { return this.node.visibleChildIndex; }
|
||||
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 unwrapper: CompressedNodeUnwrapper<T>,
|
||||
private node: ITreeNode<ICompressedTreeNode<T> | null, TFilterData>
|
||||
) { }
|
||||
}
|
||||
|
||||
function mapList<T, TFilterData>(nodeMapper: CompressedNodeWeakMapper<T, TFilterData>, list: IList<ITreeNode<T, TFilterData>>): IList<ITreeNode<ICompressedTreeNode<T>, TFilterData>> {
|
||||
return {
|
||||
splice(start: number, deleteCount: number, toInsert: ITreeNode<ICompressedTreeNode<T>, TFilterData>[]): void {
|
||||
list.splice(start, deleteCount, toInsert.map(node => nodeMapper.map(node)) as ITreeNode<T, TFilterData>[]);
|
||||
},
|
||||
updateElementHeight(index: number, height: number): void {
|
||||
list.updateElementHeight(index, height);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function mapOptions<T, TFilterData>(compressedNodeUnwrapper: CompressedNodeUnwrapper<T>, options: ICompressibleObjectTreeModelOptions<T, TFilterData>): ICompressedObjectTreeModelOptions<T, TFilterData> {
|
||||
return {
|
||||
...options,
|
||||
sorter: options.sorter && {
|
||||
compare(node: ICompressedTreeNode<T>, otherNode: ICompressedTreeNode<T>): number {
|
||||
return options.sorter!.compare(node.elements[0], otherNode.elements[0]);
|
||||
}
|
||||
},
|
||||
identityProvider: options.identityProvider && {
|
||||
getId(node: ICompressedTreeNode<T>): { toString(): string; } {
|
||||
return options.identityProvider!.getId(compressedNodeUnwrapper(node));
|
||||
}
|
||||
},
|
||||
filter: options.filter && {
|
||||
filter(node: ICompressedTreeNode<T>, parentVisibility: TreeVisibility): TreeFilterResult<TFilterData> {
|
||||
return options.filter!.filter(compressedNodeUnwrapper(node), parentVisibility);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICompressibleObjectTreeModelOptions<T, TFilterData> extends IObjectTreeModelOptions<T, TFilterData> {
|
||||
readonly compressionEnabled?: boolean;
|
||||
readonly elementMapper?: ElementMapper<T>;
|
||||
}
|
||||
|
||||
export class CompressibleObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements IObjectTreeModel<T, TFilterData> {
|
||||
|
||||
readonly rootRef = null;
|
||||
|
||||
get onDidSplice(): Event<ITreeModelSpliceEvent<T | null, TFilterData>> {
|
||||
return Event.map(this.model.onDidSplice, ({ insertedNodes, deletedNodes }) => ({
|
||||
insertedNodes: insertedNodes.map(node => this.nodeMapper.map(node)),
|
||||
deletedNodes: deletedNodes.map(node => this.nodeMapper.map(node)),
|
||||
}));
|
||||
}
|
||||
|
||||
get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<T | null, TFilterData>> {
|
||||
return Event.map(this.model.onDidChangeCollapseState, ({ node, deep }) => ({
|
||||
node: this.nodeMapper.map(node),
|
||||
deep
|
||||
}));
|
||||
}
|
||||
|
||||
get onDidChangeRenderNodeCount(): Event<ITreeNode<T | null, TFilterData>> {
|
||||
return Event.map(this.model.onDidChangeRenderNodeCount, node => this.nodeMapper.map(node));
|
||||
}
|
||||
|
||||
private elementMapper: ElementMapper<T>;
|
||||
private nodeMapper: CompressedNodeWeakMapper<T, TFilterData>;
|
||||
private model: CompressedObjectTreeModel<T, TFilterData>;
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
list: IList<ITreeNode<T, TFilterData>>,
|
||||
options: ICompressibleObjectTreeModelOptions<T, TFilterData> = {}
|
||||
) {
|
||||
this.elementMapper = options.elementMapper || DefaultElementMapper;
|
||||
const compressedNodeUnwrapper: CompressedNodeUnwrapper<T> = node => this.elementMapper(node.elements);
|
||||
this.nodeMapper = new WeakMapper(node => new CompressedTreeNodeWrapper(compressedNodeUnwrapper, node));
|
||||
|
||||
this.model = new CompressedObjectTreeModel(user, mapList(this.nodeMapper, list), mapOptions(compressedNodeUnwrapper, options));
|
||||
}
|
||||
|
||||
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()): void {
|
||||
this.model.setChildren(element, children);
|
||||
}
|
||||
|
||||
isCompressionEnabled(): boolean {
|
||||
return this.model.isCompressionEnabled();
|
||||
}
|
||||
|
||||
setCompressionEnabled(enabled: boolean): void {
|
||||
this.model.setCompressionEnabled(enabled);
|
||||
}
|
||||
|
||||
has(location: T | null): boolean {
|
||||
return this.model.has(location);
|
||||
}
|
||||
|
||||
getListIndex(location: T | null): number {
|
||||
return this.model.getListIndex(location);
|
||||
}
|
||||
|
||||
getListRenderCount(location: T | null): number {
|
||||
return this.model.getListRenderCount(location);
|
||||
}
|
||||
|
||||
getNode(location?: T | null | undefined): ITreeNode<T | null, any> {
|
||||
return this.nodeMapper.map(this.model.getNode(location));
|
||||
}
|
||||
|
||||
getNodeLocation(node: ITreeNode<T | null, any>): T | null {
|
||||
return node.element;
|
||||
}
|
||||
|
||||
getParentNodeLocation(location: T | null): T | null {
|
||||
return this.model.getParentNodeLocation(location);
|
||||
}
|
||||
|
||||
getFirstElementChild(location: T | null): T | null | undefined {
|
||||
const result = this.model.getFirstElementChild(location);
|
||||
|
||||
if (result === null || typeof result === 'undefined') {
|
||||
return result;
|
||||
}
|
||||
|
||||
return this.elementMapper(result.elements);
|
||||
}
|
||||
|
||||
getLastElementAncestor(location?: T | null | undefined): T | null | undefined {
|
||||
const result = this.model.getLastElementAncestor(location);
|
||||
|
||||
if (result === null || typeof result === 'undefined') {
|
||||
return result;
|
||||
}
|
||||
|
||||
return this.elementMapper(result.elements);
|
||||
}
|
||||
|
||||
isCollapsible(location: T | null): boolean {
|
||||
return this.model.isCollapsible(location);
|
||||
}
|
||||
|
||||
setCollapsible(location: T | null, collapsed?: boolean): boolean {
|
||||
return this.model.setCollapsible(location, collapsed);
|
||||
}
|
||||
|
||||
isCollapsed(location: T | null): boolean {
|
||||
return this.model.isCollapsed(location);
|
||||
}
|
||||
|
||||
setCollapsed(location: T | null, collapsed?: boolean | undefined, recursive?: boolean | undefined): boolean {
|
||||
return this.model.setCollapsed(location, collapsed, recursive);
|
||||
}
|
||||
|
||||
expandTo(location: T | null): void {
|
||||
return this.model.expandTo(location);
|
||||
}
|
||||
|
||||
rerender(location: T | null): void {
|
||||
return this.model.rerender(location);
|
||||
}
|
||||
|
||||
updateElementHeight(element: T, height: number): void {
|
||||
this.model.updateElementHeight(element, height);
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
return this.model.refilter();
|
||||
}
|
||||
|
||||
resort(element: T | null = null, recursive = true): void {
|
||||
return this.model.resort(element, recursive);
|
||||
}
|
||||
|
||||
getCompressedTreeNode(location: T | null = null): ITreeNode<ICompressedTreeNode<T> | null, TFilterData> {
|
||||
return this.model.getNode(location);
|
||||
}
|
||||
}
|
205
lib/vscode/src/vs/base/browser/ui/tree/dataTree.ts
Normal file
205
lib/vscode/src/vs/base/browser/ui/tree/dataTree.ts
Normal file
@ -0,0 +1,205 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
|
||||
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, IDataSource, TreeError } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
|
||||
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { IList } from 'vs/base/browser/ui/tree/indexTreeModel';
|
||||
|
||||
export interface IDataTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
|
||||
readonly sorter?: ITreeSorter<T>;
|
||||
}
|
||||
|
||||
export interface IDataTreeViewState {
|
||||
readonly focus: string[];
|
||||
readonly selection: string[];
|
||||
readonly expanded: string[];
|
||||
readonly scrollTop: number;
|
||||
}
|
||||
|
||||
export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
|
||||
|
||||
protected model!: ObjectTreeModel<T, TFilterData>;
|
||||
private input: TInput | undefined;
|
||||
|
||||
private identityProvider: IIdentityProvider<T> | undefined;
|
||||
private nodesByIdentity = new Map<string, ITreeNode<T, TFilterData>>();
|
||||
|
||||
constructor(
|
||||
private user: string,
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
renderers: ITreeRenderer<T, TFilterData, any>[],
|
||||
private dataSource: IDataSource<TInput, T>,
|
||||
options: IDataTreeOptions<T, TFilterData> = {}
|
||||
) {
|
||||
super(user, container, delegate, renderers, options as IDataTreeOptions<T | null, TFilterData>);
|
||||
this.identityProvider = options.identityProvider;
|
||||
}
|
||||
|
||||
// Model
|
||||
|
||||
getInput(): TInput | undefined {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
setInput(input: TInput, viewState?: IDataTreeViewState): void {
|
||||
if (viewState && !this.identityProvider) {
|
||||
throw new TreeError(this.user, 'Can\'t restore tree view state without an identity provider');
|
||||
}
|
||||
|
||||
this.input = input;
|
||||
|
||||
if (!viewState) {
|
||||
this._refresh(input);
|
||||
return;
|
||||
}
|
||||
|
||||
const focus: T[] = [];
|
||||
const selection: T[] = [];
|
||||
|
||||
const isCollapsed = (element: T) => {
|
||||
const id = this.identityProvider!.getId(element).toString();
|
||||
return viewState.expanded.indexOf(id) === -1;
|
||||
};
|
||||
|
||||
const onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
const id = this.identityProvider!.getId(node.element).toString();
|
||||
|
||||
if (viewState.focus.indexOf(id) > -1) {
|
||||
focus.push(node.element);
|
||||
}
|
||||
|
||||
if (viewState.selection.indexOf(id) > -1) {
|
||||
selection.push(node.element);
|
||||
}
|
||||
};
|
||||
|
||||
this._refresh(input, isCollapsed, onDidCreateNode);
|
||||
this.setFocus(focus);
|
||||
this.setSelection(selection);
|
||||
|
||||
if (viewState && typeof viewState.scrollTop === 'number') {
|
||||
this.scrollTop = viewState.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
updateChildren(element: TInput | T = this.input!): void {
|
||||
if (typeof this.input === 'undefined') {
|
||||
throw new TreeError(this.user, 'Tree input not set');
|
||||
}
|
||||
|
||||
let isCollapsed: ((el: T) => boolean | undefined) | undefined;
|
||||
|
||||
if (this.identityProvider) {
|
||||
isCollapsed = element => {
|
||||
const id = this.identityProvider!.getId(element).toString();
|
||||
const node = this.nodesByIdentity.get(id);
|
||||
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return node.collapsed;
|
||||
};
|
||||
}
|
||||
|
||||
this._refresh(element, isCollapsed);
|
||||
}
|
||||
|
||||
resort(element: T | TInput = this.input!, recursive = true): void {
|
||||
this.model.resort((element === this.input ? null : element) as T, recursive);
|
||||
}
|
||||
|
||||
// View
|
||||
|
||||
refresh(element?: T): void {
|
||||
if (element === undefined) {
|
||||
this.view.rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.rerender(element);
|
||||
}
|
||||
|
||||
// Implementation
|
||||
|
||||
private _refresh(element: TInput | T, isCollapsed?: (el: T) => boolean | undefined, onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void): void {
|
||||
let onDidDeleteNode: ((node: ITreeNode<T, TFilterData>) => void) | undefined;
|
||||
|
||||
if (this.identityProvider) {
|
||||
const insertedElements = new Set<string>();
|
||||
|
||||
const outerOnDidCreateNode = onDidCreateNode;
|
||||
onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
const id = this.identityProvider!.getId(node.element).toString();
|
||||
|
||||
insertedElements.add(id);
|
||||
this.nodesByIdentity.set(id, node);
|
||||
|
||||
if (outerOnDidCreateNode) {
|
||||
outerOnDidCreateNode(node);
|
||||
}
|
||||
};
|
||||
|
||||
onDidDeleteNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
const id = this.identityProvider!.getId(node.element).toString();
|
||||
|
||||
if (!insertedElements.has(id)) {
|
||||
this.nodesByIdentity.delete(id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.model.setChildren((element === this.input ? null : element) as T, this.iterate(element, isCollapsed).elements, onDidCreateNode, onDidDeleteNode);
|
||||
}
|
||||
|
||||
private iterate(element: TInput | T, isCollapsed?: (el: T) => boolean | undefined): { elements: Iterable<ITreeElement<T>>, size: number } {
|
||||
const children = [...this.dataSource.getChildren(element)];
|
||||
const elements = Iterable.map(children, element => {
|
||||
const { elements: children, size } = this.iterate(element, isCollapsed);
|
||||
const collapsible = this.dataSource.hasChildren ? this.dataSource.hasChildren(element) : undefined;
|
||||
const collapsed = size === 0 ? undefined : (isCollapsed && isCollapsed(element));
|
||||
|
||||
return { element, children, collapsible, collapsed };
|
||||
});
|
||||
|
||||
return { elements, size: children.length };
|
||||
}
|
||||
|
||||
protected createModel(user: string, view: IList<ITreeNode<T, TFilterData>>, options: IDataTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
return new ObjectTreeModel(user, view, options);
|
||||
}
|
||||
|
||||
// view state
|
||||
|
||||
getViewState(): IDataTreeViewState {
|
||||
if (!this.identityProvider) {
|
||||
throw new TreeError(this.user, 'Can\'t get tree view state without an identity provider');
|
||||
}
|
||||
|
||||
const getId = (element: T | null) => this.identityProvider!.getId(element!).toString();
|
||||
const focus = this.getFocus().map(getId);
|
||||
const selection = this.getSelection().map(getId);
|
||||
|
||||
const expanded: string[] = [];
|
||||
const root = this.model.getNode();
|
||||
const queue = [root];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift()!;
|
||||
|
||||
if (node !== root && node.collapsible && !node.collapsed) {
|
||||
expanded.push(getId(node.element!));
|
||||
}
|
||||
|
||||
queue.push(...node.children);
|
||||
}
|
||||
|
||||
return { focus, selection, expanded, scrollTop: this.scrollTop };
|
||||
}
|
||||
}
|
50
lib/vscode/src/vs/base/browser/ui/tree/indexTree.ts
Normal file
50
lib/vscode/src/vs/base/browser/ui/tree/indexTree.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/tree';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
|
||||
import { IndexTreeModel, IList } 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> { }
|
||||
|
||||
export class IndexTree<T, TFilterData = void> extends AbstractTree<T, TFilterData, number[]> {
|
||||
|
||||
protected model!: IndexTreeModel<T, TFilterData>;
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
renderers: ITreeRenderer<T, TFilterData, any>[],
|
||||
private rootElement: T,
|
||||
options: IIndexTreeOptions<T, TFilterData> = {}
|
||||
) {
|
||||
super(user, container, delegate, renderers, options);
|
||||
}
|
||||
|
||||
splice(location: number[], deleteCount: number, toInsert: Iterable<ITreeElement<T>> = Iterable.empty()): void {
|
||||
this.model.splice(location, deleteCount, toInsert);
|
||||
}
|
||||
|
||||
rerender(location?: number[]): void {
|
||||
if (location === undefined) {
|
||||
this.view.rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.rerender(location);
|
||||
}
|
||||
|
||||
updateElementHeight(location: number[], height: number): void {
|
||||
this.model.updateElementHeight(location, height);
|
||||
}
|
||||
|
||||
protected createModel(user: string, view: IList<ITreeNode<T, TFilterData>>, options: IIndexTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, number[]> {
|
||||
return new IndexTreeModel(user, view, this.rootElement, options);
|
||||
}
|
||||
}
|
677
lib/vscode/src/vs/base/browser/ui/tree/indexTreeModel.ts
Normal file
677
lib/vscode/src/vs/base/browser/ui/tree/indexTreeModel.ts
Normal file
@ -0,0 +1,677 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICollapseStateChangeEvent, ITreeElement, ITreeFilter, ITreeFilterDataResult, ITreeModel, ITreeNode, TreeVisibility, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
|
||||
import { tail2 } from 'vs/base/common/arrays';
|
||||
import { Emitter, Event, EventBufferer } from 'vs/base/common/event';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
|
||||
// Exported for tests
|
||||
export interface IIndexTreeNode<T, TFilterData = void> extends ITreeNode<T, TFilterData> {
|
||||
readonly parent: IIndexTreeNode<T, TFilterData> | undefined;
|
||||
readonly children: IIndexTreeNode<T, TFilterData>[];
|
||||
visibleChildrenCount: number;
|
||||
visibleChildIndex: number;
|
||||
collapsible: boolean;
|
||||
collapsed: boolean;
|
||||
renderNodeCount: number;
|
||||
visibility: TreeVisibility;
|
||||
visible: boolean;
|
||||
filterData: TFilterData | undefined;
|
||||
}
|
||||
|
||||
export function isFilterResult<T>(obj: any): obj is ITreeFilterDataResult<T> {
|
||||
return typeof obj === 'object' && 'visibility' in obj && 'data' in obj;
|
||||
}
|
||||
|
||||
export 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> {
|
||||
readonly collapseByDefault?: boolean; // defaults to false
|
||||
readonly filter?: ITreeFilter<T, TFilterData>;
|
||||
readonly autoExpandSingleChildren?: boolean;
|
||||
}
|
||||
|
||||
interface CollapsibleStateUpdate {
|
||||
readonly collapsible: boolean;
|
||||
}
|
||||
|
||||
interface CollapsedStateUpdate {
|
||||
readonly collapsed: boolean;
|
||||
readonly recursive: boolean;
|
||||
}
|
||||
|
||||
type CollapseStateUpdate = CollapsibleStateUpdate | CollapsedStateUpdate;
|
||||
|
||||
function isCollapsibleStateUpdate(update: CollapseStateUpdate): update is CollapsibleStateUpdate {
|
||||
return typeof (update as any).collapsible === 'boolean';
|
||||
}
|
||||
|
||||
export interface IList<T> extends ISpliceable<T> {
|
||||
updateElementHeight(index: number, height: number): void;
|
||||
}
|
||||
|
||||
export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = void> implements ITreeModel<T, TFilterData, number[]> {
|
||||
|
||||
readonly rootRef = [];
|
||||
|
||||
private root: IIndexTreeNode<T, TFilterData>;
|
||||
private eventBufferer = new EventBufferer();
|
||||
|
||||
private readonly _onDidChangeCollapseState = new Emitter<ICollapseStateChangeEvent<T, TFilterData>>();
|
||||
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>> = this.eventBufferer.wrapEvent(this._onDidChangeCollapseState.event);
|
||||
|
||||
private readonly _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>;
|
||||
private autoExpandSingleChildren: boolean;
|
||||
|
||||
private readonly _onDidSplice = new Emitter<ITreeModelSpliceEvent<T, TFilterData>>();
|
||||
readonly onDidSplice = this._onDidSplice.event;
|
||||
|
||||
constructor(
|
||||
private user: string,
|
||||
private list: IList<ITreeNode<T, TFilterData>>,
|
||||
rootElement: T,
|
||||
options: IIndexTreeModelOptions<T, TFilterData> = {}
|
||||
) {
|
||||
this.collapseByDefault = typeof options.collapseByDefault === 'undefined' ? false : options.collapseByDefault;
|
||||
this.filter = options.filter;
|
||||
this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren;
|
||||
|
||||
this.root = {
|
||||
parent: undefined,
|
||||
element: rootElement,
|
||||
children: [],
|
||||
depth: 0,
|
||||
visibleChildrenCount: 0,
|
||||
visibleChildIndex: -1,
|
||||
collapsible: false,
|
||||
collapsed: false,
|
||||
renderNodeCount: 0,
|
||||
visibility: TreeVisibility.Visible,
|
||||
visible: true,
|
||||
filterData: undefined
|
||||
};
|
||||
}
|
||||
|
||||
splice(
|
||||
location: number[],
|
||||
deleteCount: number,
|
||||
toInsert: Iterable<ITreeElement<T>> = Iterable.empty(),
|
||||
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
|
||||
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
|
||||
): void {
|
||||
if (location.length === 0) {
|
||||
throw new TreeError(this.user, 'Invalid tree location');
|
||||
}
|
||||
|
||||
const { parentNode, listIndex, revealed, visible } = this.getParentNodeWithListIndex(location);
|
||||
const treeListElementsToInsert: ITreeNode<T, TFilterData>[] = [];
|
||||
const nodesToInsertIterator = Iterable.map(toInsert, el => this.createTreeNode(el, parentNode, parentNode.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, revealed, treeListElementsToInsert, onDidCreateNode));
|
||||
|
||||
const lastIndex = location[location.length - 1];
|
||||
|
||||
// figure out what's the visible child start index right before the
|
||||
// splice point
|
||||
let visibleChildStartIndex = 0;
|
||||
|
||||
for (let i = lastIndex; i >= 0 && i < parentNode.children.length; i--) {
|
||||
const child = parentNode.children[i];
|
||||
|
||||
if (child.visible) {
|
||||
visibleChildStartIndex = child.visibleChildIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const nodesToInsert: IIndexTreeNode<T, TFilterData>[] = [];
|
||||
let insertedVisibleChildrenCount = 0;
|
||||
let renderNodeCount = 0;
|
||||
|
||||
for (const child of nodesToInsertIterator) {
|
||||
nodesToInsert.push(child);
|
||||
renderNodeCount += child.renderNodeCount;
|
||||
|
||||
if (child.visible) {
|
||||
child.visibleChildIndex = visibleChildStartIndex + insertedVisibleChildrenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert);
|
||||
|
||||
// figure out what is the count of deleted visible children
|
||||
let deletedVisibleChildrenCount = 0;
|
||||
|
||||
for (const child of deletedNodes) {
|
||||
if (child.visible) {
|
||||
deletedVisibleChildrenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// and adjust for all visible children after the splice point
|
||||
if (deletedVisibleChildrenCount !== 0) {
|
||||
for (let i = lastIndex + nodesToInsert.length; i < parentNode.children.length; i++) {
|
||||
const child = parentNode.children[i];
|
||||
|
||||
if (child.visible) {
|
||||
child.visibleChildIndex -= deletedVisibleChildrenCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update parent's visible children count
|
||||
parentNode.visibleChildrenCount += insertedVisibleChildrenCount - deletedVisibleChildrenCount;
|
||||
|
||||
if (revealed && visible) {
|
||||
const visibleDeleteCount = deletedNodes.reduce((r, node) => r + (node.visible ? node.renderNodeCount : 0), 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);
|
||||
}
|
||||
|
||||
this._onDidSplice.fire({ insertedNodes: nodesToInsert, deletedNodes });
|
||||
|
||||
let node: IIndexTreeNode<T, TFilterData> | undefined = parentNode;
|
||||
|
||||
while (node) {
|
||||
if (node.visibility === TreeVisibility.Recurse) {
|
||||
this.refilter();
|
||||
break;
|
||||
}
|
||||
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
|
||||
rerender(location: number[]): void {
|
||||
if (location.length === 0) {
|
||||
throw new TreeError(this.user, 'Invalid tree location');
|
||||
}
|
||||
|
||||
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
|
||||
|
||||
if (node.visible && revealed) {
|
||||
this.list.splice(listIndex, 1, [node]);
|
||||
}
|
||||
}
|
||||
|
||||
updateElementHeight(location: number[], height: number): void {
|
||||
if (location.length === 0) {
|
||||
throw new TreeError(this.user, 'Invalid tree location');
|
||||
}
|
||||
|
||||
const { listIndex } = this.getTreeNodeWithListIndex(location);
|
||||
this.list.updateElementHeight(listIndex, height);
|
||||
}
|
||||
|
||||
has(location: number[]): boolean {
|
||||
return this.hasTreeNode(location);
|
||||
}
|
||||
|
||||
getListIndex(location: number[]): number {
|
||||
const { listIndex, visible, revealed } = this.getTreeNodeWithListIndex(location);
|
||||
return visible && revealed ? listIndex : -1;
|
||||
}
|
||||
|
||||
getListRenderCount(location: number[]): number {
|
||||
return this.getTreeNode(location).renderNodeCount;
|
||||
}
|
||||
|
||||
isCollapsible(location: number[]): boolean {
|
||||
return this.getTreeNode(location).collapsible;
|
||||
}
|
||||
|
||||
setCollapsible(location: number[], collapsible?: boolean): boolean {
|
||||
const node = this.getTreeNode(location);
|
||||
|
||||
if (typeof collapsible === 'undefined') {
|
||||
collapsible = !node.collapsible;
|
||||
}
|
||||
|
||||
const update: CollapsibleStateUpdate = { collapsible };
|
||||
return this.eventBufferer.bufferEvents(() => this._setCollapseState(location, update));
|
||||
}
|
||||
|
||||
isCollapsed(location: number[]): boolean {
|
||||
return this.getTreeNode(location).collapsed;
|
||||
}
|
||||
|
||||
setCollapsed(location: number[], collapsed?: boolean, recursive?: boolean): boolean {
|
||||
const node = this.getTreeNode(location);
|
||||
|
||||
if (typeof collapsed === 'undefined') {
|
||||
collapsed = !node.collapsed;
|
||||
}
|
||||
|
||||
const update: CollapsedStateUpdate = { collapsed, recursive: recursive || false };
|
||||
return this.eventBufferer.bufferEvents(() => this._setCollapseState(location, update));
|
||||
}
|
||||
|
||||
private _setCollapseState(location: number[], update: CollapseStateUpdate): boolean {
|
||||
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
|
||||
|
||||
const result = this._setListNodeCollapseState(node, listIndex, revealed, update);
|
||||
|
||||
if (node !== this.root && this.autoExpandSingleChildren && result && !isCollapsibleStateUpdate(update) && node.collapsible && !node.collapsed && !update.recursive) {
|
||||
let onlyVisibleChildIndex = -1;
|
||||
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i];
|
||||
|
||||
if (child.visible) {
|
||||
if (onlyVisibleChildIndex > -1) {
|
||||
onlyVisibleChildIndex = -1;
|
||||
break;
|
||||
} else {
|
||||
onlyVisibleChildIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onlyVisibleChildIndex > -1) {
|
||||
this._setCollapseState([...location, onlyVisibleChildIndex], update);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _setListNodeCollapseState(node: IIndexTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, update: CollapseStateUpdate): boolean {
|
||||
const result = this._setNodeCollapseState(node, update, false);
|
||||
|
||||
if (!revealed || !node.visible || !result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const previousRenderNodeCount = node.renderNodeCount;
|
||||
const toInsert = this.updateNodeAfterCollapseChange(node);
|
||||
const deleteCount = previousRenderNodeCount - (listIndex === -1 ? 0 : 1);
|
||||
this.list.splice(listIndex + 1, deleteCount, toInsert.slice(1));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _setNodeCollapseState(node: IIndexTreeNode<T, TFilterData>, update: CollapseStateUpdate, deep: boolean): boolean {
|
||||
let result: boolean;
|
||||
|
||||
if (node === this.root) {
|
||||
result = false;
|
||||
} else {
|
||||
if (isCollapsibleStateUpdate(update)) {
|
||||
result = node.collapsible !== update.collapsible;
|
||||
node.collapsible = update.collapsible;
|
||||
} else if (!node.collapsible) {
|
||||
result = false;
|
||||
} else {
|
||||
result = node.collapsed !== update.collapsed;
|
||||
node.collapsed = update.collapsed;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
this._onDidChangeCollapseState.fire({ node, deep });
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCollapsibleStateUpdate(update) && update.recursive) {
|
||||
for (const child of node.children) {
|
||||
result = this._setNodeCollapseState(child, update, true) || result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
expandTo(location: number[]): void {
|
||||
this.eventBufferer.bufferEvents(() => {
|
||||
let node = this.getTreeNode(location);
|
||||
|
||||
while (node.parent) {
|
||||
node = node.parent;
|
||||
location = location.slice(0, location.length - 1);
|
||||
|
||||
if (node.collapsed) {
|
||||
this._setCollapseState(location, { collapsed: false, recursive: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
const previousRenderNodeCount = this.root.renderNodeCount;
|
||||
const toInsert = this.updateNodeAfterFilterChange(this.root);
|
||||
this.list.splice(0, previousRenderNodeCount, toInsert);
|
||||
}
|
||||
|
||||
private createTreeNode(
|
||||
treeElement: ITreeElement<T>,
|
||||
parent: IIndexTreeNode<T, TFilterData>,
|
||||
parentVisibility: TreeVisibility,
|
||||
revealed: boolean,
|
||||
treeListElements: ITreeNode<T, TFilterData>[],
|
||||
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void
|
||||
): IIndexTreeNode<T, TFilterData> {
|
||||
const node: IIndexTreeNode<T, TFilterData> = {
|
||||
parent,
|
||||
element: treeElement.element,
|
||||
children: [],
|
||||
depth: parent.depth + 1,
|
||||
visibleChildrenCount: 0,
|
||||
visibleChildIndex: -1,
|
||||
collapsible: typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : (typeof treeElement.collapsed !== 'undefined'),
|
||||
collapsed: typeof treeElement.collapsed === 'undefined' ? this.collapseByDefault : treeElement.collapsed,
|
||||
renderNodeCount: 1,
|
||||
visibility: TreeVisibility.Visible,
|
||||
visible: true,
|
||||
filterData: undefined
|
||||
};
|
||||
|
||||
const visibility = this._filterNode(node, parentVisibility);
|
||||
node.visibility = visibility;
|
||||
|
||||
if (revealed) {
|
||||
treeListElements.push(node);
|
||||
}
|
||||
|
||||
const childElements = treeElement.children || Iterable.empty();
|
||||
const childRevealed = revealed && visibility !== TreeVisibility.Hidden && !node.collapsed;
|
||||
const childNodes = Iterable.map(childElements, el => this.createTreeNode(el, node, visibility, childRevealed, treeListElements, onDidCreateNode));
|
||||
|
||||
let visibleChildrenCount = 0;
|
||||
let renderNodeCount = 1;
|
||||
|
||||
for (const child of childNodes) {
|
||||
node.children.push(child);
|
||||
renderNodeCount += child.renderNodeCount;
|
||||
|
||||
if (child.visible) {
|
||||
child.visibleChildIndex = visibleChildrenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
node.collapsible = node.collapsible || node.children.length > 0;
|
||||
node.visibleChildrenCount = visibleChildrenCount;
|
||||
node.visible = visibility === TreeVisibility.Recurse ? visibleChildrenCount > 0 : (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: IIndexTreeNode<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: IIndexTreeNode<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: IIndexTreeNode<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: IIndexTreeNode<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;
|
||||
node.renderNodeCount = 0;
|
||||
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) {
|
||||
let visibleChildIndex = 0;
|
||||
|
||||
for (const child of node.children) {
|
||||
hasVisibleDescendants = this._updateNodeAfterFilterChange(child, visibility!, result, revealed && !node.collapsed) || hasVisibleDescendants;
|
||||
|
||||
if (child.visible) {
|
||||
child.visibleChildIndex = visibleChildIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
node.visibleChildrenCount = visibleChildIndex;
|
||||
} else {
|
||||
node.visibleChildrenCount = 0;
|
||||
}
|
||||
|
||||
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: IIndexTreeNode<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: IIndexTreeNode<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 hasTreeNode(location: number[], node: IIndexTreeNode<T, TFilterData> = this.root): boolean {
|
||||
if (!location || location.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [index, ...rest] = location;
|
||||
|
||||
if (index < 0 || index > node.children.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.hasTreeNode(rest, node.children[index]);
|
||||
}
|
||||
|
||||
// cheap
|
||||
private getTreeNode(location: number[], node: IIndexTreeNode<T, TFilterData> = this.root): IIndexTreeNode<T, TFilterData> {
|
||||
if (!location || location.length === 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const [index, ...rest] = location;
|
||||
|
||||
if (index < 0 || index > node.children.length) {
|
||||
throw new TreeError(this.user, 'Invalid tree location');
|
||||
}
|
||||
|
||||
return this.getTreeNode(rest, node.children[index]);
|
||||
}
|
||||
|
||||
// expensive
|
||||
private getTreeNodeWithListIndex(location: number[]): { node: IIndexTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, visible: boolean } {
|
||||
if (location.length === 0) {
|
||||
return { node: this.root, listIndex: -1, revealed: true, visible: false };
|
||||
}
|
||||
|
||||
const { parentNode, listIndex, revealed, visible } = this.getParentNodeWithListIndex(location);
|
||||
const index = location[location.length - 1];
|
||||
|
||||
if (index < 0 || index > parentNode.children.length) {
|
||||
throw new TreeError(this.user, 'Invalid tree location');
|
||||
}
|
||||
|
||||
const node = parentNode.children[index];
|
||||
|
||||
return { node, listIndex, revealed, visible: visible && node.visible };
|
||||
}
|
||||
|
||||
private getParentNodeWithListIndex(location: number[], node: IIndexTreeNode<T, TFilterData> = this.root, listIndex: number = 0, revealed = true, visible = true): { parentNode: IIndexTreeNode<T, TFilterData>; listIndex: number; revealed: boolean; visible: boolean; } {
|
||||
const [index, ...rest] = location;
|
||||
|
||||
if (index < 0 || index > node.children.length) {
|
||||
throw new TreeError(this.user, 'Invalid tree location');
|
||||
}
|
||||
|
||||
// TODO@joao perf!
|
||||
for (let i = 0; i < index; i++) {
|
||||
listIndex += node.children[i].renderNodeCount;
|
||||
}
|
||||
|
||||
revealed = revealed && !node.collapsed;
|
||||
visible = visible && node.visible;
|
||||
|
||||
if (rest.length === 0) {
|
||||
return { parentNode: node, listIndex, revealed, visible };
|
||||
}
|
||||
|
||||
return this.getParentNodeWithListIndex(rest, node.children[index], listIndex + 1, revealed, visible);
|
||||
}
|
||||
|
||||
getNode(location: number[] = []): ITreeNode<T, TFilterData> {
|
||||
return this.getTreeNode(location);
|
||||
}
|
||||
|
||||
// TODO@joao perf!
|
||||
getNodeLocation(node: ITreeNode<T, TFilterData>): number[] {
|
||||
const location: number[] = [];
|
||||
let indexTreeNode = node as IIndexTreeNode<T, TFilterData>; // typing woes
|
||||
|
||||
while (indexTreeNode.parent) {
|
||||
location.push(indexTreeNode.parent.children.indexOf(indexTreeNode));
|
||||
indexTreeNode = indexTreeNode.parent;
|
||||
}
|
||||
|
||||
return location.reverse();
|
||||
}
|
||||
|
||||
getParentNodeLocation(location: number[]): number[] | undefined {
|
||||
if (location.length === 0) {
|
||||
return undefined;
|
||||
} else if (location.length === 1) {
|
||||
return [];
|
||||
} else {
|
||||
return tail2(location)[0];
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
12
lib/vscode/src/vs/base/browser/ui/tree/media/paneviewlet.css
Normal file
12
lib/vscode/src/vs/base/browser/ui/tree/media/paneviewlet.css
Normal file
@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-pane-view .pane > .pane-header h3.title {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
}
|
66
lib/vscode/src/vs/base/browser/ui/tree/media/tree.css
Normal file
66
lib/vscode/src/vs/base/browser/ui/tree/media/tree.css
Normal file
@ -0,0 +1,66 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-tl-row {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-tl-indent {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 16px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hide-arrows .monaco-tl-indent {
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.monaco-tl-indent > .indent-guide {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
.monaco-tl-indent > .indent-guide {
|
||||
transition: border-color 0.1s linear;
|
||||
}
|
||||
|
||||
.monaco-tl-twistie,
|
||||
.monaco-tl-contents {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-tl-twistie {
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
padding-right: 6px;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit !important;
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.monaco-tl-contents {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-tl-twistie.collapsed::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.monaco-tl-twistie.codicon-tree-item-loading::before {
|
||||
/* Use steps to throttle FPS to reduce CPU usage */
|
||||
animation: codicon-spin 1.25s steps(30) infinite;
|
||||
}
|
210
lib/vscode/src/vs/base/browser/ui/tree/objectTree.ts
Normal file
210
lib/vscode/src/vs/base/browser/ui/tree/objectTree.ts
Normal file
@ -0,0 +1,210 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { AbstractTree, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree';
|
||||
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
|
||||
import { IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { IList } from 'vs/base/browser/ui/tree/indexTreeModel';
|
||||
|
||||
export interface IObjectTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
|
||||
readonly sorter?: ITreeSorter<T>;
|
||||
}
|
||||
|
||||
export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
|
||||
|
||||
protected model!: IObjectTreeModel<T, TFilterData>;
|
||||
|
||||
get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<T | null, TFilterData>> { return this.model.onDidChangeCollapseState; }
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
renderers: ITreeRenderer<T, TFilterData, any>[],
|
||||
options: IObjectTreeOptions<T, TFilterData> = {}
|
||||
) {
|
||||
super(user, container, delegate, renderers, options as IObjectTreeOptions<T | null, TFilterData>);
|
||||
}
|
||||
|
||||
setChildren(element: T | null, children: Iterable<ITreeElement<T>> = Iterable.empty()): void {
|
||||
this.model.setChildren(element, children);
|
||||
}
|
||||
|
||||
rerender(element?: T): void {
|
||||
if (element === undefined) {
|
||||
this.view.rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.rerender(element);
|
||||
}
|
||||
|
||||
updateElementHeight(element: T, height: number): void {
|
||||
this.model.updateElementHeight(element, height);
|
||||
}
|
||||
|
||||
resort(element: T, recursive = true): void {
|
||||
this.model.resort(element, recursive);
|
||||
}
|
||||
|
||||
hasElement(element: T): boolean {
|
||||
return this.model.has(element);
|
||||
}
|
||||
|
||||
protected createModel(user: string, view: IList<ITreeNode<T, TFilterData>>, options: IObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
return new ObjectTreeModel(user, view, options);
|
||||
}
|
||||
}
|
||||
|
||||
interface ICompressedTreeNodeProvider<T, TFilterData> {
|
||||
getCompressedTreeNode(location: T | null): ITreeNode<ICompressedTreeNode<T> | null, TFilterData>;
|
||||
}
|
||||
|
||||
export interface ICompressibleTreeRenderer<T, TFilterData = void, TTemplateData = void> extends ITreeRenderer<T, TFilterData, TTemplateData> {
|
||||
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<T>, TFilterData>, index: number, templateData: TTemplateData, height: number | undefined): void;
|
||||
disposeCompressedElements?(node: ITreeNode<ICompressedTreeNode<T>, TFilterData>, index: number, templateData: TTemplateData, height: number | undefined): void;
|
||||
}
|
||||
|
||||
interface CompressibleTemplateData<T, TFilterData, TTemplateData> {
|
||||
compressedTreeNode: ITreeNode<ICompressedTreeNode<T>, TFilterData> | undefined;
|
||||
readonly data: TTemplateData;
|
||||
}
|
||||
|
||||
class CompressibleRenderer<T extends NonNullable<any>, TFilterData, TTemplateData> implements ITreeRenderer<T, TFilterData, CompressibleTemplateData<T, TFilterData, TTemplateData>> {
|
||||
|
||||
readonly templateId: string;
|
||||
readonly onDidChangeTwistieState: Event<T> | undefined;
|
||||
|
||||
@memoize
|
||||
private get compressedTreeNodeProvider(): ICompressedTreeNodeProvider<T, TFilterData> {
|
||||
return this._compressedTreeNodeProvider();
|
||||
}
|
||||
|
||||
constructor(private _compressedTreeNodeProvider: () => ICompressedTreeNodeProvider<T, TFilterData>, private renderer: ICompressibleTreeRenderer<T, TFilterData, TTemplateData>) {
|
||||
this.templateId = renderer.templateId;
|
||||
|
||||
if (renderer.onDidChangeTwistieState) {
|
||||
this.onDidChangeTwistieState = renderer.onDidChangeTwistieState;
|
||||
}
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): CompressibleTemplateData<T, TFilterData, TTemplateData> {
|
||||
const data = this.renderer.renderTemplate(container);
|
||||
return { compressedTreeNode: undefined, data };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: CompressibleTemplateData<T, TFilterData, TTemplateData>, height: number | undefined): void {
|
||||
const compressedTreeNode = this.compressedTreeNodeProvider.getCompressedTreeNode(node.element) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
|
||||
|
||||
if (compressedTreeNode.element.elements.length === 1) {
|
||||
templateData.compressedTreeNode = undefined;
|
||||
this.renderer.renderElement(node, index, templateData.data, height);
|
||||
} else {
|
||||
templateData.compressedTreeNode = compressedTreeNode;
|
||||
this.renderer.renderCompressedElements(compressedTreeNode, index, templateData.data, height);
|
||||
}
|
||||
}
|
||||
|
||||
disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: CompressibleTemplateData<T, TFilterData, TTemplateData>, height: number | undefined): void {
|
||||
if (templateData.compressedTreeNode) {
|
||||
if (this.renderer.disposeCompressedElements) {
|
||||
this.renderer.disposeCompressedElements(templateData.compressedTreeNode, index, templateData.data, height);
|
||||
}
|
||||
} else {
|
||||
if (this.renderer.disposeElement) {
|
||||
this.renderer.disposeElement(node, index, templateData.data, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: CompressibleTemplateData<T, TFilterData, TTemplateData>): void {
|
||||
this.renderer.disposeTemplate(templateData.data);
|
||||
}
|
||||
|
||||
renderTwistie?(element: T, twistieElement: HTMLElement): void {
|
||||
if (this.renderer.renderTwistie) {
|
||||
this.renderer.renderTwistie(element, twistieElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ICompressibleKeyboardNavigationLabelProvider<T> extends IKeyboardNavigationLabelProvider<T> {
|
||||
getCompressedNodeKeyboardNavigationLabel(elements: T[]): { toString(): string | undefined; } | undefined;
|
||||
}
|
||||
|
||||
export interface ICompressibleObjectTreeOptions<T, TFilterData = void> extends IObjectTreeOptions<T, TFilterData> {
|
||||
readonly compressionEnabled?: boolean;
|
||||
readonly elementMapper?: ElementMapper<T>;
|
||||
readonly keyboardNavigationLabelProvider?: ICompressibleKeyboardNavigationLabelProvider<T>;
|
||||
}
|
||||
|
||||
function asObjectTreeOptions<T, TFilterData>(compressedTreeNodeProvider: () => ICompressedTreeNodeProvider<T, TFilterData>, options?: ICompressibleObjectTreeOptions<T, TFilterData>): IObjectTreeOptions<T, TFilterData> | undefined {
|
||||
return options && {
|
||||
...options,
|
||||
keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && {
|
||||
getKeyboardNavigationLabel(e: T) {
|
||||
let compressedTreeNode: ITreeNode<ICompressedTreeNode<T>, TFilterData>;
|
||||
|
||||
try {
|
||||
compressedTreeNode = compressedTreeNodeProvider().getCompressedTreeNode(e) as ITreeNode<ICompressedTreeNode<T>, TFilterData>;
|
||||
} catch {
|
||||
return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e);
|
||||
}
|
||||
|
||||
if (compressedTreeNode.element.elements.length === 1) {
|
||||
return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e);
|
||||
} else {
|
||||
return options.keyboardNavigationLabelProvider!.getCompressedNodeKeyboardNavigationLabel(compressedTreeNode.element.elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptionsUpdate {
|
||||
readonly compressionEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class CompressibleObjectTree<T extends NonNullable<any>, TFilterData = void> extends ObjectTree<T, TFilterData> implements ICompressedTreeNodeProvider<T, TFilterData> {
|
||||
|
||||
protected model!: CompressibleObjectTreeModel<T, TFilterData>;
|
||||
|
||||
constructor(
|
||||
user: string,
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<T>,
|
||||
renderers: ICompressibleTreeRenderer<T, TFilterData, any>[],
|
||||
options: ICompressibleObjectTreeOptions<T, TFilterData> = {}
|
||||
) {
|
||||
const compressedTreeNodeProvider = () => this;
|
||||
const compressibleRenderers = renderers.map(r => new CompressibleRenderer<T, TFilterData, any>(compressedTreeNodeProvider, r));
|
||||
super(user, container, delegate, compressibleRenderers, asObjectTreeOptions<T, TFilterData>(compressedTreeNodeProvider, options));
|
||||
}
|
||||
|
||||
setChildren(element: T | null, children: Iterable<ICompressedTreeElement<T>> = Iterable.empty()): void {
|
||||
this.model.setChildren(element, children);
|
||||
}
|
||||
|
||||
protected createModel(user: string, view: IList<ITreeNode<T, TFilterData>>, options: ICompressibleObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
return new CompressibleObjectTreeModel(user, view, options);
|
||||
}
|
||||
|
||||
updateOptions(optionsUpdate: ICompressibleObjectTreeOptionsUpdate = {}): void {
|
||||
super.updateOptions(optionsUpdate);
|
||||
|
||||
if (typeof optionsUpdate.compressionEnabled !== 'undefined') {
|
||||
this.model.setCompressionEnabled(optionsUpdate.compressionEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
getCompressedTreeNode(element: T | null = null): ITreeNode<ICompressedTreeNode<T> | null, TFilterData> {
|
||||
return this.model.getCompressedTreeNode(element);
|
||||
}
|
||||
}
|
305
lib/vscode/src/vs/base/browser/ui/tree/objectTreeModel.ts
Normal file
305
lib/vscode/src/vs/base/browser/ui/tree/objectTreeModel.ts
Normal file
@ -0,0 +1,305 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { IndexTreeModel, IIndexTreeModelOptions, IList } from 'vs/base/browser/ui/tree/indexTreeModel';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ITreeModel, ITreeNode, ITreeElement, ITreeSorter, ICollapseStateChangeEvent, ITreeModelSpliceEvent, TreeError } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
|
||||
export type ITreeNodeCallback<T, TFilterData> = (node: ITreeNode<T, TFilterData>) => void;
|
||||
|
||||
export interface IObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> extends ITreeModel<T | null, TFilterData, T | null> {
|
||||
setChildren(element: T | null, children: Iterable<ITreeElement<T>> | undefined): void;
|
||||
resort(element?: T | null, recursive?: boolean): void;
|
||||
updateElementHeight(element: T, height: number): void;
|
||||
}
|
||||
|
||||
export interface IObjectTreeModelOptions<T, TFilterData> extends IIndexTreeModelOptions<T, TFilterData> {
|
||||
readonly sorter?: ITreeSorter<T>;
|
||||
readonly identityProvider?: IIdentityProvider<T>;
|
||||
}
|
||||
|
||||
export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements IObjectTreeModel<T, TFilterData> {
|
||||
|
||||
readonly rootRef = null;
|
||||
|
||||
private model: IndexTreeModel<T | null, TFilterData>;
|
||||
private nodes = new Map<T | null, ITreeNode<T, TFilterData>>();
|
||||
private readonly nodesByIdentity = new Map<string, ITreeNode<T, TFilterData>>();
|
||||
private readonly identityProvider?: IIdentityProvider<T>;
|
||||
private sorter?: ITreeSorter<{ element: T; }>;
|
||||
|
||||
readonly onDidSplice: Event<ITreeModelSpliceEvent<T | null, TFilterData>>;
|
||||
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
|
||||
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
|
||||
|
||||
get size(): number { return this.nodes.size; }
|
||||
|
||||
constructor(
|
||||
private user: string,
|
||||
list: IList<ITreeNode<T, TFilterData>>,
|
||||
options: IObjectTreeModelOptions<T, TFilterData> = {}
|
||||
) {
|
||||
this.model = new IndexTreeModel(user, list, null, options);
|
||||
this.onDidSplice = this.model.onDidSplice;
|
||||
this.onDidChangeCollapseState = this.model.onDidChangeCollapseState as Event<ICollapseStateChangeEvent<T, TFilterData>>;
|
||||
this.onDidChangeRenderNodeCount = this.model.onDidChangeRenderNodeCount as Event<ITreeNode<T, TFilterData>>;
|
||||
|
||||
if (options.sorter) {
|
||||
this.sorter = {
|
||||
compare(a, b) {
|
||||
return options.sorter!.compare(a.element, b.element);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.identityProvider = options.identityProvider;
|
||||
}
|
||||
|
||||
setChildren(
|
||||
element: T | null,
|
||||
children: Iterable<ITreeElement<T>> = Iterable.empty(),
|
||||
onDidCreateNode?: ITreeNodeCallback<T, TFilterData>,
|
||||
onDidDeleteNode?: ITreeNodeCallback<T, TFilterData>
|
||||
): void {
|
||||
const location = this.getElementLocation(element);
|
||||
this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode);
|
||||
}
|
||||
|
||||
private _setChildren(
|
||||
location: number[],
|
||||
children: Iterable<ITreeElement<T>> = Iterable.empty(),
|
||||
onDidCreateNode?: ITreeNodeCallback<T, TFilterData>,
|
||||
onDidDeleteNode?: ITreeNodeCallback<T, TFilterData>
|
||||
): void {
|
||||
const insertedElements = new Set<T | null>();
|
||||
const insertedElementIds = new Set<string>();
|
||||
|
||||
const _onDidCreateNode = (node: ITreeNode<T | null, TFilterData>) => {
|
||||
if (node.element === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tnode = node as ITreeNode<T, TFilterData>;
|
||||
|
||||
insertedElements.add(tnode.element);
|
||||
this.nodes.set(tnode.element, tnode);
|
||||
|
||||
if (this.identityProvider) {
|
||||
const id = this.identityProvider.getId(tnode.element).toString();
|
||||
insertedElementIds.add(id);
|
||||
this.nodesByIdentity.set(id, tnode);
|
||||
}
|
||||
|
||||
if (onDidCreateNode) {
|
||||
onDidCreateNode(tnode);
|
||||
}
|
||||
};
|
||||
|
||||
const _onDidDeleteNode = (node: ITreeNode<T | null, TFilterData>) => {
|
||||
if (node.element === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tnode = node as ITreeNode<T, TFilterData>;
|
||||
|
||||
if (!insertedElements.has(tnode.element)) {
|
||||
this.nodes.delete(tnode.element);
|
||||
}
|
||||
|
||||
if (this.identityProvider) {
|
||||
const id = this.identityProvider.getId(tnode.element).toString();
|
||||
if (!insertedElementIds.has(id)) {
|
||||
this.nodesByIdentity.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (onDidDeleteNode) {
|
||||
onDidDeleteNode(tnode);
|
||||
}
|
||||
};
|
||||
|
||||
this.model.splice(
|
||||
[...location, 0],
|
||||
Number.MAX_VALUE,
|
||||
children,
|
||||
_onDidCreateNode,
|
||||
_onDidDeleteNode
|
||||
);
|
||||
}
|
||||
|
||||
private preserveCollapseState(elements: Iterable<ITreeElement<T>> = Iterable.empty()): Iterable<ITreeElement<T>> {
|
||||
if (this.sorter) {
|
||||
elements = mergeSort([...elements], this.sorter.compare.bind(this.sorter));
|
||||
}
|
||||
|
||||
return Iterable.map(elements, treeElement => {
|
||||
let node = this.nodes.get(treeElement.element);
|
||||
|
||||
if (!node && this.identityProvider) {
|
||||
const id = this.identityProvider.getId(treeElement.element).toString();
|
||||
node = this.nodesByIdentity.get(id);
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return {
|
||||
...treeElement,
|
||||
children: this.preserveCollapseState(treeElement.children)
|
||||
};
|
||||
}
|
||||
|
||||
const collapsible = typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : node.collapsible;
|
||||
const collapsed = typeof treeElement.collapsed !== 'undefined' ? treeElement.collapsed : node.collapsed;
|
||||
|
||||
return {
|
||||
...treeElement,
|
||||
collapsible,
|
||||
collapsed,
|
||||
children: this.preserveCollapseState(treeElement.children)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
rerender(element: T | null): void {
|
||||
const location = this.getElementLocation(element);
|
||||
this.model.rerender(location);
|
||||
}
|
||||
|
||||
updateElementHeight(element: T, height: number): void {
|
||||
const location = this.getElementLocation(element);
|
||||
this.model.updateElementHeight(location, height);
|
||||
}
|
||||
|
||||
resort(element: T | null = null, recursive = true): void {
|
||||
if (!this.sorter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = this.getElementLocation(element);
|
||||
const node = this.model.getNode(location);
|
||||
|
||||
this._setChildren(location, this.resortChildren(node, recursive));
|
||||
}
|
||||
|
||||
private resortChildren(node: ITreeNode<T | null, TFilterData>, recursive: boolean, first = true): Iterable<ITreeElement<T>> {
|
||||
let childrenNodes = [...node.children] as ITreeNode<T, TFilterData>[];
|
||||
|
||||
if (recursive || first) {
|
||||
childrenNodes = mergeSort(childrenNodes, this.sorter!.compare.bind(this.sorter));
|
||||
}
|
||||
|
||||
return Iterable.map<ITreeNode<T | null, TFilterData>, ITreeElement<T>>(childrenNodes, node => ({
|
||||
element: node.element as T,
|
||||
collapsible: node.collapsible,
|
||||
collapsed: node.collapsed,
|
||||
children: this.resortChildren(node, recursive, false)
|
||||
}));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
has(element: T | null): boolean {
|
||||
return this.nodes.has(element);
|
||||
}
|
||||
|
||||
getListIndex(element: T | null): number {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.getListIndex(location);
|
||||
}
|
||||
|
||||
getListRenderCount(element: T | null): number {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.getListRenderCount(location);
|
||||
}
|
||||
|
||||
isCollapsible(element: T | null): boolean {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.isCollapsible(location);
|
||||
}
|
||||
|
||||
setCollapsible(element: T | null, collapsible?: boolean): boolean {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.setCollapsible(location, collapsible);
|
||||
}
|
||||
|
||||
isCollapsed(element: T | null): boolean {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.isCollapsed(location);
|
||||
}
|
||||
|
||||
setCollapsed(element: T | null, collapsed?: boolean, recursive?: boolean): boolean {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.setCollapsed(location, collapsed, recursive);
|
||||
}
|
||||
|
||||
expandTo(element: T | null): void {
|
||||
const location = this.getElementLocation(element);
|
||||
this.model.expandTo(location);
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
this.model.refilter();
|
||||
}
|
||||
|
||||
getNode(element: T | null = null): ITreeNode<T | null, TFilterData> {
|
||||
if (element === null) {
|
||||
return this.model.getNode(this.model.rootRef);
|
||||
}
|
||||
|
||||
const node = this.nodes.get(element);
|
||||
|
||||
if (!node) {
|
||||
throw new TreeError(this.user, `Tree element not found: ${element}`);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
getNodeLocation(node: ITreeNode<T, TFilterData>): T | null {
|
||||
return node.element;
|
||||
}
|
||||
|
||||
getParentNodeLocation(element: T | null): T | null {
|
||||
if (element === null) {
|
||||
throw new TreeError(this.user, `Invalid getParentNodeLocation call`);
|
||||
}
|
||||
|
||||
const node = this.nodes.get(element);
|
||||
|
||||
if (!node) {
|
||||
throw new TreeError(this.user, `Tree element not found: ${element}`);
|
||||
}
|
||||
|
||||
const location = this.model.getNodeLocation(node);
|
||||
const parentLocation = this.model.getParentNodeLocation(location);
|
||||
const parent = this.model.getNode(parentLocation);
|
||||
|
||||
return parent.element;
|
||||
}
|
||||
|
||||
private getElementLocation(element: T | null): number[] {
|
||||
if (element === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const node = this.nodes.get(element);
|
||||
|
||||
if (!node) {
|
||||
throw new TreeError(this.user, `Tree element not found: ${element}`);
|
||||
}
|
||||
|
||||
return this.model.getNodeLocation(node);
|
||||
}
|
||||
}
|
221
lib/vscode/src/vs/base/browser/ui/tree/tree.ts
Normal file
221
lib/vscode/src/vs/base/browser/ui/tree/tree.ts
Normal file
@ -0,0 +1,221 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IListRenderer, IListDragOverReaction, IListDragAndDrop, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
|
||||
import { IDragAndDropData } from 'vs/base/browser/dnd';
|
||||
|
||||
export const enum TreeVisibility {
|
||||
|
||||
/**
|
||||
* The tree node should be hidden.
|
||||
*/
|
||||
Hidden,
|
||||
|
||||
/**
|
||||
* The tree node should be visible.
|
||||
*/
|
||||
Visible,
|
||||
|
||||
/**
|
||||
* The tree node should be visible if any of its descendants is visible.
|
||||
*/
|
||||
Recurse
|
||||
}
|
||||
|
||||
/**
|
||||
* A composed filter result containing the visibility result as well as
|
||||
* metadata.
|
||||
*/
|
||||
export interface ITreeFilterDataResult<TFilterData> {
|
||||
|
||||
/**
|
||||
* Whether the node should be visible.
|
||||
*/
|
||||
visibility: boolean | TreeVisibility;
|
||||
|
||||
/**
|
||||
* Metadata about the element's visibility which gets forwarded to the
|
||||
* renderer once the element gets rendered.
|
||||
*/
|
||||
data: TFilterData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
}
|
||||
|
||||
export interface ITreeSorter<T> {
|
||||
compare(element: T, otherElement: T): number;
|
||||
}
|
||||
|
||||
export interface ITreeElement<T> {
|
||||
readonly element: T;
|
||||
readonly children?: Iterable<ITreeElement<T>>;
|
||||
readonly collapsible?: boolean;
|
||||
readonly collapsed?: boolean;
|
||||
}
|
||||
|
||||
export interface ITreeNode<T, TFilterData = void> {
|
||||
readonly element: T;
|
||||
readonly children: ITreeNode<T, TFilterData>[];
|
||||
readonly depth: number;
|
||||
readonly visibleChildrenCount: number;
|
||||
readonly visibleChildIndex: number;
|
||||
readonly collapsible: boolean;
|
||||
readonly collapsed: boolean;
|
||||
readonly visible: boolean;
|
||||
readonly filterData: TFilterData | undefined;
|
||||
}
|
||||
|
||||
export interface ICollapseStateChangeEvent<T, TFilterData> {
|
||||
node: ITreeNode<T, TFilterData>;
|
||||
deep: boolean;
|
||||
}
|
||||
|
||||
export interface ITreeModelSpliceEvent<T, TFilterData> {
|
||||
insertedNodes: ITreeNode<T, TFilterData>[];
|
||||
deletedNodes: ITreeNode<T, TFilterData>[];
|
||||
}
|
||||
|
||||
export interface ITreeModel<T, TFilterData, TRef> {
|
||||
readonly rootRef: TRef;
|
||||
|
||||
readonly onDidSplice: Event<ITreeModelSpliceEvent<T, TFilterData>>;
|
||||
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
|
||||
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
|
||||
|
||||
has(location: TRef): boolean;
|
||||
|
||||
getListIndex(location: TRef): number;
|
||||
getListRenderCount(location: TRef): number;
|
||||
getNode(location?: TRef): ITreeNode<T, any>;
|
||||
getNodeLocation(node: ITreeNode<T, any>): TRef;
|
||||
getParentNodeLocation(location: TRef): TRef | undefined;
|
||||
|
||||
getFirstElementChild(location: TRef): T | undefined;
|
||||
getLastElementAncestor(location?: TRef): T | undefined;
|
||||
|
||||
isCollapsible(location: TRef): boolean;
|
||||
setCollapsible(location: TRef, collapsible?: boolean): boolean;
|
||||
isCollapsed(location: TRef): boolean;
|
||||
setCollapsed(location: TRef, collapsed?: boolean, recursive?: boolean): boolean;
|
||||
expandTo(location: TRef): void;
|
||||
|
||||
rerender(location: TRef): void;
|
||||
refilter(): void;
|
||||
}
|
||||
|
||||
export interface ITreeRenderer<T, TFilterData = void, TTemplateData = void> extends IListRenderer<ITreeNode<T, TFilterData>, TTemplateData> {
|
||||
renderTwistie?(element: T, twistieElement: HTMLElement): void;
|
||||
onDidChangeTwistieState?: Event<T>;
|
||||
}
|
||||
|
||||
export interface ITreeEvent<T> {
|
||||
elements: T[];
|
||||
browserEvent?: UIEvent;
|
||||
}
|
||||
|
||||
export enum TreeMouseEventTarget {
|
||||
Unknown,
|
||||
Twistie,
|
||||
Element
|
||||
}
|
||||
|
||||
export interface ITreeMouseEvent<T> {
|
||||
browserEvent: MouseEvent;
|
||||
element: T | null;
|
||||
target: TreeMouseEventTarget;
|
||||
}
|
||||
|
||||
export interface ITreeContextMenuEvent<T> {
|
||||
browserEvent: UIEvent;
|
||||
element: T | null;
|
||||
anchor: HTMLElement | { x: number; y: number; };
|
||||
}
|
||||
|
||||
export interface ITreeNavigator<T> {
|
||||
current(): T | null;
|
||||
previous(): T | null;
|
||||
first(): T | null;
|
||||
last(): T | null;
|
||||
next(): T | null;
|
||||
}
|
||||
|
||||
export interface IDataSource<TInput, T> {
|
||||
hasChildren?(element: TInput | T): boolean;
|
||||
getChildren(element: TInput | T): Iterable<T>;
|
||||
}
|
||||
|
||||
export interface IAsyncDataSource<TInput, T> {
|
||||
hasChildren(element: TInput | T): boolean;
|
||||
getChildren(element: TInput | T): Iterable<T> | Promise<Iterable<T>>;
|
||||
}
|
||||
|
||||
export const enum TreeDragOverBubble {
|
||||
Down,
|
||||
Up
|
||||
}
|
||||
|
||||
export interface ITreeDragOverReaction extends IListDragOverReaction {
|
||||
bubble?: TreeDragOverBubble;
|
||||
autoExpand?: boolean;
|
||||
}
|
||||
|
||||
export const TreeDragOverReactions = {
|
||||
acceptBubbleUp(): ITreeDragOverReaction { return { accept: true, bubble: TreeDragOverBubble.Up }; },
|
||||
acceptBubbleDown(autoExpand = false): ITreeDragOverReaction { return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand }; },
|
||||
acceptCopyBubbleUp(): ITreeDragOverReaction { return { accept: true, bubble: TreeDragOverBubble.Up, effect: ListDragOverEffect.Copy }; },
|
||||
acceptCopyBubbleDown(autoExpand = false): ITreeDragOverReaction { return { accept: true, bubble: TreeDragOverBubble.Down, effect: ListDragOverEffect.Copy, autoExpand }; }
|
||||
};
|
||||
|
||||
export interface ITreeDragAndDrop<T> extends IListDragAndDrop<T> {
|
||||
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction;
|
||||
}
|
||||
|
||||
export class TreeError extends Error {
|
||||
|
||||
constructor(user: string, message: string) {
|
||||
super(`TreeError [${user}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class WeakMapper<K extends object, V> {
|
||||
|
||||
constructor(private fn: (k: K) => V) { }
|
||||
|
||||
private _map = new WeakMap<K, V>();
|
||||
|
||||
map(key: K): V {
|
||||
let result = this._map.get(key);
|
||||
|
||||
if (!result) {
|
||||
result = this.fn(key);
|
||||
this._map.set(key, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
23
lib/vscode/src/vs/base/browser/ui/tree/treeDefaults.ts
Normal file
23
lib/vscode/src/vs/base/browser/ui/tree/treeDefaults.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree';
|
||||
|
||||
export class CollapseAllAction<TInput, T, TFilterData = void> extends Action {
|
||||
|
||||
constructor(private viewer: AsyncDataTree<TInput, T, TFilterData>, enabled: boolean) {
|
||||
super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'collapse-all', enabled);
|
||||
}
|
||||
|
||||
async run(): Promise<any> {
|
||||
this.viewer.collapseAll();
|
||||
this.viewer.setSelection([]);
|
||||
this.viewer.setFocus([]);
|
||||
this.viewer.domFocus();
|
||||
this.viewer.focusFirst();
|
||||
}
|
||||
}
|
14
lib/vscode/src/vs/base/browser/ui/tree/treeIcons.ts
Normal file
14
lib/vscode/src/vs/base/browser/ui/tree/treeIcons.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
|
||||
export const treeItemExpandedIcon = registerIcon('tree-item-expanded', Codicon.chevronDown); // collapsed is done with rotation
|
||||
|
||||
export const treeFilterOnTypeOnIcon = registerIcon('tree-filter-on-type-on', Codicon.listFilter);
|
||||
export const treeFilterOnTypeOffIcon = registerIcon('tree-filter-on-type-off', Codicon.listSelection);
|
||||
export const treeFilterClearIcon = registerIcon('tree-filter-clear', Codicon.close);
|
||||
|
||||
export const treeItemLoadingIcon = registerIcon('tree-item-loading', Codicon.loading);
|
Reference in New Issue
Block a user