mirror of
https://github.com/Microsoft/vscode
synced 2024-10-30 19:48:09 +00:00
Merge branch 'joao/fix-78388'
This commit is contained in:
commit
ec9a617c20
9 changed files with 184 additions and 32 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue