1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { injectable } from 'inversify';
|
18 | import { Event, Emitter, WaitUntilEvent } from '../../common/event';
|
19 | import { Disposable, DisposableCollection } from '../../common/disposable';
|
20 | import { CancellationToken, CancellationTokenSource } from '../../common/cancellation';
|
21 | import { timeout } from '../../common/promise-util';
|
22 | import { isObject, Mutable } from '../../common';
|
23 |
|
24 | export const Tree = Symbol('Tree');
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | export interface Tree extends Disposable {
|
30 | |
31 |
|
32 |
|
33 |
|
34 |
|
35 | root: TreeNode | undefined;
|
36 | |
37 |
|
38 |
|
39 | readonly onChanged: Event<void>;
|
40 | |
41 |
|
42 |
|
43 | getNode(id: string | undefined): TreeNode | undefined;
|
44 | |
45 |
|
46 |
|
47 | validateNode(node: TreeNode | undefined): TreeNode | undefined;
|
48 | |
49 |
|
50 |
|
51 |
|
52 |
|
53 | refresh(): Promise<Readonly<CompositeTreeNode> | undefined>;
|
54 | |
55 |
|
56 |
|
57 |
|
58 |
|
59 | refresh(parent: Readonly<CompositeTreeNode>): Promise<Readonly<CompositeTreeNode> | undefined>;
|
60 | |
61 |
|
62 |
|
63 | readonly onNodeRefreshed: Event<Readonly<CompositeTreeNode> & WaitUntilEvent>;
|
64 | |
65 |
|
66 |
|
67 | readonly onDidChangeBusy: Event<TreeNode>;
|
68 | |
69 |
|
70 |
|
71 |
|
72 | markAsBusy(node: Readonly<TreeNode>, ms: number, token: CancellationToken): Promise<void>;
|
73 | }
|
74 |
|
75 |
|
76 |
|
77 |
|
78 | export interface TreeNode {
|
79 | |
80 |
|
81 |
|
82 | readonly id: string;
|
83 | |
84 |
|
85 |
|
86 |
|
87 |
|
88 | readonly name?: string;
|
89 | |
90 |
|
91 |
|
92 |
|
93 |
|
94 | readonly icon?: string;
|
95 | |
96 |
|
97 |
|
98 |
|
99 |
|
100 | readonly description?: string;
|
101 | |
102 |
|
103 |
|
104 |
|
105 | readonly visible?: boolean;
|
106 | |
107 |
|
108 |
|
109 |
|
110 | readonly parent: Readonly<CompositeTreeNode> | undefined;
|
111 | |
112 |
|
113 |
|
114 | readonly previousSibling?: TreeNode;
|
115 | |
116 |
|
117 |
|
118 | readonly nextSibling?: TreeNode;
|
119 | |
120 |
|
121 |
|
122 | readonly busy?: number;
|
123 | }
|
124 |
|
125 | export namespace TreeNode {
|
126 | export function is(node: unknown): node is TreeNode {
|
127 | return isObject(node) && 'id' in node && 'parent' in node;
|
128 | }
|
129 |
|
130 | export function equals(left: TreeNode | undefined, right: TreeNode | undefined): boolean {
|
131 | return left === right || (!!left && !!right && left.id === right.id);
|
132 | }
|
133 |
|
134 | export function isVisible(node: TreeNode | undefined): boolean {
|
135 | return !!node && (node.visible === undefined || node.visible);
|
136 | }
|
137 | }
|
138 |
|
139 |
|
140 |
|
141 |
|
142 | export interface CompositeTreeNode extends TreeNode {
|
143 | |
144 |
|
145 |
|
146 | children: ReadonlyArray<TreeNode>;
|
147 | }
|
148 |
|
149 | export namespace CompositeTreeNode {
|
150 | export function is(node: unknown): node is CompositeTreeNode {
|
151 | return isObject(node) && 'children' in node;
|
152 | }
|
153 |
|
154 | export function getFirstChild(parent: CompositeTreeNode): TreeNode | undefined {
|
155 | return parent.children[0];
|
156 | }
|
157 |
|
158 | export function getLastChild(parent: CompositeTreeNode): TreeNode | undefined {
|
159 | return parent.children[parent.children.length - 1];
|
160 | }
|
161 |
|
162 | export function isAncestor(parent: CompositeTreeNode, child: TreeNode | undefined): boolean {
|
163 | if (!child) {
|
164 | return false;
|
165 | }
|
166 | if (TreeNode.equals(parent, child.parent)) {
|
167 | return true;
|
168 | }
|
169 | return isAncestor(parent, child.parent);
|
170 | }
|
171 |
|
172 | export function indexOf(parent: CompositeTreeNode, node: TreeNode | undefined): number {
|
173 | if (!node) {
|
174 | return -1;
|
175 | }
|
176 | return parent.children.findIndex(child => TreeNode.equals(node, child));
|
177 | }
|
178 |
|
179 | export function addChildren(parent: CompositeTreeNode, children: TreeNode[]): CompositeTreeNode {
|
180 | for (const child of children) {
|
181 | addChild(parent, child);
|
182 | }
|
183 | return parent;
|
184 | }
|
185 |
|
186 | export function addChild(parent: CompositeTreeNode, child: TreeNode): CompositeTreeNode {
|
187 | const children = parent.children as TreeNode[];
|
188 | const index = children.findIndex(value => value.id === child.id);
|
189 | if (index !== -1) {
|
190 | children.splice(index, 1, child);
|
191 | setParent(child, index, parent);
|
192 | } else {
|
193 | children.push(child);
|
194 | setParent(child, parent.children.length - 1, parent);
|
195 | }
|
196 | return parent;
|
197 | }
|
198 |
|
199 | export function removeChild(parent: CompositeTreeNode, child: TreeNode): void {
|
200 | const children = parent.children as TreeNode[];
|
201 | const index = children.findIndex(value => value.id === child.id);
|
202 | if (index === -1) {
|
203 | return;
|
204 | }
|
205 | children.splice(index, 1);
|
206 | const { previousSibling, nextSibling } = child;
|
207 | if (previousSibling) {
|
208 | Object.assign(previousSibling, { nextSibling });
|
209 | }
|
210 | if (nextSibling) {
|
211 | Object.assign(nextSibling, { previousSibling });
|
212 | }
|
213 | }
|
214 |
|
215 | export function setParent(child: TreeNode, index: number, parent: CompositeTreeNode): void {
|
216 | const previousSibling = parent.children[index - 1];
|
217 | const nextSibling = parent.children[index + 1];
|
218 | Object.assign(child, { parent, previousSibling, nextSibling });
|
219 | if (previousSibling) {
|
220 | Object.assign(previousSibling, { nextSibling: child });
|
221 | }
|
222 | if (nextSibling) {
|
223 | Object.assign(nextSibling, { previousSibling: child });
|
224 | }
|
225 | }
|
226 | }
|
227 |
|
228 |
|
229 |
|
230 |
|
231 | @injectable()
|
232 | export class TreeImpl implements Tree {
|
233 |
|
234 | protected _root: TreeNode | undefined;
|
235 | protected readonly onChangedEmitter = new Emitter<void>();
|
236 | protected readonly onNodeRefreshedEmitter = new Emitter<CompositeTreeNode & WaitUntilEvent>();
|
237 | protected readonly toDispose = new DisposableCollection();
|
238 |
|
239 | protected readonly onDidChangeBusyEmitter = new Emitter<TreeNode>();
|
240 | readonly onDidChangeBusy = this.onDidChangeBusyEmitter.event;
|
241 |
|
242 | protected nodes: {
|
243 | [id: string]: Mutable<TreeNode> | undefined
|
244 | } = {};
|
245 |
|
246 | constructor() {
|
247 | this.toDispose.push(this.onChangedEmitter);
|
248 | this.toDispose.push(this.onNodeRefreshedEmitter);
|
249 | this.toDispose.push(this.onDidChangeBusyEmitter);
|
250 | }
|
251 |
|
252 | dispose(): void {
|
253 | this.nodes = {};
|
254 | this.toDispose.dispose();
|
255 | }
|
256 |
|
257 | get root(): TreeNode | undefined {
|
258 | return this._root;
|
259 | }
|
260 |
|
261 | protected toDisposeOnSetRoot = new DisposableCollection();
|
262 | set root(root: TreeNode | undefined) {
|
263 | this.toDisposeOnSetRoot.dispose();
|
264 | const cancelRefresh = new CancellationTokenSource();
|
265 | this.toDisposeOnSetRoot.push(cancelRefresh);
|
266 | this.nodes = {};
|
267 | this._root = root;
|
268 | this.addNode(root);
|
269 | this.refresh(undefined, cancelRefresh.token);
|
270 | }
|
271 |
|
272 | get onChanged(): Event<void> {
|
273 | return this.onChangedEmitter.event;
|
274 | }
|
275 |
|
276 | protected fireChanged(): void {
|
277 | this.onChangedEmitter.fire(undefined);
|
278 | }
|
279 |
|
280 | get onNodeRefreshed(): Event<CompositeTreeNode & WaitUntilEvent> {
|
281 | return this.onNodeRefreshedEmitter.event;
|
282 | }
|
283 |
|
284 | protected async fireNodeRefreshed(parent: CompositeTreeNode): Promise<void> {
|
285 | await WaitUntilEvent.fire(this.onNodeRefreshedEmitter, parent);
|
286 | this.fireChanged();
|
287 | }
|
288 |
|
289 | getNode(id: string | undefined): TreeNode | undefined {
|
290 | return id !== undefined ? this.nodes[id] : undefined;
|
291 | }
|
292 |
|
293 | validateNode(node: TreeNode | undefined): TreeNode | undefined {
|
294 | const id = !!node ? node.id : undefined;
|
295 | return this.getNode(id);
|
296 | }
|
297 |
|
298 | async refresh(raw?: CompositeTreeNode, cancellationToken?: CancellationToken): Promise<CompositeTreeNode | undefined> {
|
299 | const parent = !raw ? this._root : this.validateNode(raw);
|
300 | let result: CompositeTreeNode | undefined;
|
301 | if (CompositeTreeNode.is(parent)) {
|
302 | const busySource = new CancellationTokenSource();
|
303 | this.doMarkAsBusy(parent, 800, busySource.token);
|
304 | try {
|
305 | result = parent;
|
306 | const children = await this.resolveChildren(parent);
|
307 | if (cancellationToken?.isCancellationRequested) { return; }
|
308 | result = await this.setChildren(parent, children);
|
309 | if (cancellationToken?.isCancellationRequested) { return; }
|
310 | } finally {
|
311 | busySource.cancel();
|
312 | }
|
313 | }
|
314 | this.fireChanged();
|
315 | return result;
|
316 | }
|
317 |
|
318 | protected resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
|
319 | return Promise.resolve(Array.from(parent.children));
|
320 | }
|
321 |
|
322 | protected async setChildren(parent: CompositeTreeNode, children: TreeNode[]): Promise<CompositeTreeNode | undefined> {
|
323 | const root = this.getRootNode(parent);
|
324 | if (this.nodes[root.id] && this.nodes[root.id] !== root) {
|
325 | console.error(`Child node '${parent.id}' does not belong to this '${root.id}' tree.`);
|
326 | return undefined;
|
327 | }
|
328 | this.removeNode(parent);
|
329 | parent.children = children;
|
330 | this.addNode(parent);
|
331 | await this.fireNodeRefreshed(parent);
|
332 | return parent;
|
333 | }
|
334 |
|
335 | protected removeNode(node: TreeNode | undefined): void {
|
336 | if (CompositeTreeNode.is(node)) {
|
337 | node.children.forEach(child => this.removeNode(child));
|
338 | }
|
339 | if (node) {
|
340 | delete this.nodes[node.id];
|
341 | }
|
342 | }
|
343 |
|
344 | protected getRootNode(node: TreeNode): TreeNode {
|
345 | if (node.parent === undefined) {
|
346 | return node;
|
347 | } else {
|
348 | return this.getRootNode(node.parent);
|
349 | }
|
350 | }
|
351 |
|
352 | protected addNode(node: TreeNode | undefined): void {
|
353 | if (node) {
|
354 | this.nodes[node.id] = node;
|
355 | }
|
356 | if (CompositeTreeNode.is(node)) {
|
357 | const { children } = node;
|
358 | children.forEach((child, index) => {
|
359 | CompositeTreeNode.setParent(child, index, node);
|
360 | this.addNode(child);
|
361 | });
|
362 | }
|
363 | }
|
364 |
|
365 | async markAsBusy(raw: TreeNode, ms: number, token: CancellationToken): Promise<void> {
|
366 | const node = this.validateNode(raw);
|
367 | if (node) {
|
368 | await this.doMarkAsBusy(node, ms, token);
|
369 | }
|
370 | }
|
371 | protected async doMarkAsBusy(node: Mutable<TreeNode>, ms: number, token: CancellationToken): Promise<void> {
|
372 | try {
|
373 | await timeout(ms, token);
|
374 | this.doSetBusy(node);
|
375 | token.onCancellationRequested(() => this.doResetBusy(node));
|
376 | } catch {
|
377 |
|
378 | }
|
379 | }
|
380 | protected doSetBusy(node: Mutable<TreeNode>): void {
|
381 | const oldBusy = node.busy || 0;
|
382 | node.busy = oldBusy + 1;
|
383 | if (oldBusy === 0) {
|
384 | this.onDidChangeBusyEmitter.fire(node);
|
385 | }
|
386 | }
|
387 | protected doResetBusy(node: Mutable<TreeNode>): void {
|
388 | const oldBusy = node.busy || 0;
|
389 | if (oldBusy > 0) {
|
390 | node.busy = oldBusy - 1;
|
391 | if (node.busy === 0) {
|
392 | this.onDidChangeBusyEmitter.fire(node);
|
393 | }
|
394 | }
|
395 | }
|
396 |
|
397 | }
|