Merge branch 'joao/fix-78388'

This commit is contained in:
Joao Moreno 2019-09-09 12:11:41 +02:00
commit ec9a617c20
9 changed files with 184 additions and 32 deletions

View file

@ -1053,13 +1053,15 @@ class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<
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 (node.collapsible) {
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;
if (expandOnlyOnTwistieClick && onTwistie) {
return;
}
}
super.onPointer(e);
@ -1418,6 +1420,10 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
return this.model.isCollapsible(location);
}
setCollapsible(location: TRef, collapsible?: boolean): boolean {
return this.model.setCollapsible(location, collapsible);
}
isCollapsed(location: TRef): boolean {
return this.model.isCollapsed(location);
}

View file

@ -451,7 +451,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
const viewStateContext = viewState && { viewState, focus: [], selection: [] } as IAsyncDataTreeViewStateContext<TInput, T>;
await this.updateChildren(input, true, viewStateContext);
await this._updateChildren(input, true, viewStateContext);
if (viewStateContext) {
this.tree.setFocus(viewStateContext.focus);
@ -463,7 +463,11 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
}
}
async updateChildren(element: TInput | T = this.root.element, recursive = true, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
async updateChildren(element: TInput | T = this.root.element, recursive = true): Promise<void> {
await this._updateChildren(element, recursive);
}
private async _updateChildren(element: TInput | T = this.root.element, recursive = true, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
if (typeof this.root.element === 'undefined') {
throw new TreeError(this.user, 'Tree input not set');
}
@ -874,6 +878,11 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
private render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
const children = node.children.map(c => asTreeElement(c, viewStateContext));
this.tree.setChildren(node === this.root ? null : node, children);
if (node !== this.root) {
this.tree.setCollapsible(node, node.hasChildren);
}
this._onDidRender.fire();
}

View file

@ -231,6 +231,11 @@ export class CompressedTreeModel<T extends NonNullable<any>, TFilterData extends
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);
@ -397,6 +402,10 @@ export class CompressedObjectTreeModel<T extends NonNullable<any>, TFilterData e
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);
}

View file

