UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

425 lines (379 loc) • 14.3 kB
// ***************************************************************************** // Copyright (C) 2017 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { injectable } from 'inversify'; import { Event, Emitter, WaitUntilEvent } from '../../common/event'; import { Disposable, DisposableCollection } from '../../common/disposable'; import { CancellationToken, CancellationTokenSource } from '../../common/cancellation'; import { timeout } from '../../common/promise-util'; import { isObject, Mutable } from '../../common'; import { AccessibilityInformation } from '../../common/accessibility'; export const Tree = Symbol('Tree'); /** * The tree - an abstract data type. */ export interface Tree extends Disposable { /** * A root node of this tree. * Undefined if there is no root node. * Setting a root node refreshes the tree. */ root: TreeNode | undefined; /** * Emit when the tree is changed. */ readonly onChanged: Event<void>; /** * Return a node for the given identifier or undefined if such does not exist. */ getNode(id: string | undefined): TreeNode | undefined; /** * Return a valid node in this tree matching to the given; otherwise undefined. */ validateNode(node: TreeNode | undefined): TreeNode | undefined; /** * Refresh children of the root node. * * Return a valid refreshed composite root or `undefined` if such does not exist. */ refresh(): Promise<Readonly<CompositeTreeNode> | undefined>; /** * Refresh children of a node for the give node id if it is valid. * * Return a valid refreshed composite node or `undefined` if such does not exist. */ refresh(parent: Readonly<CompositeTreeNode>): Promise<Readonly<CompositeTreeNode> | undefined>; /** * Emit when the children of the given node are refreshed. */ readonly onNodeRefreshed: Event<Readonly<CompositeTreeNode> & WaitUntilEvent>; /** * Emits when the busy state of the given node is changed. */ readonly onDidChangeBusy: Event<TreeNode>; /** * Marks the give node as busy after a specified number of milliseconds. * A token source of the given token should be canceled to unmark. */ markAsBusy(node: Readonly<TreeNode>, ms: number, token: CancellationToken): Promise<void>; /** * An update to the tree node occurred, but the tree structure remains unchanged */ readonly onDidUpdate: Event<TreeNode[]>; markAsChecked(node: TreeNode, checked: boolean): void; } export interface TreeViewItemCheckboxInfo { checked: boolean; tooltip?: string; accessibilityInformation?: AccessibilityInformation } /** * The tree node. */ export interface TreeNode { /** * An unique id of this node. */ readonly id: string; /** * A human-readable name of this tree node. * * @deprecated use `LabelProvider.getName` instead or move this property to your tree node type */ readonly name?: string; /** * A css string for this tree node icon. * * @deprecated use `LabelProvider.getIcon` instead or move this property to your tree node type */ readonly icon?: string; /** * A human-readable description of this tree node. * * @deprecated use `LabelProvider.getLongName` instead or move this property to your tree node type */ readonly description?: string; /** * Test whether this node should be rendered. * If undefined then node will be rendered. */ readonly visible?: boolean; /** * A parent node of this tree node. * Undefined if this node is root. */ readonly parent: Readonly<CompositeTreeNode> | undefined; /** * A previous sibling of this tree node. */ readonly previousSibling?: TreeNode; /** * A next sibling of this tree node. */ readonly nextSibling?: TreeNode; /** * Whether this node is busy. Greater than 0 then busy; otherwise not. */ readonly busy?: number; /** * Whether this node is checked. */ readonly checkboxInfo?: TreeViewItemCheckboxInfo; } export namespace TreeNode { export function is(node: unknown): node is TreeNode { return isObject(node) && 'id' in node && 'parent' in node; } export function equals(left: TreeNode | undefined, right: TreeNode | undefined): boolean { return left === right || (!!left && !!right && left.id === right.id); } export function isVisible(node: TreeNode | undefined): boolean { return !!node && (node.visible === undefined || node.visible); } } /** * The composite tree node. */ export interface CompositeTreeNode extends TreeNode { /** * Child nodes of this tree node. */ children: ReadonlyArray<TreeNode>; } export namespace CompositeTreeNode { export function is(node: unknown): node is CompositeTreeNode { return isObject(node) && 'children' in node; } export function getFirstChild(parent: CompositeTreeNode): TreeNode | undefined { return parent.children[0]; } export function getLastChild(parent: CompositeTreeNode): TreeNode | undefined { return parent.children[parent.children.length - 1]; } export function isAncestor(parent: CompositeTreeNode, child: TreeNode | undefined): boolean { if (!child) { return false; } if (TreeNode.equals(parent, child.parent)) { return true; } return isAncestor(parent, child.parent); } export function indexOf(parent: CompositeTreeNode, node: TreeNode | undefined): number { if (!node) { return -1; } return parent.children.findIndex(child => TreeNode.equals(node, child)); } export function addChildren(parent: CompositeTreeNode, children: TreeNode[]): CompositeTreeNode { for (const child of children) { addChild(parent, child); } return parent; } export function addChild(parent: CompositeTreeNode, child: TreeNode): CompositeTreeNode { const children = parent.children as TreeNode[]; const index = children.findIndex(value => value.id === child.id); if (index !== -1) { children.splice(index, 1, child); setParent(child, index, parent); } else { children.push(child); setParent(child, parent.children.length - 1, parent); } return parent; } export function removeChild(parent: CompositeTreeNode, child: TreeNode): void { const children = parent.children as TreeNode[]; const index = children.findIndex(value => value.id === child.id); if (index === -1) { return; } children.splice(index, 1); const { previousSibling, nextSibling } = child; if (previousSibling) { Object.assign(previousSibling, { nextSibling }); } if (nextSibling) { Object.assign(nextSibling, { previousSibling }); } } export function setParent(child: TreeNode, index: number, parent: CompositeTreeNode): void { const previousSibling = parent.children[index - 1]; const nextSibling = parent.children[index + 1]; Object.assign(child, { parent, previousSibling, nextSibling }); if (previousSibling) { Object.assign(previousSibling, { nextSibling: child }); } if (nextSibling) { Object.assign(nextSibling, { previousSibling: child }); } } } /** * A default implementation of the tree. */ @injectable() export class TreeImpl implements Tree { protected _root: TreeNode | undefined; protected readonly onChangedEmitter = new Emitter<void>(); protected readonly onNodeRefreshedEmitter = new Emitter<CompositeTreeNode & WaitUntilEvent>(); protected readonly toDispose = new DisposableCollection(); protected readonly onDidChangeBusyEmitter = new Emitter<TreeNode>(); readonly onDidChangeBusy = this.onDidChangeBusyEmitter.event; protected readonly onDidUpdateEmitter = new Emitter<TreeNode[]>(); readonly onDidUpdate = this.onDidUpdateEmitter.event; protected nodes: { [id: string]: Mutable<TreeNode> | undefined } = {}; constructor() { this.toDispose.push(this.onChangedEmitter); this.toDispose.push(this.onNodeRefreshedEmitter); this.toDispose.push(this.onDidChangeBusyEmitter); } dispose(): void { this.nodes = {}; this.toDispose.dispose(); } get root(): TreeNode | undefined { return this._root; } protected toDisposeOnSetRoot = new DisposableCollection(); set root(root: TreeNode | undefined) { this.toDisposeOnSetRoot.dispose(); const cancelRefresh = new CancellationTokenSource(); this.toDisposeOnSetRoot.push(cancelRefresh); this.nodes = {}; this._root = root; this.addNode(root); this.refresh(undefined, cancelRefresh.token); } get onChanged(): Event<void> { return this.onChangedEmitter.event; } protected fireChanged(): void { this.onChangedEmitter.fire(undefined); } get onNodeRefreshed(): Event<CompositeTreeNode & WaitUntilEvent> { return this.onNodeRefreshedEmitter.event; } protected async fireNodeRefreshed(parent: CompositeTreeNode): Promise<void> { await WaitUntilEvent.fire(this.onNodeRefreshedEmitter, parent); this.fireChanged(); } getNode(id: string | undefined): TreeNode | undefined { return id !== undefined ? this.nodes[id] : undefined; } validateNode(node: TreeNode | undefined): TreeNode | undefined { const id = !!node ? node.id : undefined; return this.getNode(id); } async refresh(raw?: CompositeTreeNode, cancellationToken?: CancellationToken): Promise<CompositeTreeNode | undefined> { const parent = !raw ? this._root : this.validateNode(raw); let result: CompositeTreeNode | undefined; if (CompositeTreeNode.is(parent)) { const busySource = new CancellationTokenSource(); this.doMarkAsBusy(parent, 800, busySource.token); try { result = parent; const children = await this.resolveChildren(parent); if (cancellationToken?.isCancellationRequested) { return; } result = await this.setChildren(parent, children); if (cancellationToken?.isCancellationRequested) { return; } } finally { busySource.cancel(); } } this.fireChanged(); return result; } protected resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> { return Promise.resolve(Array.from(parent.children)); } protected async setChildren(parent: CompositeTreeNode, children: TreeNode[]): Promise<CompositeTreeNode | undefined> { const root = this.getRootNode(parent); if (this.nodes[root.id] && this.nodes[root.id] !== root) { console.error(`Child node '${parent.id}' does not belong to this '${root.id}' tree.`); return undefined; } this.removeNode(parent); parent.children = children; this.addNode(parent); await this.fireNodeRefreshed(parent); return parent; } protected removeNode(node: TreeNode | undefined): void { if (CompositeTreeNode.is(node)) { node.children.forEach(child => this.removeNode(child)); } if (node) { delete this.nodes[node.id]; } } protected getRootNode(node: TreeNode): TreeNode { if (node.parent === undefined) { return node; } else { return this.getRootNode(node.parent); } } protected addNode(node: TreeNode | undefined): void { if (node) { this.nodes[node.id] = node; } if (CompositeTreeNode.is(node)) { const { children } = node; children.forEach((child, index) => { CompositeTreeNode.setParent(child, index, node); this.addNode(child); }); } } async markAsBusy(raw: TreeNode, ms: number, token: CancellationToken): Promise<void> { const node = this.validateNode(raw); if (node) { await this.doMarkAsBusy(node, ms, token); } } markAsChecked(node: Mutable<TreeNode>, checked: boolean): void { node.checkboxInfo!.checked = checked; this.onDidUpdateEmitter.fire([node]); } protected async doMarkAsBusy(node: Mutable<TreeNode>, ms: number, token: CancellationToken): Promise<void> { try { await timeout(ms, token); this.doSetBusy(node); token.onCancellationRequested(() => this.doResetBusy(node)); } catch { /* no-op */ } } protected doSetBusy(node: Mutable<TreeNode>): void { const oldBusy = node.busy || 0; node.busy = oldBusy + 1; if (oldBusy === 0) { this.onDidChangeBusyEmitter.fire(node); } } protected doResetBusy(node: Mutable<TreeNode>): void { const oldBusy = node.busy || 0; if (oldBusy > 0) { node.busy = oldBusy - 1; if (node.busy === 0) { this.onDidChangeBusyEmitter.fire(node); } } } }