Skip to content

Commit 707bb36

Browse files
committed
ObjectTree.navigate
fixes microsoft#64793
1 parent 89a7751 commit 707bb36

5 files changed

Lines changed: 231 additions & 2 deletions

File tree

src/vs/base/browser/ui/list/listWidget.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,10 @@ export class List<T> implements ISpliceable<T>, IDisposable {
10961096
this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements));
10971097
}
10981098

1099+
element(index: number): T {
1100+
return this.view.element(index);
1101+
}
1102+
10991103
get length(): number {
11001104
return this.view.length;
11011105
}

src/vs/base/browser/ui/tree/abstractTree.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { append, $, toggleClass } from 'vs/base/browser/dom';
1111
import { Event, Relay } from 'vs/base/common/event';
1212
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
1313
import { KeyCode } from 'vs/base/common/keyCodes';
14-
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter } from 'vs/base/browser/ui/tree/tree';
14+
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
1515
import { ISpliceable } from 'vs/base/common/sequence';
1616

1717
function asListOptions<T, TFilterData>(options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
@@ -491,8 +491,68 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
491491

492492
protected abstract createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IAbstractTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, TRef>;
493493

494+
navigate(): ITreeNavigator<T> {
495+
return new TreeNavigator(this.view, this.model);
496+
}
497+
494498
dispose(): void {
495499
this.disposables = dispose(this.disposables);
496500
this.view.dispose();
497501
}
498502
}
503+
504+
interface ITreeNavigatorView<T extends NonNullable<any>, TFilterData> {
505+
readonly length: number;
506+
element(index: number): ITreeNode<T, TFilterData>;
507+
}
508+
509+
class TreeNavigator<T extends NonNullable<any>, TFilterData, TRef> implements ITreeNavigator<T> {
510+
511+
private index: number = -1;
512+
513+
constructor(private view: ITreeNavigatorView<T, TFilterData>, private model: ITreeModel<T, TFilterData, TRef>) { }
514+
515+
current(): T | null {
516+
if (this.index < 0 || this.index >= this.view.length) {
517+
return null;
518+
}
519+
520+
return this.view.element(this.index).element;
521+
}
522+
523+
previous(): T | null {
524+
this.index--;
525+
return this.current();
526+
}
527+
528+
next(): T | null {
529+
this.index++;
530+
return this.current();
531+
}
532+
533+
parent(): T | null {
534+
if (this.index < 0 || this.index >= this.view.length) {
535+
return null;
536+
}
537+
538+
const node = this.view.element(this.index);
539+
540+
if (!node.parent) {
541+
this.index = -1;
542+
return this.current();
543+
}
544+
545+
this.index = this.model.getListIndex(this.model.getNodeLocation(node.parent));
546+
return this.current();
547+
}
548+
549+
first(): T | null {
550+
this.index = 0;
551+
return this.current();
552+
}
553+
554+
last(): T | null {
555+
this.index = this.view.length - 1;
556+
return this.current();
557+
}
558+
}

src/vs/base/browser/ui/tree/indexTreeModel.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
120120
}
121121

122122
getListIndex(location: number[]): number {
123-
return this.getTreeNodeWithListIndex(location).listIndex;
123+
const { node, listIndex } = this.getTreeNodeWithListIndex(location);
124+
return node ? listIndex : -1;
124125
}
125126

