Vscode merge (#4582)

* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd

* fix issues with merges

* bump node version in azpipe

* replace license headers

* remove duplicate launch task

* fix build errors

* fix build errors

* fix tslint issues

* working through package and linux build issues

* more work

* wip

* fix packaged builds

* working through linux build errors

* wip

* wip

* wip

* fix mac and linux file limits

* iterate linux pipeline

* disable editor typing

* revert series to parallel

* remove optimize vscode from linux

* fix linting issues

* revert testing change

* add work round for new node

* readd packaging for extensions

* fix issue with angular not resolving decorator dependencies
This commit is contained in:
Anthony Dresser
2019-03-19 17:44:35 -07:00
committed by GitHub
parent 833d197412
commit 87765e8673
1879 changed files with 54505 additions and 38058 deletions

View File

@@ -5,16 +5,16 @@
import 'vs/css!./media/tree';
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IListOptions, List, IListStyles, mightProducePrintableCharacter } from 'vs/base/browser/ui/list/listWidget';
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
import { IListOptions, List, IListStyles, mightProducePrintableCharacter, MouseController } from 'vs/base/browser/ui/list/listWidget';
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { append, $, toggleClass, getDomNodePagePosition, removeClass, addClass, hasClass } from 'vs/base/browser/dom';
import { Event, Relay, Emitter, EventBufferer } from 'vs/base/common/event';
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree';
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
import { ISpliceable } from 'vs/base/common/sequence';
import { IDragAndDropData, StaticDND, DragAndDropData } from 'vs/base/browser/dnd';
import { range } from 'vs/base/common/arrays';
import { range, equals } from 'vs/base/common/arrays';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { domEvent } from 'vs/base/browser/event';
import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters';
@@ -150,7 +150,15 @@ function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T,
return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(node.element);
}
},
enableKeyboardNavigation: options.simpleKeyboardNavigation
enableKeyboardNavigation: options.simpleKeyboardNavigation,
ariaSetProvider: {
getSetSize(node) {
return node.parent!.visibleChildrenCount;
},
getPosInSet(node) {
return node.visibleChildIndex + 1;
}
}
};
}
@@ -188,7 +196,7 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
readonly templateId: string;
private renderedElements = new Map<T, ITreeNode<T, TFilterData>>();
private renderedNodes = new Map<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>>();
private indent: number;
private indent: number = TreeRenderer.DefaultIndent;
private disposables: IDisposable[] = [];
constructor(
@@ -207,7 +215,9 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
}
updateOptions(options: ITreeRendererOptions = {}): void {
this.indent = typeof options.indent === 'number' ? clamp(options.indent, 0, 20) : TreeRenderer.DefaultIndent;
if (typeof options.indent !== 'undefined') {
this.indent = clamp(options.indent, 0, 40);
}
this.renderedNodes.forEach((templateData, node) => {
templateData.twistie.style.marginLeft = `${node.depth * this.indent}px`;
@@ -223,25 +233,28 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
return { container, twistie, templateData };
}
renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
this.renderedNodes.set(node, templateData);
this.renderedElements.set(node.element, node);
renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
if (!dynamicHeightProbing) {
this.renderedNodes.set(node, templateData);
this.renderedElements.set(node.element, node);
}
const indent = TreeRenderer.DefaultIndent + (node.depth - 1) * this.indent;
templateData.twistie.style.marginLeft = `${indent}px`;
templateData.container.setAttribute('aria-posinset', String(node.visibleChildIndex + 1));
templateData.container.setAttribute('aria-setsize', String(node.parent!.visibleChildrenCount));
this.update(node, templateData);
this.renderer.renderElement(node, index, templateData.templateData);
this.renderer.renderElement(node, index, templateData.templateData, dynamicHeightProbing);
}
disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
if (this.renderer.disposeElement) {
this.renderer.disposeElement(node, index, templateData.templateData);
this.renderer.disposeElement(node, index, templateData.templateData, dynamicHeightProbing);
}
if (!dynamicHeightProbing) {
this.renderedNodes.delete(node);
this.renderedElements.delete(node.element);
}
this.renderedNodes.delete(node);
this.renderedElements.set(node.element);
}
disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
@@ -386,6 +399,15 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
private _pattern = '';
get pattern(): string { return this._pattern; }
private _filterOnType: boolean;
get filterOnType(): boolean { return this._filterOnType; }
private _empty: boolean;
get empty(): boolean { return this._empty; }
private _onDidChangeEmptyState = new Emitter<boolean>();
readonly onDidChangeEmptyState: Event<boolean> = Event.latch(this._onDidChangeEmptyState.event);
private positionClassName = 'ne';
private domNode: HTMLElement;
private messageDomNode: HTMLElement;
@@ -394,9 +416,12 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
private clearDomNode: HTMLElement;
private keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
private automaticKeyboardNavigation: boolean;
private automaticKeyboardNavigation = true;
private triggered = false;
private _onDidChangePattern = new Emitter<string>();
readonly onDidChangePattern = this._onDidChangePattern.event;
private enabledDisposables: IDisposable[] = [];
private disposables: IDisposable[] = [];
@@ -416,9 +441,10 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
this.labelDomNode = append(this.domNode, $('span.label'));
const controls = append(this.domNode, $('.controls'));
this._filterOnType = !!tree.options.filterOnType;
this.filterOnTypeDomNode = append(controls, $<HTMLInputElement>('input.filter'));
this.filterOnTypeDomNode.type = 'checkbox';
this.filterOnTypeDomNode.checked = !!tree.options.filterOnType;
this.filterOnTypeDomNode.checked = this._filterOnType;
this.filterOnTypeDomNode.tabIndex = -1;
this.updateFilterOnTypeTitle();
domEvent(this.filterOnTypeDomNode, 'input')(this.onDidChangeFilterOnType, this, this.disposables);
@@ -440,8 +466,15 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
this.enable();
}
this.filterOnTypeDomNode.checked = !!options.filterOnType;
this.automaticKeyboardNavigation = typeof options.automaticKeyboardNavigation === 'undefined' ? true : options.automaticKeyboardNavigation;
if (typeof options.filterOnType !== 'undefined') {
this._filterOnType = !!options.filterOnType;
this.filterOnTypeDomNode.checked = this._filterOnType;
}
if (typeof options.automaticKeyboardNavigation !== 'undefined') {
this.automaticKeyboardNavigation = options.automaticKeyboardNavigation;
}
this.tree.refilter();
this.render();
@@ -466,10 +499,10 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
const isPrintableCharEvent = this.keyboardNavigationLabelProvider.mightProducePrintableCharacter ? (e: IKeyboardEvent) => this.keyboardNavigationLabelProvider.mightProducePrintableCharacter!(e) : (e: IKeyboardEvent) => mightProducePrintableCharacter(e);
const onKeyDown = Event.chain(domEvent(this.view.getHTMLElement(), 'keydown'))
.filter(e => !isInputElement(e.target as HTMLElement) || e.target === this.filterOnTypeDomNode)
.map(e => new StandardKeyboardEvent(e))
.filter(this.keyboardNavigationEventFilter || (() => true))
.filter(() => this.automaticKeyboardNavigation || this.triggered)
.map(e => new StandardKeyboardEvent(e))
.filter(e => isPrintableCharEvent(e) || ((this._pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey))))
.filter(e => isPrintableCharEvent(e) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey) && !e.shiftKey)))
.forEach(e => { e.stopPropagation(); e.preventDefault(); })
.event;
@@ -521,9 +554,14 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
}
this._pattern = pattern;
this._onDidChangePattern.fire(pattern);
this.filter.pattern = pattern;
this.tree.refilter();
this.tree.focusNext(0, true);
if (pattern) {
this.tree.focusNext(0, true, undefined, node => !FuzzyScore.isDefault(node.filterData as any as FuzzyScore));
}
const focus = this.tree.getFocus();
@@ -565,6 +603,8 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
};
const onDragOver = (event: DragEvent) => {
event.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
const x = event.screenX - left;
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'none';
@@ -619,7 +659,7 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
}
private updateFilterOnTypeTitle(): void {
if (this.filterOnTypeDomNode.checked) {
if (this.filterOnType) {
this.filterOnTypeDomNode.title = localize('disable filter on type', "Disable Filter on Type");
} else {
this.filterOnTypeDomNode.title = localize('enable filter on type', "Enable Filter on Type");
@@ -631,17 +671,34 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
if (this.pattern && this.tree.options.filterOnType && noMatches) {
this.messageDomNode.textContent = localize('empty', "No elements found");
this._empty = true;
} else {
this.messageDomNode.innerHTML = '';
this._empty = false;
}
toggleClass(this.domNode, 'no-matches', noMatches);
this.domNode.title = localize('found', "Matched {0} out of {1} elements", this.filter.matchCount, this.filter.totalCount);
this.labelDomNode.textContent = this.pattern.length > 16 ? '…' + this.pattern.substr(this.pattern.length - 16) : this.pattern;
this._onDidChangeEmptyState.fire(this._empty);
}
shouldAllowFocus(node: ITreeNode<T, TFilterData>): boolean {
if (!this.enabled || !this.pattern || this.filterOnType) {
return true;
}
if (this.filter.totalCount > 0 && this.filter.matchCount <= 1) {
return true;
}
return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore);
}
dispose() {
this.disable();
this._onDidChangePattern.dispose();
this.disposables = dispose(this.disposables);
}
}
@@ -673,7 +730,7 @@ function asTreeContextMenuEvent<T>(event: IListContextMenuEvent<ITreeNode<T, any
}
export interface IKeyboardNavigationEventFilter {
(e: KeyboardEvent): boolean;
(e: StandardKeyboardEvent): boolean;
}
export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
@@ -689,6 +746,12 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
readonly dnd?: ITreeDragAndDrop<T>;
readonly autoExpandSingleChildren?: boolean;
readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
readonly expandOnlyOnTwistieClick?: boolean | ((e: T) => boolean);
}
function dfs<T, TFilterData>(node: ITreeNode<T, TFilterData>, fn: (node: ITreeNode<T, TFilterData>) => void): void {
fn(node);
node.children.forEach(child => dfs(child, fn));
}
/**
@@ -706,17 +769,19 @@ class Trait<T> {
private _nodeSet: Set<ITreeNode<T, any>> | undefined;
private get nodeSet(): Set<ITreeNode<T, any>> {
if (!this._nodeSet) {
this._nodeSet = new Set();
for (const node of this.nodes) {
this._nodeSet.add(node);
}
this._nodeSet = this.createNodeSet();
}
return this._nodeSet;
}
constructor(private identityProvider?: IIdentityProvider<T>) { }
set(nodes: ITreeNode<T, any>[], browserEvent?: UIEvent): void {
if (equals(this.nodes, nodes)) {
return;
}
this.nodes = [...nodes];
this.elements = undefined;
this._nodeSet = undefined;
@@ -737,27 +802,101 @@ class Trait<T> {
return this.nodeSet.has(node);
}
remove(nodes: ITreeNode<T, any>[]): void {
if (nodes.length === 0) {
onDidModelSplice({ insertedNodes, deletedNodes }: ITreeModelSpliceEvent<T, any>): void {
if (!this.identityProvider) {
const set = this.createNodeSet();
const visit = node => set.delete(node);
deletedNodes.forEach(node => dfs(node, visit));
this.set(values(set));
return;
}
const set = this.nodeSet;
const visit = (node: ITreeNode<T, any>) => {
set.delete(node);
node.children.forEach(visit);
};
const identityProvider = this.identityProvider;
const nodesByIdentity = new Map<string, ITreeNode<T, any>>();
this.nodes.forEach(node => nodesByIdentity.set(identityProvider.getId(node.element).toString(), node));
nodes.forEach(visit);
this.set(values(set));
const toDeleteByIdentity = new Map<string, ITreeNode<T, any>>();
const toRemoveSetter = node => toDeleteByIdentity.set(identityProvider.getId(node.element).toString(), node);
const toRemoveDeleter = node => toDeleteByIdentity.delete(identityProvider.getId(node.element).toString());
deletedNodes.forEach(node => dfs(node, toRemoveSetter));
insertedNodes.forEach(node => dfs(node, toRemoveDeleter));
toDeleteByIdentity.forEach((_, id) => nodesByIdentity.delete(id));
this.set(values(nodesByIdentity));
}
private createNodeSet(): Set<ITreeNode<T, any>> {
const set = new Set<ITreeNode<T, any>>();
for (const node of this.nodes) {
set.add(node);
}
return set;
}
}
class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<ITreeNode<T, TFilterData>> {
constructor(list: TreeNodeList<T, TFilterData, TRef>, private tree: AbstractTree<T, TFilterData, TRef>) {
super(list);
}
protected onPointer(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
if (isInputElement(e.browserEvent.target as HTMLElement)) {
return;
}
const node = e.element;
if (!node) {
return super.onPointer(e);
}
if (this.isSelectionRangeChangeEvent(e) || this.isSelectionSingleChangeEvent(e)) {
return super.onPointer(e);
}
const onTwistie = hasClass(e.browserEvent.target as HTMLElement, 'monaco-tl-twistie');
if (!this.tree.openOnSingleClick && e.browserEvent.detail !== 2 && !onTwistie) {
return super.onPointer(e);
}
let expandOnlyOnTwistieClick = false;
if (typeof this.tree.expandOnlyOnTwistieClick === 'function') {
expandOnlyOnTwistieClick = this.tree.expandOnlyOnTwistieClick(node.element);
} else {
expandOnlyOnTwistieClick = !!this.tree.expandOnlyOnTwistieClick;
}
if (expandOnlyOnTwistieClick && !onTwistie) {
return super.onPointer(e);
}
const model = ((this.tree as any).model as ITreeModel<T, TFilterData, TRef>); // internal
const location = model.getNodeLocation(node);
const recursive = e.browserEvent.altKey;
model.setCollapsed(location, undefined, recursive);
if (expandOnlyOnTwistieClick && onTwistie) {
return;
}
super.onPointer(e);
}
}
interface ITreeNodeListOptions<T, TFilterData, TRef> extends IListOptions<ITreeNode<T, TFilterData>> {
readonly tree: AbstractTree<T, TFilterData, TRef>;
}
/**
* We use this List subclass to restore selection and focus as nodes
* get rendered in the list, possibly due to a node expand() call.
*/
class TreeNodeList<T, TFilterData> extends List<ITreeNode<T, TFilterData>> {
class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>> {
constructor(
container: HTMLElement,
@@ -765,11 +904,15 @@ class TreeNodeList<T, TFilterData> extends List<ITreeNode<T, TFilterData>> {
renderers: IListRenderer<any /* TODO@joao */, any>[],
private focusTrait: Trait<T>,
private selectionTrait: Trait<T>,
options?: IListOptions<ITreeNode<T, TFilterData>>
options: ITreeNodeListOptions<T, TFilterData, TRef>
) {
super(container, virtualDelegate, renderers, options);
}
protected createMouseController(options: ITreeNodeListOptions<T, TFilterData, TRef>): MouseController<ITreeNode<T, TFilterData>> {
return new TreeNodeListMouseController(this, options.tree);
}
splice(start: number, deleteCount: number, elements: ITreeNode<T, TFilterData>[] = []): void {
super.splice(start, deleteCount, elements);
@@ -818,20 +961,20 @@ class TreeNodeList<T, TFilterData> extends List<ITreeNode<T, TFilterData>> {
export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
private view: TreeNodeList<T, TFilterData>;
protected view: TreeNodeList<T, TFilterData, TRef>;
private renderers: TreeRenderer<T, TFilterData, any>[];
private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
protected model: ITreeModel<T, TFilterData, TRef>;
private focus = new Trait<T>();
private selection = new Trait<T>();
private focus: Trait<T>;
private selection: Trait<T>;
private eventBufferer = new EventBufferer();
private typeFilterController?: TypeFilterController<T, TFilterData>;
private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
protected disposables: IDisposable[] = [];
get onDidScroll(): Event<void> { return this.view.onDidScroll; }
readonly onDidChangeFocus: Event<ITreeEvent<T>> = this.eventBufferer.wrapEvent(this.focus.onDidChange);
readonly onDidChangeSelection: Event<ITreeEvent<T>> = this.eventBufferer.wrapEvent(this.selection.onDidChange);
get onDidChangeFocus(): Event<ITreeEvent<T>> { return this.eventBufferer.wrapEvent(this.focus.onDidChange); }
get onDidChangeSelection(): Event<ITreeEvent<T>> { return this.eventBufferer.wrapEvent(this.selection.onDidChange); }
get onDidOpen(): Event<ITreeEvent<T>> { return Event.map(this.view.onDidOpen, asTreeEvent); }
get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); }
@@ -852,7 +995,14 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
readonly onWillRefilter: Event<void> = this._onWillRefilter.event;
get filterOnType(): boolean { return !!this._options.filterOnType; }
get onDidChangeTypeFilterPattern(): Event<string> { return this.typeFilterController ? this.typeFilterController.onDidChangePattern : Event.None; }
// Options TODO@joao expose options only, not Optional<>
get openOnSingleClick(): boolean { return typeof this._options.openOnSingleClick === 'undefined' ? true : this._options.openOnSingleClick; }
get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? false : this._options.expandOnlyOnTwistieClick; }
private _onDidUpdateOptions = new Emitter<IAbstractTreeOptions<T, TFilterData>>();
readonly onDidUpdateOptions: Event<IAbstractTreeOptions<T, TFilterData>> = this._onDidUpdateOptions.event;
get onDidDispose(): Event<void> { return this.view.onDidDispose; }
@@ -871,49 +1021,22 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
let filter: TypeFilter<T> | undefined;
if (_options.keyboardNavigationLabelProvider) {
filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as ITreeFilter<T, FuzzyScore>);
filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as any as ITreeFilter<T, FuzzyScore>);
_options = { ..._options, filter: filter as ITreeFilter<T, TFilterData> }; // TODO need typescript help here
this.disposables.push(filter);
}
this.view = new TreeNodeList(container, treeDelegate, this.renderers, this.focus, this.selection, asListOptions(() => this.model, _options));
this.focus = new Trait(_options.identityProvider);
this.selection = new Trait(_options.identityProvider);
this.view = new TreeNodeList(container, treeDelegate, this.renderers, this.focus, this.selection, { ...asListOptions(() => this.model, _options), tree: this });
this.model = this.createModel(this.view, _options);
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
if (this.options.identityProvider) {
const identityProvider = this.options.identityProvider;
this.model.onDidSplice(e => {
if (e.deletedNodes.length === 0) {
return;
}
this.eventBufferer.bufferEvents(() => {
const map = new Map<string, ITreeNode<T, TFilterData>>();
for (const node of e.deletedNodes) {
map.set(identityProvider.getId(node.element).toString(), node);
}
for (const node of e.insertedNodes) {
map.delete(identityProvider.getId(node.element).toString());
}
if (map.size === 0) {
return;
}
const deletedNodes = values(map);
this.focus.remove(deletedNodes);
this.selection.remove(deletedNodes);
});
}, null, this.disposables);
}
this.view.onTap(this.reactOnMouseClick, this, this.disposables);
this.view.onMouseClick(this.reactOnMouseClick, this, this.disposables);
this.model.onDidSplice(e => {
this.focus.onDidModelSplice(e);
this.selection.onDidModelSplice(e);
}, null, this.disposables);
if (_options.keyboardSupport !== false) {
const onKeyDown = Event.chain(this.view.onKeyDown)
@@ -927,17 +1050,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
if (_options.keyboardNavigationLabelProvider) {
this.typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, _options.keyboardNavigationLabelProvider);
this.focusNavigationFilter = node => {
if (!this.typeFilterController!.enabled || !this.typeFilterController!.pattern) {
return true;
}
if (filter!.totalCount > 0 && filter!.matchCount <= 1) {
return true;
}
return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore);
};
this.focusNavigationFilter = node => this.typeFilterController!.shouldAllowFocus(node);
this.disposables.push(this.typeFilterController!);
}
}
@@ -949,11 +1062,16 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
renderer.updateOptions(optionsUpdate);
}
this.view.updateOptions({ enableKeyboardNavigation: this._options.simpleKeyboardNavigation });
this.view.updateOptions({
enableKeyboardNavigation: this._options.simpleKeyboardNavigation,
automaticKeyboardNavigation: this._options.automaticKeyboardNavigation
});
if (this.typeFilterController) {
this.typeFilterController.updateOptions(this._options);
}
this._onDidUpdateOptions.fire(this._options);
}
get options(): IAbstractTreeOptions<T, TFilterData> {
@@ -977,11 +1095,21 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
}
get contentHeight(): number {
if (this.typeFilterController && this.typeFilterController.filterOnType && this.typeFilterController.empty) {
return 100;
}
return this.view.contentHeight;
}
get onDidChangeContentHeight(): Event<number> {
return this.view.onDidChangeContentHeight;
let result = this.view.onDidChangeContentHeight;
if (this.typeFilterController) {
result = Event.any(result, Event.map(this.typeFilterController.onDidChangeEmptyState, () => this.contentHeight));
}
return result;
}
get scrollTop(): number {
@@ -1000,6 +1128,18 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
return this.view.renderHeight;
}
get firstVisibleElement(): T {
const index = this.view.firstVisibleIndex;
const node = this.view.element(index);
return node.element;
}
get lastVisibleElement(): T {
const index = this.view.lastVisibleIndex;
const node = this.view.element(index);
return node.element;
}
domFocus(): void {
this.view.domFocus();
}
@@ -1061,11 +1201,11 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
}
toggleKeyboardNavigation(): void {
if (!this.typeFilterController) {
return;
}
this.view.toggleKeyboardNavigation();
this.typeFilterController.toggle();
if (this.typeFilterController) {
this.typeFilterController.toggle();
}
}
refilter(): void {
@@ -1093,40 +1233,42 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
this.view.setFocus(indexes, browserEvent, true);
}
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
this.view.focusNext(n, loop, browserEvent, this.focusNavigationFilter);
focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
this.view.focusNext(n, loop, browserEvent, filter);
}
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void {
this.view.focusPrevious(n, loop, browserEvent, this.focusNavigationFilter);
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
this.view.focusPrevious(n, loop, browserEvent, filter);
}
focusNextPage(browserEvent?: UIEvent): void {
this.view.focusNextPage(browserEvent, this.focusNavigationFilter);
focusNextPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
this.view.focusNextPage(browserEvent, filter);
}
focusPreviousPage(browserEvent?: UIEvent): void {
this.view.focusPreviousPage(browserEvent, this.focusNavigationFilter);
focusPreviousPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
this.view.focusPreviousPage(browserEvent, filter);
}
focusLast(browserEvent?: UIEvent): void {
this.view.focusLast(browserEvent, this.focusNavigationFilter);
focusLast(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
this.view.focusLast(browserEvent, filter);
}
focusFirst(browserEvent?: UIEvent): void {
this.view.focusFirst(browserEvent, this.focusNavigationFilter);
focusFirst(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
this.view.focusFirst(browserEvent, filter);
}
getFocus(): T[] {
return this.focus.get();
}
open(elements: TRef[]): void {
open(elements: TRef[], browserEvent?: UIEvent): void {
const indexes = elements.map(e => this.model.getListIndex(e));
this.view.open(indexes);
this.view.open(indexes, browserEvent);
}
reveal(location: TRef, relativeTop?: number): void {
this.model.expandTo(location);
const index = this.model.getListIndex(location);
if (index === -1) {
@@ -1152,37 +1294,6 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
// List
get visibleNodeCount(): number {
return this.view.length;
}
private reactOnMouseClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
if (isInputElement(e.browserEvent.target as HTMLElement)) {
return;
}
const node = e.element;
if (!node) {
return;
}
if (this.view.multipleSelectionController.isSelectionRangeChangeEvent(e) || this.view.multipleSelectionController.isSelectionSingleChangeEvent(e)) {
return;
}
const onTwistie = hasClass(e.browserEvent.target as HTMLElement, 'monaco-tl-twistie');
if (!this.openOnSingleClick && e.browserEvent.detail !== 2 && !onTwistie) {
return;
}
const location = this.model.getNodeLocation(node);
const recursive = e.browserEvent.altKey;
this.model.setCollapsed(location, undefined, recursive);
}
private onLeftArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();

View File

@@ -9,7 +9,7 @@ import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOve
import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop } from 'vs/base/browser/ui/tree/tree';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { timeout, always, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { timeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { Iterator } from 'vs/base/common/iterator';
import { IDragAndDropData } from 'vs/base/browser/dnd';
@@ -17,24 +17,35 @@ import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
import { toggleClass } from 'vs/base/browser/dom';
const enum AsyncDataTreeNodeState {
Uninitialized = 'uninitialized',
Loaded = 'loaded',
Loading = 'loading'
}
interface IAsyncDataTreeNode<TInput, T> {
element: TInput | T;
readonly parent: IAsyncDataTreeNode<TInput, T> | null;
readonly children: IAsyncDataTreeNode<TInput, T>[];
readonly id?: string | null;
state: AsyncDataTreeNodeState;
loading: boolean;
hasChildren: boolean;
needsRefresh: boolean;
stale: boolean;
slow: boolean;
disposed: boolean;
}
interface IAsyncDataTreeNodeRequiredProps<TInput, T> extends Partial<IAsyncDataTreeNode<TInput, T>> {
readonly element: TInput | T;
readonly parent: IAsyncDataTreeNode<TInput, T> | null;
readonly hasChildren: boolean;
}
function createAsyncDataTreeNode<TInput, T>(props: IAsyncDataTreeNodeRequiredProps<TInput, T>): IAsyncDataTreeNode<TInput, T> {
return {
...props,
children: [],
loading: false,
stale: true,
disposed: false,
slow: false
};
}
function isAncestor<TInput, T>(ancestor: IAsyncDataTreeNode<TInput, T>, descendant: IAsyncDataTreeNode<TInput, T>): boolean {
if (!descendant.parent) {
return false;
@@ -87,8 +98,8 @@ class DataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ITreeRe
return { templateData };
}
renderElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
this.renderer.renderElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData);
renderElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
this.renderer.renderElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData, dynamicHeightProbing);
}
renderTwistie(element: IAsyncDataTreeNode<TInput, T>, twistieElement: HTMLElement): boolean {
@@ -96,9 +107,9 @@ class DataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ITreeRe
return false;
}
disposeElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
disposeElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
if (this.renderer.disposeElement) {
this.renderer.disposeElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData);
this.renderer.disposeElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData, dynamicHeightProbing);
}
}
@@ -217,20 +228,26 @@ function asObjectTreeOptions<TInput, T, TFilterData>(options?: IAsyncDataTreeOpt
return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e.element as T);
}
},
sorter: undefined
sorter: undefined,
expandOnlyOnTwistieClick: typeof options.expandOnlyOnTwistieClick === 'undefined' ? undefined : (
typeof options.expandOnlyOnTwistieClick !== 'function' ? options.expandOnlyOnTwistieClick : (
e => (options.expandOnlyOnTwistieClick as ((e: T) => boolean))(e.element as T)
)
),
ariaSetProvider: undefined
};
}
function asTreeElement<TInput, T>(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ITreeElement<IAsyncDataTreeNode<TInput, T>> {
let collapsed: boolean | undefined;
if (viewStateContext && node.id) {
if (viewStateContext && viewStateContext.viewState.expanded && node.id) {
collapsed = viewStateContext.viewState.expanded.indexOf(node.id) === -1;
}
return {
element: node,
children: Iterator.map(Iterator.fromArray(node.children), child => asTreeElement(child, viewStateContext)),
children: node.hasChildren ? Iterator.map(Iterator.fromArray(node.children), child => asTreeElement(child, viewStateContext)) : [],
collapsible: node.hasChildren,
collapsed
};
@@ -245,9 +262,10 @@ export interface IAsyncDataTreeOptions<T, TFilterData = void> extends IAsyncData
}
export interface IAsyncDataTreeViewState {
readonly focus: string[];
readonly selection: string[];
readonly expanded: string[];
readonly focus?: string[];
readonly selection?: string[];
readonly expanded?: string[];
readonly scrollTop?: number;
}
interface IAsyncDataTreeViewStateContext<TInput, T> {
@@ -260,7 +278,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
private readonly tree: ObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData>;
private readonly root: IAsyncDataTreeNode<TInput, T>;
private readonly renderedNodes = new Map<null | T, IAsyncDataTreeNode<TInput, T>>();
private readonly nodes = new Map<null | T, IAsyncDataTreeNode<TInput, T>>();
private readonly sorter?: ITreeSorter<T>;
private readonly subTreeRefreshPromises = new Map<IAsyncDataTreeNode<TInput, T>, Promise<void>>();
@@ -280,12 +298,15 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
get onDidChangeSelection(): Event<ITreeEvent<T>> { return Event.map(this.tree.onDidChangeSelection, asTreeEvent); }
get onDidOpen(): Event<ITreeEvent<T>> { return Event.map(this.tree.onDidOpen, asTreeEvent); }
get onKeyDown(): Event<KeyboardEvent> { return this.tree.onKeyDown; }
get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.tree.onMouseClick, asTreeMouseEvent); }
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.tree.onMouseDblClick, asTreeMouseEvent); }
get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return Event.map(this.tree.onContextMenu, asTreeContextMenuEvent); }
get onDidFocus(): Event<void> { return this.tree.onDidFocus; }
get onDidBlur(): Event<void> { return this.tree.onDidBlur; }
get onDidUpdateOptions(): Event<IAsyncDataTreeOptionsUpdate> { return this.tree.onDidUpdateOptions; }
get filterOnType(): boolean { return this.tree.filterOnType; }
get openOnSingleClick(): boolean { return this.tree.openOnSingleClick; }
@@ -308,16 +329,11 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
this.tree = new ObjectTree(container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
this.root = {
this.root = createAsyncDataTreeNode({
element: undefined!,
parent: null,
children: [],
state: AsyncDataTreeNodeState.Uninitialized,
hasChildren: true,
needsRefresh: false,
disposed: false,
slow: false
};
hasChildren: true
});
if (this.identityProvider) {
this.root = {
@@ -326,7 +342,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
};
}
this.renderedNodes.set(null, this.root);
this.nodes.set(null, this.root);
this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables);
}
@@ -365,6 +381,14 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return this.tree.renderHeight;
}
get firstVisibleElement(): T {
return this.tree.firstVisibleElement!.element as T;
}
get lastVisibleElement(): T {
return this.tree.lastVisibleElement!.element as T;
}
domFocus(): void {
this.tree.domFocus();
}
@@ -397,6 +421,10 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
this.tree.setFocus(viewStateContext.focus);
this.tree.setSelection(viewStateContext.selection);
}
if (viewState && typeof viewState.scrollTop === 'number') {
this.scrollTop = viewState.scrollTop;
}
}
async updateChildren(element: TInput | T = this.root.element, recursive = true, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
@@ -404,7 +432,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
throw new Error('Tree input not set');
}
if (this.root.state === AsyncDataTreeNodeState.Loading) {
if (this.root.loading) {
await this.subTreeRefreshPromises.get(this.root)!;
await Event.toPromise(this._onDidRender.event);
}
@@ -412,15 +440,24 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
await this.refreshAndRenderNode(this.getDataNode(element), recursive, ChildrenResolutionReason.Refresh, viewStateContext);
}
resort(element: TInput | T = this.root.element, recursive = true): void {
this.tree.resort(this.getDataNode(element), recursive);
}
hasNode(element: TInput | T): boolean {
return element === this.root.element || this.renderedNodes.has(element as T);
return element === this.root.element || this.nodes.has(element as T);
}
// View
refresh(element: T): void {
rerender(element?: T): void {
if (element === undefined) {
this.tree.rerender();
return;
}
const node = this.getDataNode(element);
this.tree.refresh(node);
this.tree.rerender(node);
}
updateWidth(element: T): void {
@@ -446,20 +483,20 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
throw new Error('Tree input not set');
}
if (this.root.state === AsyncDataTreeNodeState.Loading) {
if (this.root.loading) {
await this.subTreeRefreshPromises.get(this.root)!;
await Event.toPromise(this._onDidRender.event);
}
const node = this.getDataNode(element);
if (node !== this.root && node.state !== AsyncDataTreeNodeState.Loading && !this.tree.isCollapsed(node)) {
if (node !== this.root && !node.loading && !this.tree.isCollapsed(node)) {
return false;
}
const result = this.tree.expand(node === this.root ? null : node, recursive);
if (node.state === AsyncDataTreeNodeState.Loading) {
if (node.loading) {
await this.subTreeRefreshPromises.get(node)!;
await Event.toPromise(this._onDidRender.event);
}
@@ -565,16 +602,10 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return (node && node.element)!;
}
// List
get visibleNodeCount(): number {
return this.tree.visibleNodeCount;
}
// Implementation
private getDataNode(element: TInput | T): IAsyncDataTreeNode<TInput, T> {
const node: IAsyncDataTreeNode<TInput, T> | undefined = this.renderedNodes.get((element === this.root.element ? null : element) as T);
const node: IAsyncDataTreeNode<TInput, T> | undefined = this.nodes.get((element === this.root.element ? null : element) as T);
if (!node) {
throw new Error(`Data tree node not found: ${element}`);
@@ -626,39 +657,19 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
private async doRefreshSubTree(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
node.state = AsyncDataTreeNodeState.Loading;
node.loading = true;
try {
await this.doRefreshNode(node, recursive, viewStateContext);
const childrenToRefresh = await this.doRefreshNode(node, recursive, viewStateContext);
node.stale = false;
if (recursive) {
const childrenToRefresh = node.children
.filter(child => {
if (child.needsRefresh) {
child.needsRefresh = false;
return true;
}
// TODO@joao: is this still needed?
if (child.hasChildren && child.state === AsyncDataTreeNodeState.Loaded) {
return true;
}
if (!viewStateContext || !child.id) {
return false;
}
return viewStateContext.viewState.expanded.indexOf(child.id) > -1;
});
await Promise.all(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext)));
}
await Promise.all(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext)));
} finally {
node.state = AsyncDataTreeNodeState.Loaded;
node.loading = false;
}
}
private async doRefreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
private async doRefreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<IAsyncDataTreeNode<TInput, T>[]> {
node.hasChildren = !!this.dataSource.hasChildren(node.element!);
let childrenPromise: Promise<T[]>;
@@ -673,21 +684,20 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
this._onDidChangeNodeSlowState.fire(node);
}, _ => null);
childrenPromise = always(this.doGetChildren(node), () => slowTimeout.cancel());
childrenPromise = this.doGetChildren(node)
.finally(() => slowTimeout.cancel());
}
try {
const children = await childrenPromise;
this.setChildren(node, children, recursive, viewStateContext);
return this.setChildren(node, children, recursive, viewStateContext);
} catch (err) {
node.needsRefresh = true;
if (node !== this.root) {
this.tree.collapse(node === this.root ? null : node);
}
if (isPromiseCanceledError(err)) {
return;
return [];
}
throw err;
@@ -715,12 +725,14 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
return children;
});
this.refreshPromises.set(node, result);
return always(result, () => this.refreshPromises.delete(node));
return result.finally(() => this.refreshPromises.delete(node));
}
private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent<IAsyncDataTreeNode<TInput, T>, any>): void {
if (!node.collapsed && (node.element.state === AsyncDataTreeNodeState.Uninitialized || node.element.needsRefresh)) {
if (!node.collapsed && node.element.stale) {
if (deep) {
this.collapse(node.element.element as T);
} else {
@@ -730,7 +742,12 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
}
private setChildren(node: IAsyncDataTreeNode<TInput, T>, childrenElements: T[], recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
private setChildren(node: IAsyncDataTreeNode<TInput, T>, childrenElements: T[], recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): IAsyncDataTreeNode<TInput, T>[] {
// perf: if the node was and still is a leaf, avoid all this hassle
if (node.children.length === 0 && childrenElements.length === 0) {
return [];
}
let nodeChildren: Map<string, IAsyncDataTreeNode<TInput, T>> | undefined;
if (this.identityProvider) {
@@ -741,66 +758,57 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
}
let childrenToRefresh: IAsyncDataTreeNode<TInput, T>[] = [];
const children = childrenElements.map<IAsyncDataTreeNode<TInput, T>>(element => {
if (!this.identityProvider) {
const hasChildren = !!this.dataSource.hasChildren(element);
return {
return createAsyncDataTreeNode({
element,
parent: node,
children: [],
state: AsyncDataTreeNodeState.Uninitialized,
hasChildren,
needsRefresh: false,
disposed: false,
slow: false
};
hasChildren: !!this.dataSource.hasChildren(element),
});
}
const id = this.identityProvider.getId(element).toString();
const asyncDataTreeNode = nodeChildren!.get(id);
if (!asyncDataTreeNode) {
const childAsyncDataTreeNode: IAsyncDataTreeNode<TInput, T> = {
element,
parent: node,
children: [],
id,
state: AsyncDataTreeNodeState.Uninitialized,
hasChildren: !!this.dataSource.hasChildren(element),
needsRefresh: false,
disposed: false,
slow: false
};
if (asyncDataTreeNode) {
asyncDataTreeNode.element = element;
asyncDataTreeNode.stale = asyncDataTreeNode.stale || recursive;
asyncDataTreeNode.hasChildren = !!this.dataSource.hasChildren(element);
if (viewStateContext) {
if (viewStateContext.viewState.focus.indexOf(id) > -1) {
viewStateContext.focus.push(childAsyncDataTreeNode);
}
if (viewStateContext.viewState.selection.indexOf(id) > -1) {
viewStateContext.selection.push(childAsyncDataTreeNode);
}
if (recursive && !this.tree.isCollapsed(asyncDataTreeNode)) {
childrenToRefresh.push(asyncDataTreeNode);
}
return childAsyncDataTreeNode;
return asyncDataTreeNode;
}
asyncDataTreeNode.element = element;
const childAsyncDataTreeNode = createAsyncDataTreeNode({
element,
parent: node,
id,
hasChildren: !!this.dataSource.hasChildren(element)
});
if (asyncDataTreeNode.state === AsyncDataTreeNodeState.Loaded || asyncDataTreeNode.hasChildren !== !!this.dataSource.hasChildren(asyncDataTreeNode.element)) {
asyncDataTreeNode.needsRefresh = true;
if (viewStateContext && viewStateContext.viewState.focus && viewStateContext.viewState.focus.indexOf(id) > -1) {
viewStateContext.focus.push(childAsyncDataTreeNode);
}
return asyncDataTreeNode;
if (viewStateContext && viewStateContext.viewState.selection && viewStateContext.viewState.selection.indexOf(id) > -1) {
viewStateContext.selection.push(childAsyncDataTreeNode);
}
if (viewStateContext && viewStateContext.viewState.expanded && viewStateContext.viewState.expanded.indexOf(id) > -1) {
childrenToRefresh.push(childAsyncDataTreeNode);
}
return childAsyncDataTreeNode;
});
// perf: if the node was and still is a leaf, avoid all these expensive no-ops
if (node.children.length === 0 && childrenElements.length === 0) {
return;
}
node.children.splice(0, node.children.length, ...children);
return childrenToRefresh;
}
private render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
@@ -809,7 +817,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
const onDidCreateNode = (treeNode: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>) => {
if (treeNode.element.element) {
insertedElements.add(treeNode.element.element as T);
this.renderedNodes.set(treeNode.element.element as T, treeNode.element);
this.nodes.set(treeNode.element.element as T, treeNode.element);
}
};
@@ -817,7 +825,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
if (treeNode.element.element) {
if (!insertedElements.has(treeNode.element.element as T)) {
treeNode.element.disposed = true;
this.renderedNodes.delete(treeNode.element.element as T);
this.nodes.delete(treeNode.element.element as T);
}
}
};
@@ -853,7 +861,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
queue.push(...node.children);
}
return { focus, selection, expanded };
return { focus, selection, expanded, scrollTop: this.scrollTop };
}
dispose(): void {

View File

@@ -17,7 +17,7 @@ export interface IDataTreeOptions<T, TFilterData = void> extends IAbstractTreeOp
export interface IDataTreeViewState {
readonly focus: string[];
readonly selection: string[];
readonly collapsed: string[];
readonly expanded: string[];
}
export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
@@ -26,6 +26,7 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
private input: TInput | undefined;
private identityProvider: IIdentityProvider<T> | undefined;
private nodesByIdentity = new Map<string, ITreeNode<T, TFilterData>>();
constructor(
container: HTMLElement,
@@ -61,19 +62,22 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
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(element);
focus.push(node.element);
}
if (viewState.selection.indexOf(id) > -1) {
selection.push(element);
selection.push(node.element);
}
return id in viewState.collapsed;
};
this._refresh(input, isCollapsed);
this._refresh(input, isCollapsed, onDidCreateNode);
this.setFocus(focus);
this.setSelection(selection);
}
@@ -83,29 +87,81 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
throw new Error('Tree input not set');
}
this._refresh(element);
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 {
this.model.refresh(element);
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): void {
this.model.setChildren((element === this.input ? null : element) as T, this.createIterator(element, isCollapsed));
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 createIterator(element: TInput | T, isCollapsed?: (el: T) => boolean): Iterator<ITreeElement<T>> {
const children = Iterator.fromArray(this.dataSource.getChildren(element));
private iterate(element: TInput | T, isCollapsed?: (el: T) => boolean | undefined): { elements: Iterator<ITreeElement<T>>, size: number } {
const children = this.dataSource.getChildren(element);
const elements = Iterator.map<any, ITreeElement<T>>(Iterator.fromArray(children), element => {
const { elements: children, size } = this.iterate(element, isCollapsed);
const collapsed = size === 0 ? undefined : (isCollapsed && isCollapsed(element));
return Iterator.map<any, ITreeElement<T>>(children, element => ({
element,
children: this.createIterator(element),
collapsed: isCollapsed && isCollapsed(element)
}));
return { element, children, collapsed };
});
return { elements, size: children.length };
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IDataTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
@@ -123,20 +179,20 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
const focus = this.getFocus().map(getId);
const selection = this.getSelection().map(getId);
const collapsed: string[] = [];
const expanded: string[] = [];
const root = this.model.getNode();
const queue = [root];
while (queue.length > 0) {
const node = queue.shift()!;
if (node !== root && node.collapsed) {
collapsed.push(getId(node.element!));
if (node !== root && node.collapsible && !node.collapsed) {
expanded.push(getId(node.element!));
}
queue.push(...node.children);
}
return { focus, selection, collapsed };
return { focus, selection, expanded };
}
}
}

View File

@@ -31,11 +31,16 @@ export class IndexTree<T, TFilterData = void> extends AbstractTree<T, TFilterDat
return this.model.splice(location, deleteCount, toInsert);
}
refresh(location: number[]): void {
this.model.refresh(location);
rerender(location?: number[]): void {
if (location === undefined) {
this.view.rerender();
return;
}
this.model.rerender(location);
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IIndexTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, number[]> {
return new IndexTreeModel(view, this.rootElement, options);
}
}
}

View File

@@ -71,8 +71,6 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
this.filter = options.filter;
this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren;
// this.onDidChangeCollapseState(node => console.log(node.collapsed, node));
this.root = {
parent: undefined,
element: rootElement,
@@ -177,7 +175,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return result;
}
refresh(location: number[]): void {
rerender(location: number[]): void {
if (location.length === 0) {
throw new Error('Invalid tree location');
}
@@ -280,6 +278,21 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
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._setCollapsed(location, false);
}
}
});
}
refilter(): void {
const previousRenderNodeCount = this.root.renderNodeCount;
const toInsert = this.updateNodeAfterFilterChange(this.root);
@@ -582,4 +595,4 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return this._getLastElementAncestor(node.children[node.children.length - 1]);
}
}
}

