UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

642 lines (561 loc) 21.7 kB
Object.defineProperty(exports, '__esModule', { value: true }); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } /* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ var _MomoizedFieldsDeriver2; function _MomoizedFieldsDeriver() { return _MomoizedFieldsDeriver2 = require('./MomoizedFieldsDeriver'); } var _commonsNodeNuclideUri2; function _commonsNodeNuclideUri() { return _commonsNodeNuclideUri2 = _interopRequireDefault(require('../../commons-node/nuclideUri')); } var _immutable2; function _immutable() { return _immutable2 = _interopRequireDefault(require('immutable')); } var DEFAULT_OPTIONS = { isExpanded: false, isSelected: false, isFocused: false, isDragHovered: false, isLoading: false, wasFetched: false, isCwd: false, isTracked: false, children: new (_immutable2 || _immutable()).default.OrderedMap(), connectionTitle: '', subscription: null, highlightedText: '', matchesFilter: true }; /** * OVERVIEW * The FileTreeNode class is almost entirely immutable. Except for the parent and the sibling * links no properties are to be updated after the creation. * * The class contains multiple derived fields. The derived fields are calculated from the options, * from the configuration values and even from children's properties. Once calculated the properties * are immutable. This calculation is handled by a separate class - `MemoizedFieldsDeriver`. It * is optimized to avoid redundant recalculations. * * Setting any of the properties (except for the aforementioned links to parent and siblings) will * create a new instance of the class, with required properties set. If, however, the set operation * is a no-op (such if setting a property to the same value it already has), new instance creation * is not skipped and same instance is returned instead. * * * THE CONFIGURATION * Is the object passed to the constructor and conceptually shared among all * instances in a tree. Should be used for properties that make no sense to be owned by the tree * elements, yet such that affect the tree. Such as the configuration whether to use the prefix * navigation, for instance, or the currently configured Working Set. * The configuration object should be treated as immutable by its owner. Whenever a change occurs * method `updateConf()` has to be called on the root(s) of the tree to notify about the change. * This call would trigger complete reconstruction of the tree, to reflect the possibly changed * derived properties. * This gives another reason to use the configuration object sparingly - it is expensive to rebuild * the entire tree. * * * CHILDREN HANDLING * In order for the tree traversal and modifications to be efficient one often * needs to find the parent of a node. Parent property, however, can't be one of the node's immutable * fields, otherwise it'd create circular references. Therefore the parent property is never given * to the node's constructor, but rather set by the parent itself when the node is assigned to it. * This means that we must avoid the state when same node node is contained in the .children map * of several other nodes. As only the latest one it was assigned to is considered its parent from * the node's perspective. * * Just like the parent property, some operations require an ability to find siblings easily. * The previous and the next sibling properties are too set when a child is assigned to its parent. * * Some of the properties are derived from the properties of children. For example, it is * beneficial to know whether a node contains a selected node in its sub-tree and the size of the * visible sub-tree. * * All property derivation and links set-up is done with one traversal only over the children. */ var FileTreeNode = (function () { _createClass(FileTreeNode, null, [{ key: 'childrenFromArray', /** * The children property is an OrderedMap instance keyed by child's name property. * This convenience function would create such OrderedMap instance from a plain JS Array * of FileTreeNode instances */ value: function childrenFromArray(children) { return new (_immutable2 || _immutable()).default.OrderedMap(children.map(function (child) { return [child.name, child]; })); } /** * The _derivedChange param is not for external use. */ }]); function FileTreeNode(options, conf) { var _deriver = arguments.length <= 2 || arguments[2] === undefined ? null : arguments[2]; _classCallCheck(this, FileTreeNode); this.parent = null; this.nextSibling = null; this.prevSibling = null; this.conf = conf; this._assignOptions(options); this._deriver = _deriver || new (_MomoizedFieldsDeriver2 || _MomoizedFieldsDeriver()).MomoizedFieldsDeriver(options.uri, options.rootUri); var derived = this._deriver.buildDerivedFields(conf); this._assignDerived(derived); this._handleChildren(); } /** * Sets the links from the children to this instance (their parent) and the links between the * siblings. * Additionally calculates the properties derived from children and assigns them to this instance */ _createClass(FileTreeNode, [{ key: '_handleChildren', value: function _handleChildren() { var _this = this; var containsSelection = this.isSelected; var containsDragHover = this.isDragHovered; var containsTrackedNode = this.isTracked; var containsFilterMatches = this.matchesFilter; var shownChildrenBelow = this.shouldBeShown ? 1 : 0; var containsHidden = !this.shouldBeShown; var prevChild = null; this.children.forEach(function (c) { c.parent = _this; c.prevSibling = prevChild; if (prevChild != null) { prevChild.nextSibling = c; } prevChild = c; if (c.containsFilterMatches) { containsFilterMatches = true; } if (!containsSelection && c.containsSelection) { containsSelection = true; } if (!containsDragHover && c.containsDragHover) { containsDragHover = true; } if (!containsTrackedNode && c.containsTrackedNode) { containsTrackedNode = true; } if (_this.shouldBeShown && _this.isExpanded) { shownChildrenBelow += c.shownChildrenBelow; } if (!containsHidden && c.containsHidden) { containsHidden = true; } }); if (prevChild != null) { prevChild.nextSibling = null; } this.containsSelection = containsSelection; this.containsDragHover = containsDragHover; this.containsTrackedNode = containsTrackedNode; this.containsFilterMatches = containsFilterMatches; this.shownChildrenBelow = shownChildrenBelow; this.containsHidden = containsHidden; } /** * Using object.assign() was proven to be less performant than direct named assignment * Since in heavy updates, nodes are created by the thousands we need to keep the creation * flow performant. */ }, { key: '_assignOptions', value: function _assignOptions(options) { // Don't pass the 100 chars limit var o = options; var D = DEFAULT_OPTIONS; this.uri = o.uri; this.rootUri = o.rootUri; this.isExpanded = o.isExpanded !== undefined ? o.isExpanded : D.isExpanded; this.isSelected = o.isSelected !== undefined ? o.isSelected : D.isSelected; this.isFocused = o.isFocused !== undefined ? o.isFocused : D.isFocused; this.isDragHovered = o.isDragHovered !== undefined ? o.isDragHovered : D.isDragHovered; this.isLoading = o.isLoading !== undefined ? o.isLoading : D.isLoading; this.wasFetched = o.wasFetched !== undefined ? o.wasFetched : D.wasFetched; this.isTracked = o.isTracked !== undefined ? o.isTracked : D.isTracked; this.isCwd = o.isCwd !== undefined ? o.isCwd : D.isCwd; this.children = o.children !== undefined ? o.children : D.children; this.connectionTitle = o.connectionTitle !== undefined ? o.connectionTitle : D.connectionTitle; this.subscription = o.subscription !== undefined ? o.subscription : D.subscription; this.highlightedText = o.highlightedText !== undefined ? o.highlightedText : D.highlightedText; this.matchesFilter = o.matchesFilter !== undefined ? o.matchesFilter : D.matchesFilter; } /** * Using object.assign() was proven to be less performant than direct named assignment * Since in heavy updates, nodes are created by the thousands we need to keep the creation * flow performant. */ }, { key: '_assignDerived', value: function _assignDerived(derived) { this.isRoot = derived.isRoot; this.name = derived.name; this.hashKey = derived.hashKey; this.relativePath = derived.relativePath; this.localPath = derived.localPath; this.isContainer = derived.isContainer; this.shouldBeShown = derived.shouldBeShown; this.shouldBeSoftened = derived.shouldBeSoftened; this.vcsStatusCode = derived.vcsStatusCode; this.repo = derived.repo; this.isIgnored = derived.isIgnored; this.checkedStatus = derived.checkedStatus; } /** * When modifying some of the properties a new instance needs to be created with all of the * properties identical except for those being modified. This method creates the baseline options * instance */ }, { key: '_buildOptions', value: function _buildOptions() { return { uri: this.uri, rootUri: this.rootUri, isExpanded: this.isExpanded, isSelected: this.isSelected, isFocused: this.isFocused, isDragHovered: this.isDragHovered, isLoading: this.isLoading, wasFetched: this.wasFetched, isTracked: this.isTracked, isCwd: this.isCwd, children: this.children, connectionTitle: this.connectionTitle, subscription: this.subscription, highlightedText: this.highlightedText, matchesFilter: this.matchesFilter }; } }, { key: 'setIsExpanded', value: function setIsExpanded(isExpanded) { return this.set({ isExpanded: isExpanded }); } }, { key: 'setIsSelected', value: function setIsSelected(isSelected) { return this.set({ isSelected: isSelected }); } }, { key: 'setIsFocused', value: function setIsFocused(isFocused) { return this.set({ isFocused: isFocused }); } }, { key: 'setIsDragHovered', value: function setIsDragHovered(isDragHovered) { return this.set({ isDragHovered: isDragHovered }); } }, { key: 'setIsLoading', value: function setIsLoading(isLoading) { return this.set({ isLoading: isLoading }); } }, { key: 'setIsTracked', value: function setIsTracked(isTracked) { return this.set({ isTracked: isTracked }); } }, { key: 'setIsCwd', value: function setIsCwd(isCwd) { return this.set({ isCwd: isCwd }); } }, { key: 'setChildren', value: function setChildren(children) { return this.set({ children: children }); } /** * Notifies the node about the change that happened in the configuration object. Will trigger * the complete reconstruction of the entire tree branch */ }, { key: 'updateConf', value: function updateConf() { var _this2 = this; var children = this.children.map(function (c) { return c.updateConf(_this2.conf); }); return this.newNode({ children: children }, this.conf); } /** * Used to modify several properties at once and skip unnecessary construction of intermediate * instances. For example: * const newNode = node.set({isExpanded: true, isSelected: false}); */ }, { key: 'set', value: function set(props) { if (this._propsAreTheSame(props)) { return this; } return this.newNode(props, this.conf); } /** * Performs an update of a tree branch. Receives two optional predicates * * The `prePredicate` is invoked at pre-descent. If the predicate returns a non-null * value it signifies that the handling of the sub-branch is complete and the descent to children * is not performed. * * The `postPredicate` is invoked on the way up. It has to return a non-null node, but it may * be the same instance as it was called with. */ }, { key: 'setRecursive', value: function setRecursive(prePredicate) { var postPredicate = arguments.length <= 1 || arguments[1] === undefined ? function (n) { return n; } : arguments[1]; if (prePredicate != null) { var newNode = prePredicate(this); if (newNode != null) { return postPredicate(newNode); } } var children = this.children.map(function (child) { return child.setRecursive(prePredicate, postPredicate); }); return postPredicate(this.setChildren(children)); } /** * Updates a single child in the map of children. The method only receives the new child instance * and the retrieval in from the map is performed by the child's name. This uses the fact * that children names (derived from their uris) are unmodifiable. Thus we won't ever have a * problem locating the value that we need to replace. */ }, { key: 'updateChild', value: function updateChild(newChild) { var children = this.children.set(newChild.name, newChild); return this.set({ children: children }); } /** * A hierarchical equivalent of forEach. The method receives two predicates * The first is invoked upon descent and with its return value controls whether need to traverse * deeper into the tree. True - descend, False - don't. */ }, { key: 'traverse', value: function traverse(preCallback) { var postCallback = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1]; var descend = preCallback(this); if (descend) { this.children.forEach(function (child) { return child.traverse(preCallback, postCallback); }); } postCallback(this); } /** * Looks for a node with the given URI in the sub branch - returns null if not found */ }, { key: 'find', value: function find(uri) { var deepestFound = this.findDeepest(uri); if (deepestFound == null || deepestFound.uri !== uri) { return null; } return deepestFound; } /** * Looks for a node with the given URI in the sub branch - returns the deepest found ancesstor * of the node being looked for. * Returns null if the node can not belong to the sub-branch */ }, { key: 'findDeepest', value: function findDeepest(uri) { if (!uri.startsWith(this.uri)) { return null; } if (uri === this.uri) { return this; } var childNamePath = (_commonsNodeNuclideUri2 || _commonsNodeNuclideUri()).default.split((_commonsNodeNuclideUri2 || _commonsNodeNuclideUri()).default.relative(this.uri, uri)); return this._findLastByNamePath(childNamePath); } /** * Finds the next node in the tree in the natural order - from top to to bottom as is displayed * in the file-tree panel, minus the indentation. Only the nodes that should be shown are returned. */ }, { key: 'findNext', value: function findNext() { if (!this.shouldBeShown) { if (this.parent != null) { return this.parent.findNext(); } return null; } if (this.shownChildrenBelow > 1) { return this.children.find(function (c) { return c.shouldBeShown; }); } // Not really an alias, but an iterating reference var it = this; while (it != null) { var nextShownSibling = it.findNextShownSibling(); if (nextShownSibling != null) { return nextShownSibling; } it = it.parent; } return null; } }, { key: 'findNextShownSibling', value: function findNextShownSibling() { var it = this.nextSibling; while (it != null && !it.shouldBeShown) { it = it.nextSibling; } return it; } /** * Finds the previous node in the tree in the natural order - from top to to bottom as is displayed * in the file-tree panel, minus the indentation. Only the nodes that should be shown are returned. */ }, { key: 'findPrevious', value: function findPrevious() { if (!this.shouldBeShown) { if (this.parent != null) { return this.parent.findPrevious(); } return null; } var prevShownSibling = this.findPrevShownSibling(); if (prevShownSibling != null) { return prevShownSibling.findLastRecursiveChild(); } return this.parent; } }, { key: 'findPrevShownSibling', value: function findPrevShownSibling() { var it = this.prevSibling; while (it != null && !it.shouldBeShown) { it = it.prevSibling; } return it; } /** * Returns the last shown descendant according to the natural tree order as is to be displayed by * the file-tree panel. (Last child of the last child of the last child...) * Or null, if none are found */ }, { key: 'findLastRecursiveChild', value: function findLastRecursiveChild() { if (!this.isContainer || !this.isExpanded || this.children.isEmpty()) { return this; } var it = this.children.last(); while (!it.shouldBeShown && it != null) { it = it.prevSibling; } if (it == null) { if (this.shouldBeShown) { return this; } return this.findPrevious(); } else { return it.findLastRecursiveChild(); } } }, { key: 'getDepth', value: function getDepth() { var it = this.parent; var depth = 0; while (it != null) { it = it.parent; depth++; } return depth; } }, { key: '_propsAreTheSame', value: function _propsAreTheSame(props) { if (props.isSelected !== undefined && this.isSelected !== props.isSelected) { return false; } if (props.isFocused !== undefined && this.isFocused !== props.isFocused) { return false; } if (props.isDragHovered !== undefined && this.isDragHovered !== props.isDragHovered) { return false; } if (props.isTracked !== undefined && this.isTracked !== props.isTracked) { return false; } if (props.isExpanded !== undefined && this.isExpanded !== props.isExpanded) { return false; } if (props.isLoading !== undefined && this.isLoading !== props.isLoading) { return false; } if (props.wasFetched !== undefined && this.wasFetched !== props.wasFetched) { return false; } if (props.isCwd !== undefined && this.isCwd !== props.isCwd) { return false; } if (props.subscription !== undefined && this.subscription !== props.subscription) { return false; } if (props.highlightedText !== undefined && this.highlightedText !== props.highlightedText) { return false; } if (props.matchesFilter !== undefined && this.matchesFilter !== props.matchesFilter) { return false; } if (props.children !== undefined && props.children !== this.children && !(_immutable2 || _immutable()).default.is(this.children, props.children)) { return false; } return true; } }, { key: 'newNode', value: function newNode(props, conf) { return new FileTreeNode(_extends({}, this._buildOptions(), props), conf, this._deriver); } }, { key: '_findLastByNamePath', value: function _findLastByNamePath(childNamePath) { if (childNamePath.length === 0) { return this; } var child = this.children.get(childNamePath[0]); if (child == null) { return this; } return child._findLastByNamePath(childNamePath.slice(1)); } }]); return FileTreeNode; })(); exports.FileTreeNode = FileTreeNode; // Mutable properties - set when the node is assigned to its parent (and are immutable after) // Derived // Derived from children