126127
setCollapsed(location: number[], collapsed: boolean): boolean {

src/vs/base/browser/ui/tree/tree.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ export interface ITreeContextMenuEvent<T> {
128128
anchor: HTMLElement | { x: number; y: number; } | undefined;
129129
}
130130

131+
export interface ITreeNavigator<T> {
132+
current(): T | null;
133+
previous(): T | null;
134+
parent(): T | null;
135+
first(): T | null;
136+
last(): T | null;
137+
next(): T | null;
138+
}
139+
131140
/**
132141
* Use this renderer when you want to re-render elements on account of
133142
* an event firing.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as assert from 'assert';
7+
import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
8+
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
9+
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
10+
import { Iterator } from 'vs/base/common/iterator';
11+
12+
suite('ObjectTree', function () {
13+
suite('TreeNavigator', function () {
14+
let tree: ObjectTree<number>;
15+
let filter = (_: number) => true;
16+
17+
setup(() => {
18+
const container = document.createElement('div');
19+
container.style.width = '200px';
20+
container.style.height = '200px';
21+
22+
const delegate = new class implements IListVirtualDelegate<number> {
23+
getHeight() { return 20; }
24+
getTemplateId(): string { return 'default'; }
25+
};
26+
27+
const renderer = new class implements ITreeRenderer<number, void, HTMLElement> {
28+
readonly templateId = 'default';
29+
renderTemplate(container: HTMLElement): HTMLElement {
30+
return container;
31+
}
32+
renderElement(element: ITreeNode<number, void>, index: number, templateData: HTMLElement): void {
33+
templateData.textContent = `${element.element}`;
34+
}
35+
disposeTemplate(): void { }
36+
};
37+
38+
tree = new ObjectTree<number>(container, delegate, [renderer], { filter: { filter: (el) => filter(el) } });
39+
tree.layout(200);
40+
});
41+
42+
teardown(() => {
43+
tree.dispose();
44+
});
45+
46+
test('should be able to navigate', () => {
47+
tree.setChildren(null, Iterator.fromArray([
48+
{
49+
element: 0, children: Iterator.fromArray([
50+
{ element: 10 },
51+
{ element: 11 },
52+
{ element: 12 },
53+
])
54+
},
55+
{ element: 1 },
56+
{ element: 2 }
57+
]));
58+
59+
const navigator = tree.navigate();
60+
61+
assert.equal(navigator.current(), null);
62+
assert.equal(navigator.next(), 0);
63+
assert.equal(navigator.current(), 0);
64+
assert.equal(navigator.next(), 10);
65+
assert.equal(navigator.current(), 10);
66+
assert.equal(navigator.next(), 11);
67+
assert.equal(navigator.current(), 11);
68+
assert.equal(navigator.next(), 12);
69+
assert.equal(navigator.current(), 12);
70+
assert.equal(navigator.next(), 1);
71+
assert.equal(navigator.current(), 1);
72+
assert.equal(navigator.next(), 2);
73+
assert.equal(navigator.current(), 2);
74+
assert.equal(navigator.previous(), 1);
75+
assert.equal(navigator.current(), 1);
76+
assert.equal(navigator.previous(), 12);
77+
assert.equal(navigator.previous(), 11);
78+
assert.equal(navigator.previous(), 10);
79+
assert.equal(navigator.previous(), 0);
80+
assert.equal(navigator.previous(), null);
81+
assert.equal(navigator.next(), 0);
82+
assert.equal(navigator.next(), 10);
83+
assert.equal(navigator.parent(), 0);
84+
assert.equal(navigator.parent(), null);
85+
assert.equal(navigator.first(), 0);
86+
assert.equal(navigator.last(), 2);
87+
});
88+
89+
test('should skip collapsed nodes', () => {
90+
tree.setChildren(null, Iterator.fromArray([
91+
{
92+
element: 0, collapsed: true, children: Iterator.fromArray([
93+
{ element: 10 },
94+
{ element: 11 },
95+
{ element: 12 },
96+
])
97+
},
98+
{ element: 1 },
99+
{ element: 2 }
100+
]));
101+
102+
const navigator = tree.navigate();
103+
104+
assert.equal(navigator.current(), null);
105+
assert.equal(navigator.next(), 0);
106+
assert.equal(navigator.next(), 1);
107+
assert.equal(navigator.next(), 2);
108+
assert.equal(navigator.next(), null);
109+
assert.equal(navigator.previous(), 2);
110+
assert.equal(navigator.previous(), 1);
111+
assert.equal(navigator.previous(), 0);
112+
assert.equal(navigator.previous(), null);
113+
assert.equal(navigator.next(), 0);
114+
assert.equal(navigator.parent(), null);
115+
assert.equal(navigator.first(), 0);
116+
assert.equal(navigator.last(), 2);
117+
});
118+
119+
test('should skip filtered elements', () => {
120+
filter = el => el % 2 === 0;
121+
122+
tree.setChildren(null, Iterator.fromArray([
123+
{
124+
element: 0, children: Iterator.fromArray([
125+
{ element: 10 },
126+
{ element: 11 },
127+
{ element: 12 },
128+
])
129+
},
130+
{ element: 1 },
131+
{ element: 2 }
132+
]));
133+
134+
const navigator = tree.navigate();
135+
136+
assert.equal(navigator.current(), null);
137+
assert.equal(navigator.next(), 0);
138+
assert.equal(navigator.next(), 10);
139+
assert.equal(navigator.next(), 12);
140+
assert.equal(navigator.next(), 2);
141+
assert.equal(navigator.next(), null);
142+
assert.equal(navigator.previous(), 2);
143+
assert.equal(navigator.previous(), 12);
144+
assert.equal(navigator.previous(), 10);
145+
assert.equal(navigator.previous(), 0);
146+
assert.equal(navigator.previous(), null);
147+
assert.equal(navigator.next(), 0);
148+
assert.equal(navigator.next(), 10);
149+
assert.equal(navigator.parent(), 0);
150+
assert.equal(navigator.parent(), null);
151+
assert.equal(navigator.first(), 0);
152+
assert.equal(navigator.last(), 2);
153+
});
154+
});
155+
});

0 commit comments

Comments
 (0)