View File

@@ -36,11 +36,20 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
return this.model.setChildren(element, children, onDidCreateNode, onDidDeleteNode);
}
refresh(element: T): void {
this.model.refresh(element);
rerender(element?: T): void {
if (element === undefined) {
this.view.rerender();
return;
}
this.model.rerender(element);
}
resort(element: T, recursive = true): void {
this.model.resort(element, recursive);
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
return new ObjectTreeModel(view, options);
}
}
}

View File

@@ -19,7 +19,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
private model: IndexTreeModel<T | null, TFilterData>;
private nodes = new Map<T | null, ITreeNode<T, TFilterData>>();
private sorter?: ITreeSorter<ITreeElement<T>>;
private sorter?: ITreeSorter<{ element: T; }>;
readonly onDidSplice: Event<ITreeModelSpliceEvent<T | null, TFilterData>>;
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
@@ -49,6 +49,15 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
): Iterator<ITreeElement<T | null>> {
const location = this.getElementLocation(element);
return this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode);
}
private _setChildren(
location: number[],
children: ISequence<ITreeElement<T>> | undefined,
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
): Iterator<ITreeElement<T | null>> {
const insertedElements = new Set<T | null>();
const _onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
@@ -73,13 +82,13 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
return this.model.splice(
[...location, 0],
Number.MAX_VALUE,
this.preserveCollapseState(children),
children,
_onDidCreateNode,
_onDidDeleteNode
);
}
private preserveCollapseState(elements: ISequence<ITreeElement<T | null>> | undefined): ISequence<ITreeElement<T | null>> {
private preserveCollapseState(elements: ISequence<ITreeElement<T>> | undefined): ISequence<ITreeElement<T>> {
let iterator = elements ? getSequenceIterator(elements) : Iterator.empty<ITreeElement<T>>();
if (this.sorter) {
@@ -90,11 +99,14 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
const node = this.nodes.get(treeElement.element);
if (!node) {
return treeElement;
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 : (collapsible && node.collapsed);
const collapsed = typeof treeElement.collapsed !== 'undefined' ? treeElement.collapsed : node.collapsed;
return {
...treeElement,
@@ -105,9 +117,35 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
});
}
refresh(element: T): void {
rerender(element: T): void {
const location = this.getElementLocation(element);
this.model.refresh(location);
this.model.rerender(location);
}
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): ISequence<ITreeElement<T>> {
let childrenNodes = Iterator.fromArray(node.children as ITreeNode<T, TFilterData>[]);
if (recursive || first) {
childrenNodes = Iterator.fromArray(Iterator.collect(childrenNodes).sort(this.sorter!.compare.bind(this.sorter)));
}
return Iterator.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)
}));
}
getParentElement(ref: T | null = null): T | null {
@@ -150,6 +188,11 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
return this.model.setCollapsed(location, collapsed, recursive);
}
expandTo(element: T): void {
const location = this.getElementLocation(element);
this.model.expandTo(location);
}
refilter(): void {
this.model.refilter();
}

View File

@@ -122,6 +122,7 @@ export interface ITreeModel<T, TFilterData, TRef> {
isCollapsible(location: TRef): boolean;
isCollapsed(location: TRef): boolean;
setCollapsed(location: TRef, collapsed?: boolean, recursive?: boolean): boolean;
expandTo(location: TRef): void;
refilter(): void;
}
@@ -144,7 +145,7 @@ export interface ITreeMouseEvent<T> {
export interface ITreeContextMenuEvent<T> {
browserEvent: UIEvent;
element: T | null;
anchor: HTMLElement | { x: number; y: number; } | undefined;
anchor: HTMLElement | { x: number; y: number; };
}
export interface ITreeNavigator<T> {