UNPKG

13.4 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2017 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { injectable } from 'inversify';
18import { Event, Emitter, WaitUntilEvent } from '../../common/event';
19import { Disposable, DisposableCollection } from '../../common/disposable';
20import { CancellationToken, CancellationTokenSource } from '../../common/cancellation';
21import { timeout } from '../../common/promise-util';
22import { isObject, Mutable } from '../../common';
23
24export const Tree = Symbol('Tree');
25
26/**
27 * The tree - an abstract data type.
28 */
29export interface Tree extends Disposable {
30 /**
31 * A root node of this tree.
32 * Undefined if there is no root node.
33 * Setting a root node refreshes the tree.
34 */
35 root: TreeNode | undefined;
36 /**
37 * Emit when the tree is changed.
38 */
39 readonly onChanged: Event<void>;
40 /**
41 * Return a node for the given identifier or undefined if such does not exist.
42 */
43 getNode(id: string | undefined): TreeNode | undefined;
44 /**
45 * Return a valid node in this tree matching to the given; otherwise undefined.
46 */
47 validateNode(node: TreeNode | undefined): TreeNode | undefined;
48 /**
49 * Refresh children of the root node.
50 *
51 * Return a valid refreshed composite root or `undefined` if such does not exist.
52 */
53 refresh(): Promise<Readonly<CompositeTreeNode> | undefined>;
54 /**
55 * Refresh children of a node for the give node id if it is valid.
56 *
57 * Return a valid refreshed composite node or `undefined` if such does not exist.
58 */
59 refresh(parent: Readonly<CompositeTreeNode>): Promise<Readonly<CompositeTreeNode> | undefined>;
60 /**
61 * Emit when the children of the given node are refreshed.
62 */
63 readonly onNodeRefreshed: Event<Readonly<CompositeTreeNode> & WaitUntilEvent>;
64 /**
65 * Emits when the busy state of the given node is changed.
66 */
67 readonly onDidChangeBusy: Event<TreeNode>;
68 /**
69 * Marks the give node as busy after a specified number of milliseconds.
70 * A token source of the given token should be canceled to unmark.
71 */
72 markAsBusy(node: Readonly<TreeNode>, ms: number, token: CancellationToken): Promise<void>;
73}
74
75/**
76 * The tree node.
77 */
78export interface TreeNode {
79 /**
80 * An unique id of this node.
81 */
82 readonly id: string;
83 /**
84 * A human-readable name of this tree node.
85 *
86 * @deprecated use `LabelProvider.getName` instead or move this property to your tree node type
87 */
88 readonly name?: string;
89 /**
90 * A css string for this tree node icon.
91 *
92 * @deprecated use `LabelProvider.getIcon` instead or move this property to your tree node type
93 */
94 readonly icon?: string;
95 /**
96 * A human-readable description of this tree node.
97 *
98 * @deprecated use `LabelProvider.getLongName` instead or move this property to your tree node type
99 */
100 readonly description?: string;
101 /**
102 * Test whether this node should be rendered.
103 * If undefined then node will be rendered.
104 */
105 readonly visible?: boolean;
106 /**
107 * A parent node of this tree node.
108 * Undefined if this node is root.
109 */
110 readonly parent: Readonly<CompositeTreeNode> | undefined;
111 /**
112 * A previous sibling of this tree node.
113 */
114 readonly previousSibling?: TreeNode;
115 /**
116 * A next sibling of this tree node.
117 */
118 readonly nextSibling?: TreeNode;
119 /**
120 * Whether this node is busy. Greater than 0 then busy; otherwise not.
121 */
122 readonly busy?: number;
123}
124
125export 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 * The composite tree node.
141 */
142export interface CompositeTreeNode extends TreeNode {
143 /**
144 * Child nodes of this tree node.
145 */
146 children: ReadonlyArray<TreeNode>;
147}
148
149export 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 * A default implementation of the tree.
230 */
231@injectable()
232export 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 /* no-op */
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}