// // Base class for the sidebar's tree controllers. // // Apple Container has no event stream, so the sidebar polls. Reloading a // TreeView drops the user's selection, so each refresh compares a fingerprint // of the fetched data and only reloads when something actually changed. // class Controller { constructor(id) { this.id = id; this.elements = []; this.fingerprint = null; // Collapsed rather than expanded, so groups keep their default of open. this.collapsed = new Set(); this.disposables = new CompositeDisposable(); this.tree = new TreeView(id, { dataProvider: this }); this.disposables.add(this.tree); this.disposables.add( this.tree.onDidExpandElement((element) => { this.collapsed.delete(element.identifier); element.expanded = true; }) ); this.disposables.add( this.tree.onDidCollapseElement((element) => { this.collapsed.add(element.identifier); element.expanded = false; }) ); } // Subclasses return the elements to display, or null to leave the tree as // it is (used when the backing service is unavailable). async fetch() { return []; } // A stable string describing the current data. When it does not change, // the tree is left alone. signature(elements) { let describe = (element) => [element.identifier, element.revision]; return JSON.stringify( elements.map((element) => describe(element).concat([(element.children || []).map(describe)])) ); } get visible() { // TreeView.visible is only meaningful once the sidebar has been shown. return this.tree.visible !== false; } async refresh(options = {}) { let elements; try { elements = await this.fetch(); } catch (error) { if (nova.inDevMode()) { console.warn(`${this.id}: ${error.message}`); } elements = []; } if (elements === null) { return; } this.restoreExpansion(elements); let signature = this.signature(elements); if (!options.force && signature === this.fingerprint) { return; } this.fingerprint = signature; this.elements = elements; this.tree.reload(); } // Rebuilt models are fresh objects, so expansion has to be carried over. restoreExpansion(elements) { for (let element of elements) { if (!element.children || element.children.length === 0) { continue; } if (this.collapsed.has(element.identifier)) { element.expanded = false; } } } // Empties the tree, for when the backing service goes away. clear() { if (this.elements.length === 0) { return; } this.elements = []; this.fingerprint = null; this.tree.reload(); } // Invalidates the fingerprint so the next refresh always reloads. invalidate() { this.fingerprint = null; } reload() { this.invalidate(); return this.refresh({ force: true }); } get selection() { return this.tree.selection || []; } getChildren(element) { if (!element) { return this.elements; } return element.children || []; } getParent(element) { return element.parent; } getTreeItem() { throw new Error("getTreeItem must be implemented by the controller."); } register(command, handler) { this.disposables.add(nova.commands.register(command, handler)); } watch(key, handler) { this.disposables.add(nova.config.onDidChange(key, handler)); } dispose() { this.disposables.dispose(); } } module.exports = { Controller };