@ -46,6 +46,21 @@ export interface IIndexTreeModelOptions<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 class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = void> implements ITreeModel<T, TFilterData, number[]> {
readonly rootRef = [];
@ -205,6 +220,17 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
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;
}
@ -216,15 +242,16 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
collapsed = !node.collapsed;
}
return this.eventBufferer.bufferEvents(() => this._setCollapsed(location, collapsed!, recursive));
const update: CollapsedStateUpdate = { collapsed, recursive: recursive || false };
return this.eventBufferer.bufferEvents(() => this._setCollapseState(location, update));
}
private _setCollapsed(location: number[], collapsed: boolean, recursive?: boolean): boolean {
private _setCollapseState(location: number[], update: CollapseStateUpdate): boolean {
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
const result = this._setListNodeCollapsed(node, listIndex, revealed, collapsed!, recursive || false);
const result = this._setListNodeCollapseState(node, listIndex, revealed, update);
if (node !== this.root && this.autoExpandSingleChildren && !collapsed! && !recursive) {
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++) {
@ -241,15 +268,15 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
}
if (onlyVisibleChildIndex > -1) {
this._setCollapsed([...location, onlyVisibleChildIndex], false, false);
this._setCollapseState([...location, onlyVisibleChildIndex], update);
}
}
return result;
}
private _setListNodeCollapsed(node: IMutableTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, collapsed: boolean, recursive: boolean): boolean {
const result = this._setNodeCollapsed(node, collapsed, recursive, false);
private _setListNodeCollapseState(node: IMutableTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, update: CollapseStateUpdate): boolean {
const result = this._setNodeCollapseState(node, update, false);
if (!revealed || !node.visible) {
return result;
@ -263,20 +290,28 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
return result;
}
private _setNodeCollapsed(node: IMutableTreeNode<T, TFilterData>, collapsed: boolean, recursive: boolean, deep: boolean): boolean {
let result = node.collapsible && node.collapsed !== collapsed;
private _setNodeCollapseState(node: IMutableTreeNode<T, TFilterData>, update: CollapseStateUpdate, deep: boolean): boolean {
let result: boolean;
if (node.collapsible) {
node.collapsed = collapsed;
if (node === this.root) {
result = false;
} else {
if (isCollapsibleStateUpdate(update)) {
result = node.collapsible !== update.collapsible;
node.collapsible = update.collapsible;
} else {
result = node.collapsed !== update.collapsed;
node.collapsed = update.collapsed;
}
if (result) {
this._onDidChangeCollapseState.fire({ node, deep });
}
}
if (recursive) {
if (!isCollapsibleStateUpdate(update) && update.recursive) {
for (const child of node.children) {
result = this._setNodeCollapsed(child, collapsed, true, true) || result;
result = this._setNodeCollapseState(child, update, true) || result;
}
}
@ -292,7 +327,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
location = location.slice(0, location.length - 1);
if (node.collapsed) {
this._setCollapsed(location, false);
this._setCollapseState(location, { collapsed: false, recursive: false });
}
}
});

View file

@ -216,6 +216,11 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
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);

View file

@ -120,6 +120,7 @@ export interface ITreeModel<T, TFilterData, TRef> {
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;

View file

@ -355,4 +355,38 @@ suite('AsyncDataTree', function () {
race = await Promise.race([pExpandA.then(() => 'expand'), timeout(1).then(() => 'timeout')]);
assert.equal(race, 'expand', 'expand(a) should now be done');
});
test('issue #78388 - tree should react to hasChildren toggles', async () => {
const container = document.createElement('div');
const model = new Model({
id: 'root',
children: [{
id: 'a'
}]
});
const tree = new AsyncDataTree<Element, Element>('test', container, new VirtualDelegate(), [new Renderer()], new DataSource(), { identityProvider: new IdentityProvider() });
tree.layout(200);
await tree.setInput(model.root);
assert.equal(container.querySelectorAll('.monaco-list-row').length, 1);
let twistie = container.querySelector('.monaco-list-row:first-child .monaco-tl-twistie') as HTMLElement;
assert(!hasClass(twistie, 'collapsible'));
assert(!hasClass(twistie, 'collapsed'));
model.get('a').children = [{ id: 'aa' }];
await tree.updateChildren(model.get('a'), false);
assert.equal(container.querySelectorAll('.monaco-list-row').length, 1);
twistie = container.querySelector('.monaco-list-row:first-child .monaco-tl-twistie') as HTMLElement;
assert(hasClass(twistie, 'collapsible'));
assert(hasClass(twistie, 'collapsed'));
model.get('a').children = [];
await tree.updateChildren(model.get('a'), false);
assert.equal(container.querySelectorAll('.monaco-list-row').length, 1);
twistie = container.querySelector('.monaco-list-row:first-child .monaco-tl-twistie') as HTMLElement;
assert(!hasClass(twistie, 'collapsible'));
assert(!hasClass(twistie, 'collapsed'));
});
});

View file

@ -333,6 +333,67 @@ suite('IndexTreeModel', function () {
assert.deepEqual(toArray(list), [1, 11, 2]);
});
test('setCollapsible', () => {
const list: ITreeNode<number>[] = [];
const model = new IndexTreeModel<number>('test', toSpliceable(list), -1);
model.splice([0], 0, Iterator.fromArray([
{
element: 0, children: Iterator.fromArray([
{ element: 10 }
])
}
]));
assert.deepEqual(list.length, 2);
model.setCollapsible([0], false);
assert.deepEqual(list.length, 2);
assert.deepEqual(list[0].element, 0);
assert.deepEqual(list[0].collapsible, false);
assert.deepEqual(list[0].collapsed, false);
assert.deepEqual(list[1].element, 10);
assert.deepEqual(list[1].collapsible, false);
assert.deepEqual(list[1].collapsed, false);
model.setCollapsed([0], true);
assert.deepEqual(list.length, 1);
assert.deepEqual(list[0].element, 0);
assert.deepEqual(list[0].collapsible, false);
assert.deepEqual(list[0].collapsed, true);
model.setCollapsed([0], false);
assert.deepEqual(list[0].element, 0);
assert.deepEqual(list[0].collapsible, false);
assert.deepEqual(list[0].collapsed, false);
assert.deepEqual(list[1].element, 10);
assert.deepEqual(list[1].collapsible, false);
assert.deepEqual(list[1].collapsed, false);
model.setCollapsible([0], true);
assert.deepEqual(list.length, 2);
assert.deepEqual(list[0].element, 0);
assert.deepEqual(list[0].collapsible, true);
assert.deepEqual(list[0].collapsed, false);
assert.deepEqual(list[1].element, 10);
assert.deepEqual(list[1].collapsible, false);
assert.deepEqual(list[1].collapsed, false);
model.setCollapsed([0], true);
assert.deepEqual(list.length, 1);
assert.deepEqual(list[0].element, 0);
assert.deepEqual(list[0].collapsible, true);
assert.deepEqual(list[0].collapsed, true);
model.setCollapsed([0], false);
assert.deepEqual(list[0].element, 0);
assert.deepEqual(list[0].collapsible, true);
assert.deepEqual(list[0].collapsed, false);
assert.deepEqual(list[1].element, 10);
assert.deepEqual(list[1].collapsible, false);
assert.deepEqual(list[1].collapsed, false);
});
test('simple filter', function () {
const list: ITreeNode<number>[] = [];
const filter = new class implements ITreeFilter<number> {

View file

@ -597,16 +597,8 @@ export class CustomTreeView extends Disposable implements ITreeView {
const tree = this.tree;
if (tree) {
this.refreshing = true;
const parents: Set<ITreeItem> = new Set<ITreeItem>();
elements.forEach(element => {
if (element !== this.root) {
const parent = tree.getParentElement(element);
parents.add(parent);
} else {
parents.add(element);
}
});
await Promise.all(Array.from(parents.values()).map(element => tree.updateChildren(element, true)));
await Promise.all(elements.map(element => tree.updateChildren(element, true)));
elements.map(element => tree.rerender(element));
this.refreshing = false;
this.updateContentAreas();
if (this.focused) {