var selectionViewInfo = this.props.selectionViewInfo,

width = selectionViewInfo.get("width") - this.props.widthAdjustment,

/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*global define, $*/
/*unittests: FileTreeView*/

/**
 * This is the view layer (template) for the file tree in the sidebar. It takes a FileTreeViewModel
 * and renders it to the given element using React. User actions are signaled via an ActionCreator
 * (in the Flux sense).
 */
define(function (require, exports, module) {
    "use strict";

    var React             = require("thirdparty/react"),
        Immutable         = require("thirdparty/immutable"),
        _                 = require("thirdparty/lodash"),
        FileUtils         = require("file/FileUtils"),
        FileTreeViewModel = require("project/FileTreeViewModel"),
        ViewUtils         = require("utils/ViewUtils"),
        KeyEvent          = require("utils/KeyEvent");

    var DOM = React.DOM;

    /**
     * @private
     * @type {Immutable.Map}
     *
     * Stores the file tree extensions for adding classes and icons. The keys of the map
     * are the "categories" of the extensions and values are vectors of the callback functions.
     */
    var _extensions = Immutable.Map();

    // Constants

    // Time range from first click to second click to invoke renaming.
    var CLICK_RENAME_MINIMUM  = 500,
        RIGHT_MOUSE_BUTTON    = 2,
        LEFT_MOUSE_BUTTON     = 0;

    /**
     * @private
     *
     * Returns the name of a file without its extension.
     *
     * @param {string} fullname The complete name of the file (not including the rest of the path)
     * @param {string} extension The file extension
     * @return {string} The fullname without the extension
     */
    function _getName(fullname, extension) {
        return extension !== "" ? fullname.substring(0, fullname.length - extension.length - 1) : fullname;
    }

    /**
     * Mixin that allows a component to compute the full path to its directory entry.
     */
    var pathComputer = {
        /**
         * Computes the full path of the file represented by this input.
         */
        myPath: function () {
            var result = this.props.parentPath + this.props.name;

            // Add trailing slash for directories
            if (!FileTreeViewModel.isFile(this.props.entry) && _.last(result) !== "/") {
                result += "/";
            }

            return result;
        }
    };

    /**
     * This is a mixin that provides rename input behavior. It is responsible for taking keyboard input
     * and invoking the correct action based on that input.
     */
    var renameBehavior = {
        /**
         * If the user presses enter or escape, we either successfully complete or cancel, respectively,
         * the rename or create operation that is underway.
         */
        handleKeyDown: function (e) {
            if (e.keyCode === KeyEvent.DOM_VK_ESCAPE) {
                this.props.actions.cancelRename();
            } else if (e.keyCode === KeyEvent.DOM_VK_RETURN) {
                this.props.actions.performRename();
            }
        },

        /**
         * The rename or create operation can be completed or canceled by actions outside of
         * this component, so we keep the model up to date by sending every update via an action.
         */
        handleKeyUp: function () {
            this.props.actions.setRenameValue(this.refs.name.getDOMNode().value.trim());
        }
    };

    /**
     * @private
     *
     * Gets an appropriate width given the text provided.
     *
     * @param {string} text Text to measure
     * @return {int} Width to use
     */
    function _measureText(text) {
        var measuringElement = $("<div />", { css : { "position" : "absolute", "top" : "-200px", "left" : ("-1000px"), "visibility" : "hidden" } }).appendTo("body");
        measuringElement.text("pW" + text);
        var width = measuringElement.width();
        measuringElement.remove();
        return width;
    }

    /**
     * @private
     *
     * This component presents an input field to the user for renaming a file.
     *
     * Props:
     * * parentPath: the full path of the directory containing this file
     * * name: the name of the file, including the extension
     * * actions: the action creator responsible for communicating actions the user has taken
     */
    var fileRenameInput = React.createClass({
        mixins: [renameBehavior],

        /**
         * When this component is displayed, we scroll it into view and select the portion
         * of the filename that excludes the extension.
         */
        componentDidMount: function () {
            var fullname = this.props.name,
                extension = FileUtils.getSmartFileExtension(fullname);

            var node = this.refs.name.getDOMNode();
            node.setSelectionRange(0, _getName(fullname, extension).length);
            ViewUtils.scrollElementIntoView($("#project-files-container"), $(node), true);
        },

        render: function () {
            var width = _measureText(this.props.name);

            return DOM.input({
                className: "jstree-rename-input " + this.props.fileClasses,
                type: "text",
                defaultValue: this.props.name,
                autoFocus: true,
                onKeyDown: this.handleKeyDown,
                onKeyUp: this.handleKeyUp,
                style: {
                    width: width
                },
                ref: "name"
            });
        }
    });

    /**
     * @private
     *
     * This mixin handles right click (or control click on Mac) action to make a file
     * the "context" object for performing operations like rename.
     */
    var contextSettable = {

        /**
         * Send matching mouseDown events to the action creator as a setContext action.
         */
        handleMouseDown: function (e) {
            if (e.button === RIGHT_MOUSE_BUTTON ||
                    (this.props.platform === "mac" && e.button === LEFT_MOUSE_BUTTON && e.ctrlKey)) {
                this.props.actions.setContext(this.myPath());
                return false;
            }
        }
    };

    /**
     * @private
     *
     * Returns true if the value is defined (used in `.filter`)
     *
     * @param {Object} value value to test
     * @return {boolean} true if value is defined
     */
    function isDefined(value) {
        return value !== undefined;
    }

    /**
     * Mixin for components that support the "icons" and "addClass" extension points.
     * `fileNode` and `directoryNode` support this.
     */
    var extendable = {

        /**
         * Calls the icon providers to get the collection of icons (most likely just one) for
         * the current file or directory.
         *
         * @return {Array.<ReactComponent>} icon components to render
         */
        getIcons: function () {
            var result,
                extensions = this.props.extensions;

            if (extensions && extensions.get("icons")) {
                var data = this.getDataForExtension();
                result = extensions.get("icons").map(function (callback) {
                    try {
                        var result = callback(data);
                        if (!React.isValidComponent(result)) {
                            result = React.DOM.span({
                                dangerouslySetInnerHTML: {
                                    __html: $(result)[0].outerHTML
                                }
                            });
                        }
                        return result;
                    } catch (e) {
                        console.warn("Exception thrown in FileTreeView icon provider:", e);
                    }
                }).filter(isDefined).toArray();
            }

            if (!result || result.length === 0) {
                result = [DOM.ins({
                    className: "jstree-icon"
                }, " ")];
            }
            return result;
        },

        /**
         * Calls the addClass providers to get the classes (in string form) to add for the current
         * file or directory.
         *
         * @param {string} classes Initial classes for this node
         * @return {string} classes for the current node
         */
        getClasses: function (classes) {
            var extensions = this.props.extensions;

            if (extensions && extensions.get("addClass")) {
                var data = this.getDataForExtension();
                classes = classes + " " + extensions.get("addClass").map(function (callback) {
                    try {
                        return callback(data);
                    } catch (e) {
                        console.warn("Exception thrown in FileTreeView addClass provider:", e);
                    }
                }).filter(isDefined).toArray().join(" ");
            }

            return classes;
        }
    };

    /**
     * @private
     *
     * Component to display a file in the tree.
     *
     * Props:
     * * parentPath: the full path of the directory containing this file
     * * name: the name of the file, including the extension
     * * entry: the object with the relevant metadata for the file (whether it's selected or is the context file)
     * * actions: the action creator responsible for communicating actions the user has taken
     * * extensions: registered extensions for the file tree
     * * forceRender: causes the component to run render
     */
    var fileNode = React.createClass({
        mixins: [contextSettable, pathComputer, extendable],

        /**
         * Ensures that we always have a state object.
         */
        getInitialState: function () {
            return {
                clickTimer: null
            };
        },

        /**
         * Thanks to immutable objects, we can just do a start object identity check to know
         * whether or not we need to re-render.
         */
        shouldComponentUpdate: function (nextProps, nextState) {
            return nextProps.forceRender ||
                this.props.entry !== nextProps.entry ||
                this.props.extensions !== nextProps.extensions;
        },

        /**
         * If this node is newly selected, scroll it into view. Also, move the selection or
         * context boxes as appropriate.
         */
        componentDidUpdate: function (prevProps, prevState) {
            var wasSelected = prevProps.entry.get("selected"),
                isSelected  = this.props.entry.get("selected");

            if (isSelected && !wasSelected) {
                // TODO: This shouldn't really know about project-files-container
                // directly. It is probably the case that our React tree should actually
                // start with project-files-container instead of just the interior of
                // project-files-container and then the file tree will be one self-contained
                // functional unit.
                ViewUtils.scrollElementIntoView($("#project-files-container"), $(this.getDOMNode()), true);
            } else if (!isSelected && wasSelected && this.state.clickTimer !== null) {
                this.clearTimer();
            }
        },

        clearTimer: function () {
            if (this.state.clickTimer !== null) {
                window.clearTimeout(this.state.clickTimer);
                this.setState({
                    clickTimer: null
                });
            }
        },

        startRename: function () {
            if (!this.props.entry.get("rename")) {
                this.props.actions.startRename(this.myPath());
            }
            this.clearTimer();
        },

        /**
         * When the user clicks on the node, we'll either select it or, if they've clicked twice
         * with a bit of delay in between, we'll invoke the `startRename` action.
         */
        handleClick: function (e) {
            // If we're renaming, allow the click to go through to the rename input.
            if (this.props.entry.get("rename")) {
                return true;
            }

            if (e.button !== LEFT_MOUSE_BUTTON) {
                return;
            }

            if (this.props.entry.get("selected")) {
                if (this.state.clickTimer === null && !this.props.entry.get("rename")) {
                    var timer = window.setTimeout(this.startRename, CLICK_RENAME_MINIMUM);
                    this.setState({
                        clickTimer: timer
                    });
                }
            } else {
                this.props.actions.setSelected(this.myPath());
            }
            return false;
        },

        /**
         * When the user double clicks, we will select this file and add it to the working
         * set (via the `selectInWorkingSet` action.)
         */
        handleDoubleClick: function () {
            if (!this.props.entry.get("rename")) {
                if (this.state.clickTimer !== null) {
                    this.clearTimer();
                }
                this.props.actions.selectInWorkingSet(this.myPath());
            }
        },

        /**
         * Create the data object to pass to extensions.
         *
         * @return {{name: {string}, isFile: {boolean}, fullPath: {string}}} Data for extensions
         */
        getDataForExtension: function () {
            return {
                name: this.props.name,
                isFile: true,
                fullPath: this.myPath()
            };
        },

        render: function () {
            var fullname = this.props.name,
                extension = FileUtils.getSmartFileExtension(fullname),
                name = _getName(fullname, extension);

            if (extension) {
                extension = DOM.span({
                    className: "extension"
                }, "." + extension);
            }

            var nameDisplay,
                cx = React.addons.classSet;

            var fileClasses = cx({
                'jstree-clicked selected-node': this.props.entry.get("selected"),
                'context-node': this.props.entry.get("context")
            });

            if (this.props.entry.get("rename")) {
                nameDisplay = fileRenameInput({
                    actions: this.props.actions,
                    entry: this.props.entry,
                    name: this.props.name,
                    parentPath: this.props.parentPath,
                    fileClasses: fileClasses
                });
            } else {
                // Need to flatten the argument list because getIcons returns an array
                var aArgs = _.flatten([{
                    href: "#",
                    className: fileClasses
                }, this.getIcons(), name, extension], true);
                nameDisplay = DOM.a.apply(DOM.a, aArgs);
            }

            return DOM.li({
                className: this.getClasses("jstree-leaf"),
                onClick: this.handleClick,
                onMouseDown: this.handleMouseDown,
                onDoubleClick: this.handleDoubleClick
            },
                DOM.ins({
                    className: "jstree-icon"
                }, " "),
                nameDisplay);
        }
    });

    /**
     * @private
     *
     * Creates a comparison function for sorting a directory's contents with directories
     * appearing before files.
     *
     * We're sorting the keys of the directory (the names) based partly on the values,
     * so we use a closure to capture the map itself so that we can look up the
     * values as needed.
     *
     * @param {Immutable.Map} contents The directory's contents
     * @return {function(string,string)} Comparator that sorts directories first.
     */
    function _buildDirsFirstComparator(contents) {
        function _dirsFirstCompare(a, b) {
            var aIsFile = FileTreeViewModel.isFile(contents.get(a)),
                bIsFile = FileTreeViewModel.isFile(contents.get(b));

            if (!aIsFile && bIsFile) {
                return -1;
            } else if (aIsFile && !bIsFile) {
                return 1;
            } else {
                return FileUtils.compareFilenames(a, b);
            }
        }
        return _dirsFirstCompare;
    }

    /**
     * @private
     *
     * Sort a directory either alphabetically or with subdirectories listed first.
     *
     * @param {Immutable.Map} contents the directory's contents
     * @param {boolean} dirsFirst true to sort subdirectories first
     * @return {Immutable.Map} sorted mapping
     */
    function _sortDirectoryContents(contents, dirsFirst) {
        if (dirsFirst) {
            return contents.keySeq().sort(_buildDirsFirstComparator(contents));
        } else {
            return contents.keySeq().sort(FileUtils.compareFilenames);
        }
    }

    // Forward references to keep JSLint happy.
    var directoryNode, directoryContents;

    /**
     * @private
     *
     * Component that provides the input for renaming a directory.
     *
     * Props:
     * * parentPath: the full path of the directory containing this file
     * * name: the name of the file, including the extension
     * * actions: the action creator responsible for communicating actions the user has taken
     */
    var directoryRenameInput = React.createClass({
        mixins: [renameBehavior],

        render: function () {
            var width = _measureText(this.props.name);

            return DOM.input({
                className: "jstree-rename-input",
                type: "text",
                defaultValue: this.props.name,
                autoFocus: true,
                onKeyDown: this.handleKeyDown,
                onKeyUp: this.handleKeyUp,
                ref: "name",
                style: {
                    width: width
                }
            });
        }
    });

    /**
     * @private
     *
     * Displays a directory (but not its contents) in the tree.
     *
     * Props:
     * * parentPath: the full path of the directory containing this file
     * * name: the name of the directory
     * * entry: the object with the relevant metadata for the file (whether it's selected or is the context file)
     * * actions: the action creator responsible for communicating actions the user has taken
     * * sortDirectoriesFirst: whether the directories should be displayed first when listing the contents of a directory
     * * extensions: registered extensions for the file tree
     * * forceRender: causes the component to run render
     */
    directoryNode = React.createClass({
        mixins: [contextSettable, pathComputer, extendable],

        /**
         * We need to update this component if the sort order changes or our entry object
         * changes. Thanks to immutability, if any of the directory contents change, our
         * entry object will change.
         */
        shouldComponentUpdate: function (nextProps, nextState) {
            return nextProps.forceRender ||
                this.props.entry !== nextProps.entry ||
                this.props.sortDirectoriesFirst !== nextProps.sortDirectoriesFirst ||
                this.props.extensions !== nextProps.extensions;
        },

        /**
         * If you click on a directory, it will toggle between open and closed.
         */
        handleClick: function (event) {
            if (event.button !== LEFT_MOUSE_BUTTON) {
                return;
            }

            var isOpen = this.props.entry.get("open"),
                setOpen = isOpen ? false : true;

            if (event.metaKey || event.ctrlKey) {
                // ctrl-alt-click toggles this directory and its children
                if (event.altKey) {
                    if (setOpen) {
                        // when opening, we only open the immediate children because
                        // opening a whole subtree could be really slow (consider
                        // a `node_modules` directory, for example).
                        this.props.actions.toggleSubdirectories(this.myPath(), setOpen);
                        this.props.actions.setDirectoryOpen(this.myPath(), setOpen);
                    } else {
                        // When closing, we recursively close the whole subtree.
                        this.props.actions.closeSubtree(this.myPath());
                    }
                } else {
                    // ctrl-click toggles the sibling directories
                    this.props.actions.toggleSubdirectories(this.props.parentPath, setOpen);
                }
            } else {
                // directory toggle with no modifier
                this.props.actions.setDirectoryOpen(this.myPath(), setOpen);
            }
            return false;
        },

        /**
         * Create the data object to pass to extensions.
         *
         * @return {{name: {string}, isFile: {boolean}, fullPath: {string}}} Data for extensions
         */
        getDataForExtension: function () {
            return {
                name: this.props.name,
                isFile: false,
                fullPath: this.myPath()
            };
        },

        render: function () {
            var entry = this.props.entry,
                nodeClass,
                childNodes,
                children = entry.get("children"),
                isOpen = entry.get("open"),
                directoryClasses = "";

            if (isOpen && children) {
                nodeClass = "open";
                childNodes = directoryContents({
                    parentPath: this.myPath(),
                    contents: children,
                    extensions: this.props.extensions,
                    actions: this.props.actions,
                    forceRender: this.props.forceRender,
                    platform: this.props.platform,
                    sortDirectoriesFirst: this.props.sortDirectoriesFirst
                });
            } else {
                nodeClass = "closed";
            }

            if (this.props.entry.get("selected")) {
                directoryClasses += " jstree-clicked sidebar-selection";
            }

            if (entry.get("context")) {
                directoryClasses += " context-node";
            }

            var nameDisplay;
            if (entry.get("rename")) {
                nameDisplay = directoryRenameInput({
                    actions: this.props.actions,
                    entry: this.props.entry,
                    name: this.props.name,
                    parentPath: this.props.parentPath
                });
            } else {
                // Need to flatten the arguments because getIcons returns an array
                var aArgs = _.flatten([{
                    href: "#",
                    className: directoryClasses
                }, this.getIcons(), this.props.name], true);
                nameDisplay = DOM.a.apply(DOM.a, aArgs);
            }

            return DOM.li({
                className: this.getClasses("jstree-" + nodeClass),
                onClick: this.handleClick,
                onMouseDown: this.handleMouseDown
            },
                DOM.ins({
                    className: "jstree-icon"
                }, " "),
                nameDisplay,
                childNodes);
        }
    });

    /**
     * @private
     *
     * Displays the contents of a directory.
     *
     * Props:
     * * isRoot: whether this directory is the root of the tree
     * * parentPath: the full path of the directory containing this file
     * * contents: the map of name/child entry pairs for this directory
     * * actions: the action creator responsible for communicating actions the user has taken
     * * sortDirectoriesFirst: whether the directories should be displayed first when listing the contents of a directory
     * * extensions: registered extensions for the file tree
     * * forceRender: causes the component to run render
     */
    directoryContents = React.createClass({

        /**
         * Need to re-render if the sort order or the contents change.
         */
        shouldComponentUpdate: function (nextProps, nextState) {
            return nextProps.forceRender ||
                this.props.contents !== nextProps.contents ||
                this.props.sortDirectoriesFirst !== nextProps.sortDirectoriesFirst ||
                this.props.extensions !== nextProps.extensions;
        },

        render: function () {
            var extensions = this.props.extensions,
                iconClass = extensions && extensions.get("icons") ? "jstree-icons" : "jstree-no-icons",
                ulProps = this.props.isRoot ? {
                    className: "jstree-brackets jstree-no-dots " + iconClass
                } : null;

            var contents = this.props.contents,
                namesInOrder = _sortDirectoryContents(contents, this.props.sortDirectoriesFirst);

            return DOM.ul(ulProps, namesInOrder.map(function (name) {
                var entry = contents.get(name);

                if (FileTreeViewModel.isFile(entry)) {
                    return fileNode({
                        parentPath: this.props.parentPath,
                        name: name,
                        entry: entry,
                        actions: this.props.actions,
                        extensions: this.props.extensions,
                        forceRender: this.props.forceRender,
                        platform: this.props.platform,
                        key: name
                    });
                } else {
                    return directoryNode({
                        parentPath: this.props.parentPath,
                        name: name,
                        entry: entry,
                        actions: this.props.actions,
                        extensions: this.props.extensions,
                        sortDirectoriesFirst: this.props.sortDirectoriesFirst,
                        forceRender: this.props.forceRender,
                        platform: this.props.platform,
                        key: name
                    });
                }
            }.bind(this)).toArray());
        }
    });

    /**
     * Displays the absolutely positioned box for the selection or context in the
     * file tree. Its position is determined by passed-in info about the scroller in which
     * the tree resides and the top of the selected node (as reported by the node itself).
     *
     * Props:
     * * selectionViewInfo: Immutable.Map with width, scrollTop, scrollLeft and offsetTop for the tree container
     * * visible: should this be visible now
     * * widthAdjustment: if this box should not fill the entire width, pass in a positive number here which is subtracted from the width in selectionViewInfo
     */
    var fileSelectionBox = React.createClass({

        /**
         * Sets up initial state.
         */
        getInitialState: function () {
            return {
                initialScroll: 0
            };
        },

        /**
         * When the component has updated in the DOM, reposition it to where the currently
         * selected node is located now.
         */
        componentDidUpdate: function () {
            if (!this.props.visible) {
                return;
            }

            var node = this.getDOMNode(),
                selectedNode = $(node.parentNode).find(this.props.selectedClassName),
                selectionViewInfo = this.props.selectionViewInfo;

            if (selectedNode.length === 0) {
                return;
            }

            node.style.top = selectedNode.offset().top - selectionViewInfo.get("offsetTop") + selectionViewInfo.get("scrollTop") - selectedNode.position().top + "px";
        },

        render: function () {
            var selectionViewInfo = this.props.selectionViewInfo;

            return DOM.div({
                style: {
                    overflow: "auto",
                    left: selectionViewInfo.get("scrollLeft"),
                    width: selectionViewInfo.get("width") - this.props.widthAdjustment,
                    display: this.props.visible ? "block" : "none"
                },
                className: this.props.className
            });
        }
    });

    /**
     * @private
     *
     * This is the root component of the file tree.
     *
     * Props:
     * * treeData: the root of the tree (an Immutable.Map with the contents of the project root)
     * * sortDirectoriesFirst: whether the directories should be displayed first when listing the contents of a directory
     * * parentPath: the full path of the directory containing this file
     * * actions: the action creator responsible for communicating actions the user has taken
     * * extensions: registered extensions for the file tree
     * * forceRender: causes the component to run render
     */
    var fileTreeView = React.createClass({

        /**
         * Update for any change in the tree data or directory sorting preference.
         */
        shouldComponentUpdate: function (nextProps, nextState) {
            return nextProps.forceRender ||
                this.props.treeData !== nextProps.treeData ||
                this.props.sortDirectoriesFirst !== nextProps.sortDirectoriesFirst ||
                this.props.extensions !== nextProps.extensions ||
                this.props.selectionViewInfo !== nextProps.selectionViewInfo;
        },

        render: function () {
            var selectionBackground = fileSelectionBox({
                ref: "selectionBackground",
                selectionViewInfo: this.props.selectionViewInfo,
                className: "filetree-selection",
                visible: this.props.selectionViewInfo.get("hasSelection"),
                widthAdjustment: 0,
                selectedClassName: ".selected-node",
                forceUpdate: true
            }),
                contextBackground = fileSelectionBox({
                    ref: "contextBackground",
                    selectionViewInfo: this.props.selectionViewInfo,
                    className: "filetree-context",
                    visible: this.props.selectionViewInfo.get("hasContext"),
                    widthAdjustment: 0,
                    selectedClassName: ".context-node",
                    forceUpdate: true
                });

            return DOM.div(
                null,
                selectionBackground,
                contextBackground,
                directoryContents({
                    isRoot: true,
                    parentPath: this.props.parentPath,
                    sortDirectoriesFirst: this.props.sortDirectoriesFirst,
                    contents: this.props.treeData,
                    extensions: this.props.extensions,
                    actions: this.props.actions,
                    forceRender: this.props.forceRender,
                    platform: this.props.platform
                })
            );
        }
    });

    /**
     * Renders the file tree to the given element.
     *
     * @param {DOMNode|jQuery} element Element in which to render this file tree
     * @param {FileTreeViewModel} viewModel the data container
     * @param {Directory} projectRoot Directory object from which the fullPath of the project root is extracted
     * @param {ActionCreator} actions object with methods used to communicate events that originate from the user
     * @param {boolean} forceRender Run render on the entire tree (useful if an extension has new data that it needs rendered)
     * @param {string} platform mac, win, linux
     */
    function render(element, viewModel, projectRoot, actions, forceRender, platform) {
        if (!projectRoot) {
            return;
        }

        React.renderComponent(fileTreeView({
            treeData: viewModel.treeData,
            selectionViewInfo: viewModel.selectionViewInfo,
            sortDirectoriesFirst: viewModel.sortDirectoriesFirst,
            parentPath: projectRoot.fullPath,
            actions: actions,
            extensions: _extensions,
            platform: platform,
            forceRender: forceRender
        }),
              element);
    }

    /**
     * @private
     *
     * Add an extension for the given category (icons, addClass).
     *
     * @param {string} category Category to which the extension is being added
     * @param {function} callback The extension function itself
     */
    function _addExtension(category, callback) {
        if (!callback || typeof callback !== "function") {
            console.error("Attempt to add FileTreeView", category, "extension without a callback function");
            return;
        }
        var callbackList = _extensions.get(category);
        if (!callbackList) {
            callbackList = Immutable.Vector();
        }
        callbackList = callbackList.push(callback);
        _extensions = _extensions.set(category, callbackList);
    }

    /**
     * Adds an icon provider. The icon provider is a function which takes a data object and
     * returns a React.DOM.ins instance for the icons within the tree. The callback can
     * alternatively return a string, DOM node or a jQuery object for the ins node to be added.
     *
     * The data object contains:
     *
     * * `name`: the file or directory name
     * * `fullPath`: full path to the file or directory
     * * `isFile`: true if it's a file, false if it's a directory
     */
    function addIconProvider(callback) {
        _addExtension("icons", callback);
    }

    /**
     * Adds an additional classes provider which can return classes that should be added to a
     * given file or directory in the tree.
     *
     * The data object contains:
     *
     * * `name`: the file or directory name
     * * `fullPath`: full path to the file or directory
     * * `isFile`: true if it's a file, false if it's a directory
     */
    function addClassesProvider(callback) {
        _addExtension("addClass", callback);
    }

    // Private API for testing
    exports._sortFormattedDirectory = _sortDirectoryContents;
    exports._fileNode = fileNode;
    exports._directoryNode = directoryNode;
    exports._directoryContents = directoryContents;
    exports._fileTreeView = fileTreeView;

    // Public API
    exports.addIconProvider = addIconProvider;
    exports.addClassesProvider = addClassesProvider;
    exports.render = render;
});


html {
    background-color: #e6e9e9;
    background-image: -webkit-linear-gradient(270deg,rgb(230,233,233) 0%,rgb(216,221,221) 100%);
    background-image: linear-gradient(270deg,rgb(230,233,233) 0%,rgb(216,221,221) 100%);
    -webkit-font-smoothing: antialiased;
}

body {
    margin: 0 auto;
    padding: 2em 2em 4em;
    max-width: 800px;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 16px;
    line-height: 1.5em;
    color: #545454;
    background-color: #ffffff;
    box-shadow: 0 0 2px rgba(0, 0, 0, 0.06);
}

h1, h2, h3, h4, h5, h6 {
    color: #222;
    font-weight: 600;
    line-height: 1.3em;
}

h2 {
    margin-top: 1.3em;
}

a {
    color: #0083e8;
}

b, strong {
    font-weight: 600;
}

samp {
    display: none;
}

img {
    -webkit-animation: colorize 2s cubic-bezier(0, 0, .78, .36) 1;
    animation: colorize 2s cubic-bezier(0, 0, .78, .36) 1;
    background: transparent;
    border: 10px solid rgba(0, 0, 0, 0.12);
    border-radius: 4px;
    display: block;
    margin: 1.3em auto;
    max-width: 95%;
}

@-webkit-keyframes colorize {
    0% {
        -webkit-filter: grayscale(100%);
    }
    100% {
        -webkit-filter: grayscale(0%);
    }
}

@keyframes colorize {
    0% {
        filter: grayscale(100%);
    }
    100% {
        filter: grayscale(0%);
    }
}


fs-extra

fs-extra contains methods that aren't included in the vanilla Node.js fs package. Such as mkdir -p, cp -r, and rm -rf.

fs
file
file system
copy
directory
extra
mkdirp
mkdir
mkdirs
recursive
json
read
write
extra
delete
remove
touch
create
text
output

Node.js: fs-extra
=================

[![build status](https://secure.travis-ci.org/jprichardson/node-fs-extra.png)](http://travis-ci.org/jprichardson/node-fs-extra)

This module adds a few extra file system methods that aren't included in the native `fs` module. It is a drop in replacement for `fs`.

Why?
----

I got tired of including `mkdirp`, `rimraf`, and `cp -r` in most of my projects.

Installation
------------

    npm install --save fs-extra

Usage
-----

Drop in replacement for native `fs`.

```javascript
var fs = require('fs-extra');
```

Methods
-------

**NOTE:** You can still use the native Node.js methods. They are copied over to `fs-extra`.

### copy(src, dest, callback)

Copy a file or directory. The directory can have contents. Like `cp -r`. There isn't a synchronous version implemented yet.

Sync: (none)

Examples:

```javascript
var fs = require('fs-extra');

fs.copy('/tmp/myfile', '/tmp/mynewfile', function(err){
  if (err) {
    console.error(err);
  }
  else {
    console.log("success!")
  }
}); //copies file

fs.copy('/tmp/mydir', '/tmp/mynewdir', function(err){
  if (err) {
    console.error(err);
  }
  else {
    console.log("success!")
  }
}); //copies directory, even if it has subdirectories or files
```

### createFile(file, callback)

Creates a file. If the file that is requested to be created is in directories that do not exist, these directories are created. If the file already exists, it is **NOT MODIFIED**.

Sync: `createFileSync()`

Example:

```javascript
var fs = require('fs-extra')
  , file = '/tmp/this/path/does/not/exist/file.txt'

fs.createFile(file, function(err) {
  console.log(err); //null

  //file has now been created, including the directory it is to be placed in
})
```

### mkdirs(dir, callback)

Creates a directory. If the parent hierarchy doesn't exist, it's created. Like `mkdir -p`.

Alias: `mkdirp()`

Sync: `mkdirsSync()` / `mkdirpSync()`

Examples:

```javascript
var fs = require('fs-extra');

fs.mkdirs('/tmp/some/long/path/that/prob/doesnt/exist', function(err){
  if (err) {
    console.error(err);
  }
  else {
    console.log("success!")
  }
});

fs.mkdirsSync('/tmp/another/path');
```

### outputFile(file, data, callback)

Almost the same as `writeFile`, except that if the directory does not exist, it's created.

Sync: `outputFileSync()`

Example:

```javascript
var fs = require('fs-extra')
  , file = '/tmp/this/path/does/not/exist/file.txt'

fs.outputFile(file, 'hello!', function(err) {
  console.log(err); //null

  fs.readFile(file, 'utf8', function(err, data) {
    console.log(data); //hello!
  })
})
```

### outputJson(file, data, callback)

Almost the same as `writeJson`, except that if the directory does not exist, it's created.

Alias: `outputJSON()

Sync: `outputJsonSync()`, `outputJSONSync()`

Example:

```javascript
var fs = require('fs-extra')
  , file = '/tmp/this/path/does/not/exist/file.txt'

fs.outputJson(file, {name: 'JP'}, function(err) {
  console.log(err); //null

  fs.readJson(file, function(err, data) {
    console.log(data.name); //'JP
  })
})
```

### readJson(file, callback)

Reads a JSON file and then parses it into an object.

Alias: `readJSON()`

Sync: `readJsonSync()`, `readJSONSync()`

Example:

```javascript
var fs = require('fs-extra');

fs.readJson('./package.json', function(err, packageObj) {
  console.log(packageObj.version); //0.1.3
});
```

### remove(dir, callback)

Removes a file or directory. The directory can have contents. Like `rm -rf`.

Alias: `delete()`

Sync: `removeSync()` / `deleteSync()`

Examples:

```javascript
var fs = require('fs-extra');

fs.remove('/tmp/myfile', function(err){
  if (err) {
    console.error(err);
  }
  else {
    console.log("success!")
  }
});

fs.removeSync('/home/jprichardson'); //I just deleted my entire HOME directory.
```

### writeJson(file, object, callback)

Writes an object to a JSON file.

Alias: `writeJSON()`

Sync: `writeJsonSync()`, `writeJSONSync()`

Example:

```javascript
var fs = require('fs-extra');
fs.writeJson('./package.json', {name: 'fs-extra'}, function(err){
  console.log(err);
});
```

Roadmap to 1.0.0
-----------------

This contains items that I'm considering doing. I'd love community feedback.

* File system walker. I really like this one: https://github.com/daaku/nodejs-walker ... this might be adding too much. Thoughts?
* File/directory tree watcher. There are quite a few. ... this also might be adding too much. Thoughts?
* Method to move files.
* Copy sync.
* Thinking about moving `rimraf`, `ncp`, and `mkdirps` code into this library. I'd like fs-extra to be a stable library that module authors
can depend upon. A bunch of other dependencies kinda sucks for modules/libraries. (I'm leaning against this now.)
* Change documentation to use the `fse` prefix instead of `fs`. This may encourage people to start using `fse` as a prefix and hence make their code clearer that they're not using the native `fs`. I'm very undecided on this one since `fs-extra` is a drop in replacement for the native `fs`. (I'm leaning against this now.)

Naming
------

I put a lot of thought into the naming of these function. Inspired by @coolaj86's request. So he deserves much of the credit for raising the issue. See discussion(s) here:

* https://github.com/jprichardson/node-fs-extra/issues/2
* https://github.com/flatiron/utile/issues/11
* https://github.com/ryanmcgrath/wrench-js/issues/29
* https://github.com/substack/node-mkdirp/issues/17

First, I believe that in as many cases as possible, the [Node.js naming schemes](http://nodejs.org/api/fs.html) should be chosen. However, there are problems with the Node.js own naming schemes.

For example, `fs.readFile()` and `fs.readdir()`: the **F** is capitalized in *File* and the **d** is not capitalized in *dir*. Perhaps a bit pedantic, but they should still be consistent. Also, Node.js has chosen a lot of POSIX naming schemes, which I believe is great. See: `fs.mkdir()`, `fs.rmdir()`, `fs.chown()`, etc.

We have a dilemma though. How do you consistently name methods that perform the following POSIX commands: `cp`, `cp -r`, `mkdir -p`, and `rm -rf`?

My perspective: when in doubt, err on the side of simplicity. Consider that for a moment. A directory is just a hierarchical grouping of directories and files. So when you want to copy it or remove it, in most cases you'll want to copy or remove all of its contents. When you want to create a directory, if the directory that it's suppose to be contained in does not exist, then in most cases you'll want to create that too.

So, if you want to remove a file or a directory regardless of whether it has contents, just call `fs.remove(path)` or its alias `fs.delete(path)`. If you want to copy a file or a directory whether it has contents, just call `fs.copy(source, destination)`. If you want to create a directory regardless of whether its parent directories exist, just call `fs.mkdirs(path)` or `fs.mkdirp(path)`.

Contributors
-------------
- [JP Richardson](https://github.com/jprichardson)
- [Mike McNeil](https://github.com/mikermcneil)
- [Ian Crowther](https://github.com/iancrowther)
- [Stephen Mathieson](https://github.com/stephenmathieson)
- `<your name here>`

License
-------

Licensed under MIT

Copyright (c) 2011-2013 JP Richardson

[1]: http://nodejs.org/docs/latest/api/fs.html

[jsonfile]: https://github.com/jprichardson/node-jsonfile

[aboutjp]: http://about.me/jprichardson
[twitter]: http://twitter.com/jprichardson
[procbits]: http://procbits.com
[gitpilot]: http://gitpilot.com


width: selectionViewInfo.get("width") - this.props.widthAdjustment,

display: this.props.visible ? "block" : "none"

keep keep keep keep replace replace keep keep keep

/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/* unittests: ProjectModel */
/*global define, brackets, $ */

/**
 * Provides the data source for a project and manages the view model for the FileTreeView.
 */
define(function (require, exports, module) {
    "use strict";

    var InMemoryFile        = require("document/InMemoryFile"),
        FileUtils           = require("file/FileUtils"),
        _                   = require("thirdparty/lodash"),
        FileSystem          = require("filesystem/FileSystem"),
        FileSystemError     = require("filesystem/FileSystemError"),
        FileTreeViewModel   = require("project/FileTreeViewModel"),
        Async               = require("utils/Async");

    // Constants
    var EVENT_CHANGE            = "change",
        EVENT_SHOULD_SELECT     = "select",
        EVENT_SHOULD_FOCUS      = "focus",
        ERROR_CREATION          = "creationError",
        ERROR_INVALID_FILENAME  = "invalidFilename";

    /**
     * @private
     * File and folder names which are not displayed or searched
     * TODO: We should add the rest of the file names that TAR excludes:
     *    http://www.gnu.org/software/tar/manual/html_section/exclude.html
     * TODO: This should be user configurable
     *    https://github.com/adobe/brackets/issues/6781
     * @type {RegExp}
     */
    var _exclusionListRegEx = /\.pyc$|^\.git$|^\.gitmodules$|^\.svn$|^\.DS_Store$|^Thumbs\.db$|^\.hg$|^CVS$|^\.hgtags$|^\.idea$|^\.c9revisions$|^\.SyncArchive$|^\.SyncID$|^\.SyncIgnore$|\~$/;

    /**
     * @private
     * A string containing all invalid characters for a specific platform.
     * This will be used to construct a regular expression for checking invalid filenames.
     * When a filename with one of these invalid characters are detected, then it is
     * also used to substitute the place holder of the error message.
     */
    var _invalidChars;

    /**
     * @private
     * RegEx to validate if a filename is not allowed even if the system allows it.
     * This is done to prevent cross-platform issues.
     */

    var _illegalFilenamesRegEx = /^(\.+|com[1-9]|lpt[1-9]|nul|con|prn|aux|)$|\.+$/i;

    /**
     * Returns true if this matches valid filename specifications.
     *
     * TODO: This likely belongs in FileUtils.
     *
     * @param {string} filename to check
     * @param {string} invalidChars List of characters that are disallowed
     * @return {boolean} true if the filename is valid
     */
    function isValidFilename(filename, invalidChars) {
        // Validate file name
        // Checks for valid Windows filenames:
        // See http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
        return !((filename.search(new RegExp("[" + invalidChars + "]+")) !== -1) ||
                 filename.match(_illegalFilenamesRegEx));
    }

    /**
     * @private
     * See shouldShow
     */
    function _shouldShowName(name) {
        return !name.match(_exclusionListRegEx);
    }

    /**
     * Returns false for files and directories that are not commonly useful to display.
     *
     * @param {!FileSystemEntry} entry File or directory to filter
     * @return {boolean} true if the file should be displayed
     */
    function shouldShow(entry) {
        return _shouldShowName(entry.name);
    }

    // Constants used by the ProjectModel

    var FILE_RENAMING     = 0,
        FILE_CREATING     = 1,
        RENAME_CANCELLED  = 2;

    /**
     * @private
     *
     * Determines if a path string is pointing to a directory (does it have a trailing slash?)
     *
     * @param {string} path Path to test.
     */
    function _pathIsFile(path) {
        return _.last(path) !== "/";
    }

    /**
     * @private
     *
     * Gets the FileSystem object (either a File or Directory) based on the path provided.
     *
     * @param {string} path Path to retrieve
     */
    function _getFSObject(path) {
        if (!path) {
            return path;
        } else if (_pathIsFile(path)) {
            return FileSystem.getFileForPath(path);
        }
        return FileSystem.getDirectoryForPath(path);
    }

    /**
     * @private
     *
     * Given what is possible a FileSystem object, return its path (if a string path is passed in,
     * it will be returned as-is).
     *
     * @param {FileSystemEntry} fsobj Object from which the path should be extracted
     */
    function _getPathFromFSObject(fsobj) {
        if (fsobj && fsobj.fullPath) {
            return fsobj.fullPath;
        }
        return fsobj;
    }

    /**
     * Creates a new file or folder at the given path. The returned promise is rejected if the filename
     * is invalid, the new path already exists or some other filesystem error comes up.
     *
     * @param {string} path path to create
     * @param {boolean} isFolder true if the new entry is a folder
     * @return {$.Promise} resolved when the file or directory has been created.
     */
    function doCreate(path, isFolder) {
        var d = new $.Deferred();

        var name = FileUtils.getBaseName(path);
        if (!isValidFilename(name, _invalidChars)) {
            return d.reject(ERROR_INVALID_FILENAME).promise();
        }

        FileSystem.resolve(path, function (err) {
            if (!err) {
                // Item already exists, fail with error
                d.reject(FileSystemError.ALREADY_EXISTS);
            } else {
                if (isFolder) {
                    var directory = FileSystem.getDirectoryForPath(path);

                    directory.create(function (err) {
                        if (err) {
                            d.reject(err);
                        } else {
                            d.resolve(directory);
                        }
                    });
                } else {
                    // Create an empty file
                    var file = FileSystem.getFileForPath(path);

                    FileUtils.writeText(file, "").then(function () {
                        d.resolve(file);
                    }, d.reject);
                }
            }
        });

        return d.promise();
    }

    /**
     * @constructor
     *
     * The ProjectModel provides methods for accessing information about the current open project.
     * It also manages the view model to display a FileTreeView of the project.
     */
    function ProjectModel(initial) {
        initial = initial || {};
        if (initial.projectRoot) {
            this.projectRoot = initial.projectRoot;
        }

        if (initial.focused !== undefined) {
            this._focused = initial.focused;
        }
        this._viewModel = new FileTreeViewModel.FileTreeViewModel();
        this._viewModel.on(FileTreeViewModel.EVENT_CHANGE, function () {
            $(this).trigger(EVENT_CHANGE);
        }.bind(this));
        this._selections = {};
    }

    /**
     * @type {Directory}
     *
     * The root Directory object for the project.
     */
    ProjectModel.prototype.projectRoot = null;

    /**
     * @private
     * @type {FileTreeViewModel}
     *
     * The view model for this project.
     */
    ProjectModel.prototype._viewModel = null;

    /**
     * @private
     * @type {string}
     *
     * Encoded URL
     * @see getBaseUrl(), setBaseUrl()
     */
    ProjectModel.prototype._projectBaseUrl = "";

    /**
     * @private
     * @type {{selected: ?string, context: ?string, previousContext: ?string, rename: ?Object}}
     *
     * Keeps track of selected files, context, previous context and files
     * that are being renamed or created.
     */
    ProjectModel.prototype._selections = null;

    /**
     * @private
     * @type {boolean}
     *
     * Flag to store whether the file tree has focus.
     */
    ProjectModel.prototype._focused = true;

    /**
     * @private
     * @type {string}
     *
     * Current file path being viewed.
     */
    ProjectModel.prototype._currentPath = null;

    /**
     * @private
     * @type {?$.Promise.<Array<File>>}
     *
     * A promise that is resolved with an array of all project files. Used by
     * ProjectManager.getAllFiles().
     */
    ProjectModel.prototype._allFilesCachePromise = null;

    /**
     * Sets whether the file tree is focused or not.
     *
     * @param {boolean} focused True if the file tree has the focus.
     */
    ProjectModel.prototype.setFocused = function (focused) {
        this._focused = focused;
        if (!focused) {
            this.setSelected(null);
        }
    };

    /**
     * Sets the width of the selection bar.
     *
     * @param {int} width New width
     */
    ProjectModel.prototype.setSelectionWidth = function (width) {
        this._viewModel.setSelectionWidth(width);
    };

    /**
     * Tracks the scroller position.
     *
     * @param {int} scrollTop Top of scroll position
     * @param {int} scrollLeft Left of scroll position
     * @param {int} offsetTop Top of scroller element
     */
    ProjectModel.prototype.setScrollerInfo = function (scrollTop, scrollLeft, offsetTop) {
        this._viewModel.setSelectionScrollerInfo(scrollTop, scrollLeft, offsetTop);
    };

    /**
     * Returns the encoded Base URL of the currently loaded project, or empty string if no project
     * is open (during startup, or running outside of app shell).
     * @return {String}
     */
    ProjectModel.prototype.getBaseUrl = function getBaseUrl() {
        return this._projectBaseUrl;
    };

    /**
     * Sets the encoded Base URL of the currently loaded project.
     * @param {String}
     */
    ProjectModel.prototype.setBaseUrl = function setBaseUrl(projectBaseUrl) {
        // Ensure trailing slash to be consistent with projectRoot.fullPath
        // so they're interchangable (i.e. easy to convert back and forth)
        if (projectBaseUrl.length > 0 && projectBaseUrl[projectBaseUrl.length - 1] !== "/") {
            projectBaseUrl += "/";
        }

        this._projectBaseUrl = projectBaseUrl;
        return projectBaseUrl;
    };

    /**
     * Returns true if absPath lies within the project, false otherwise.
     * Does not support paths containing ".."
     *
     * @param {string|FileSystemEntry} absPathOrEntry
     * @return {boolean}
     */
    ProjectModel.prototype.isWithinProject = function isWithinProject(absPathOrEntry) {
        var absPath = absPathOrEntry.fullPath || absPathOrEntry;
        return (this.projectRoot && absPath.indexOf(this.projectRoot.fullPath) === 0);
    };

    /**
     * If absPath lies within the project, returns a project-relative path. Else returns absPath
     * unmodified.
     * Does not support paths containing ".."
     *
     * @param {!string} absPath
     * @return {!string}
     */
    ProjectModel.prototype.makeProjectRelativeIfPossible = function makeProjectRelativeIfPossible(absPath) {
        if (absPath && this.isWithinProject(absPath)) {
            return absPath.slice(this.projectRoot.fullPath.length);
        }
        return absPath;
    };

    /**
     * @private
     *
     * Returns a promise that resolves with a cached copy of all project files.
     * Used by ProjectManager.getAllFiles(). Ensures that at most one un-cached
     * directory traversal is active at a time, which is useful at project load
     * time when watchers (and hence filesystem-level caching) has not finished
     * starting up. The cache is cleared on every filesystem change event, and
     * also on project load and unload.
     *
     * @return {$.Promise.<Array.<File>>}
     */
    ProjectModel.prototype._getAllFilesCache = function _getAllFilesCache() {
        if (!this._allFilesCachePromise) {
            var deferred = new $.Deferred(),
                allFiles = [],
                allFilesVisitor = function (entry) {
                    if (shouldShow(entry)) {
                        if (entry.isFile) {
                            allFiles.push(entry);
                        }
                        return true;
                    }
                    return false;
                };

            this._allFilesCachePromise = deferred.promise();

            this.projectRoot.visit(allFilesVisitor, function (err) {
                if (err) {
                    deferred.reject(err);
                } else {
                    deferred.resolve(allFiles);
                }
            }.bind(this));
        }

        return this._allFilesCachePromise;
    };

    /**
     * Returns an Array of all files for this project, optionally including
     * files additional files provided. Files are filtered out by shouldShow().
     *
     * @param {function (File, number):boolean=} filter Optional function to filter
     *          the file list (does not filter directory traversal). API matches Array.filter().
     * @param {Array.<File>=} additionalFiles Additional files to include (for example, the WorkingSet)
     *          Only adds files that are *not* under the project root or untitled documents.
     *
     * @return {$.Promise} Promise that is resolved with an Array of File objects.
     */
    ProjectModel.prototype.getAllFiles = function getAllFiles(filter, additionalFiles) {
        // The filter and includeWorkingSet params are both optional.
        // Handle the case where filter is omitted but includeWorkingSet is
        // specified.
        if (additionalFiles === undefined && typeof (filter) !== "function") {
            additionalFiles = filter;
            filter = null;
        }

        var filteredFilesDeferred = new $.Deferred();

        // First gather all files in project proper
        // Note that with proper promises we may be able to fix this so that we're not doing this
        // anti-pattern of creating a separate deferred rather than just chaining off of the promise
        // from _getAllFilesCache
        this._getAllFilesCache().done(function (result) {
            // Add working set entries, if requested
            if (additionalFiles) {
                additionalFiles.forEach(function (file) {
                    if (result.indexOf(file) === -1 && !(file instanceof InMemoryFile)) {
                        result.push(file);
                    }
                });
            }

            // Filter list, if requested
            if (filter) {
                result = result.filter(filter);
            }

            // If a done handler attached to the returned filtered files promise
            // throws an exception that isn't handled here then it will leave
            // _allFilesCachePromise in an inconsistent state such that no
            // additional done handlers will ever be called!
            try {
                filteredFilesDeferred.resolve(result);
            } catch (e) {
                console.warn("Unhandled exception in getAllFiles handler: ", e);
            }
        }).fail(function (err) {
            try {
                filteredFilesDeferred.reject(err);
            } catch (e) {
                console.warn("Unhandled exception in getAllFiles handler: ", e);
            }
        });

        return filteredFilesDeferred.promise();
    };

    /**
     * @private
     *
     * Resets the all files cache.
     */
    ProjectModel.prototype._resetCache = function _resetCache() {
        this._allFilesCachePromise = null;
    };

    /**
     * Adds an event listener for this ProjectModel. See jQuery's documentation for .on.
     *
     * Available events:
     *
     * * EVENT_CHANGE (`change`) - Fired when there's a change that should refresh the UI
     * * EVENT_SHOULD_SELECT (`select`) - Specifies that a selection has been made in the file tree and that the file tree should be selected
     * * ERROR_CREATION (`creationError`) - Triggered when there is a problem creating a file.
     */
    ProjectModel.prototype.on = function (event, handler) {
        $(this).on(event, handler);
    };

    /**
     * Removes an event listener for this ProjectModel. See jQuery's documentation for .off.
     */
    ProjectModel.prototype.off = function (event, handler) {
        $(this).off(event, handler);
    };

    /**
     * Sets the project root (effectively resetting this ProjectModel).
     *
     * @param {Directory} projectRoot new project root
     * @return {$.Promise} resolved when the project root has been updated
     */
    ProjectModel.prototype.setProjectRoot = function (projectRoot) {
        this.projectRoot = projectRoot;
        this._resetCache();
        this._viewModel._rootChanged();

        var d = new $.Deferred(),
            self = this;

        projectRoot.getContents(function (err, contents) {
            if (err) {
                d.reject(err);
            } else {
                self._viewModel.setDirectoryContents("", contents);
                d.resolve();
            }
        });
        return d.promise();
    };

    /**
     * @private
     *
     * Gets the contents of a directory at the given path.
     *
     * @param {string} path path to retrieve
     * @return {$.Promise} Resolved with the directory contents.
     */
    ProjectModel.prototype._getDirectoryContents = function (path) {
        var d = new $.Deferred();
        FileSystem.getDirectoryForPath(path).getContents(function (err, contents) {
            if (err) {
                d.reject(err);
            } else {
                d.resolve(contents);
            }
        });
        return d.promise();
    };

    /**
     * Opens or closes the given directory in the file tree.
     *
     * @param {string} path Path to open
     * @param {boolean} open `true` to open the path
     * @return {$.Promise} resolved when the path has been opened.
     */
    ProjectModel.prototype.setDirectoryOpen = function (path, open) {
        var projectRelative = this.makeProjectRelativeIfPossible(path),
            needsLoading    = !this._viewModel.isPathLoaded(projectRelative),
            d               = new $.Deferred(),
            self            = this;

        function onSuccess(contents) {
            // Update the view model
            if (contents) {
                self._viewModel.setDirectoryContents(projectRelative, contents);
            }

            if (open) {
                self._viewModel.openPath(projectRelative);
                if (self._focused) {
                    var currentPathInProject = self.makeProjectRelativeIfPossible(self._currentPath);
                    if (self._viewModel.isFilePathVisible(currentPathInProject)) {
                        self.setSelected(self._currentPath, true);
                    } else {
                        self.setSelected(null);
                    }
                }
            } else {
                self._viewModel.setDirectoryOpen(projectRelative, false);
                var selected = self._selections.selected;
                if (selected) {
                    var relativeSelected = self.makeProjectRelativeIfPossible(selected);
                    if (!self._viewModel.isFilePathVisible(relativeSelected)) {
                        self.setSelected(null);
                    }
                }
            }

            d.resolve();
        }

        // If the view model doesn't have the data it needs, we load it now, otherwise we can just
        // manage the selection and resovle the promise.
        if (open && needsLoading) {
            var parentDirectory = FileUtils.getDirectoryPath(FileUtils.stripTrailingSlash(path));
            this.setDirectoryOpen(parentDirectory, true).then(function () {
                self._getDirectoryContents(path).then(onSuccess).fail(function (err) {
                    d.reject(err);
                });
            }, function (err) {
                d.reject(err);
            });
        } else {
            onSuccess();
        }

        return d.promise();
    };

    /**
     * Shows the given path in the tree and selects it if it's a file. Any intermediate directories
     * will be opened and a promise is returned to show when the entire operation is complete.
     *
     * @param {string|File|Directory} path full path to the file or directory
     * @return {$.Promise} promise resolved when the path is shown
     */
    ProjectModel.prototype.showInTree = function (path) {
        var d = new $.Deferred();
        path = _getPathFromFSObject(path);

        var projectRelative = this.makeProjectRelativeIfPossible(path);

        // Not in project?
        if (projectRelative[0] === "/") {
            d.resolve();
        } else {
            var parentDirectory = FileUtils.getDirectoryPath(path),
                self = this;
            this.setDirectoryOpen(parentDirectory, true).then(function () {
                if (_pathIsFile(path)) {
                    self.setSelected(path);
                }
                d.resolve();
            }, function (err) {
                d.reject(err);
            });
        }
        return d.promise();
    };

    /**
     * Selects the given path in the file tree and opens the file (unless doNotOpen is specified).
     * Directories will not be selected.
     *
     * When the selection changes, any rename operation that is currently underway will be completed.
     *
     * @param {string} path full path to the file being selected
     * @param {boolean} doNotOpen `true` if the file should not be opened.
     */
    ProjectModel.prototype.setSelected = function (path, doNotOpen) {
        path = _getPathFromFSObject(path);

        // Directories are not selectable
        if (!_pathIsFile(path)) {
            return;
        }

        var oldProjectPath = this.makeProjectRelativeIfPossible(this._selections.selected),
            pathInProject = this.makeProjectRelativeIfPossible(path);

        if (path && !this._viewModel.isFilePathVisible(pathInProject)) {
            path = null;
            pathInProject = null;
        }

        this.performRename();

        this._viewModel.moveMarker("selected", oldProjectPath, pathInProject);
        if (this._selections.context) {
            this._viewModel.moveMarker("context", this.makeProjectRelativeIfPossible(this._selections.context), null);
            delete this._selections.context;
        }

        var previousSelection = this._selections.selected;
        this._selections.selected = path;

        if (path) {
            if (!doNotOpen) {
                $(this).trigger(EVENT_SHOULD_SELECT, {
                    path: path,
                    previousPath: previousSelection,
                    hadFocus: this._focused
                });
            }

            $(this).trigger(EVENT_SHOULD_FOCUS);
        }
    };

    /**
     * Gets the currently selected file or directory.
     *
     * @return {FileSystemEntry} the filesystem object for the currently selected file
     */
    ProjectModel.prototype.getSelected = function () {
        return _getFSObject(this._selections.selected);
    };

    /**
     * Keeps track of which file is currently being edited.
     *
     * @param {File|string} curFile Currently edited file.
     */
    ProjectModel.prototype.setCurrentFile = function (curFile) {
        this._currentPath = _getPathFromFSObject(curFile);
    };

    /**
     * Adds the file at the given path to the Working Set and selects it there.
     *
     * @param {string} path full path of file to open in Working Set
     */
    ProjectModel.prototype.selectInWorkingSet = function (path) {
        this.performRename();
        $(this).trigger(EVENT_SHOULD_SELECT, {
            path: path,
            add: true
        });
    };

    /**
     * Sets the context (for context menu operations) to the given path. This is independent from the
     * open/selected file.
     *
     * @param {string} path full path of file or directory to which the context should be setBaseUrl
     * @param {boolean} _doNotRename True if this context change should not cause a rename operation to finish. This is a special case that goes with context menu handling.
     * @param {boolean} _saveContext True if the current context should be saved (see comment below)
     */
    ProjectModel.prototype.setContext = function (path, _doNotRename, _saveContext) {
        // This bit is not ideal: when the user right-clicks on an item in the file tree
        // and there is already a context menu up, the FileTreeView sends a signal to set the
        // context to the new element but the PopupManager follows that with a message that it's
        // closing the context menu (because it closes the previous one and then opens the new
        // one.) This timing means that we need to provide some special case handling here.
        if (_saveContext) {
            if (!path) {
                this._selections.previousContext = this._selections.context;
            } else {
                this._selections.previousContext = path;
            }
        } else {
            delete this._selections.previousContext;
        }

        path = _getPathFromFSObject(path);

        if (!_doNotRename) {
            this.performRename();
        }
        var currentContext = this._selections.context;
        this._selections.context = path;
        this._viewModel.moveMarker("context", this.makeProjectRelativeIfPossible(currentContext),
                                   this.makeProjectRelativeIfPossible(path));
    };

    /**
     * Restores the context to the last non-null context. This is specifically here to handle
     * the sequence of messages that we get from the project context menu.
     */
    ProjectModel.prototype.restoreContext = function () {
        if (this._selections.previousContext) {
            this.setContext(this._selections.previousContext);
        }
    };

    /**
     * Gets the currently selected context.
     *
     * @return {FileSystemEntry} filesystem object for the context file or directory
     */
    ProjectModel.prototype.getContext = function () {
        return _getFSObject(this._selections.context);
    };

    /**
     * Starts a rename operation for the file or directory at the given path. If the path is
     * not provided, the current context is used.
     *
     * If a rename operation is underway, it will be completed automatically.
     *
     * The Promise returned is resolved with an object with a `newPath` property with the renamed path. If the user cancels the operation, the promise is resolved with the value RENAME_CANCELLED.
     *
     * @param {string=} path optional path to start renaming
     * @return {$.Promise} resolved when the operation is complete.
     */
    ProjectModel.prototype.startRename = function (path) {
        path = _getPathFromFSObject(path);
        if (!path) {
            path = this._selections.context;
            if (!path) {
                return new $.Deferred().resolve().promise();
            }
        }

        if (this._selections.rename && this._selections.rename.path === path) {
            return;
        }

        if (path !== this._selections.context) {
            this.setContext(path);
        } else {
            this.performRename();
        }

        this._viewModel.moveMarker("rename", null,
                                   this.makeProjectRelativeIfPossible(path));
        var d = new $.Deferred();
        this._selections.rename = {
            deferred: d,
            type: FILE_RENAMING,
            path: path,
            newName: FileUtils.getBaseName(path)
        };
        return d.promise();
    };

    /**
     * Sets the new value for the rename operation that is in progress (started previously with a call
     * to `startRename`).
     *
     * @param {string} name new name for the file or directory being renamed
     */
    ProjectModel.prototype.setRenameValue = function (name) {
        if (!this._selections.rename) {
            return;
        }
        this._selections.rename.newName = name;
    };

    /**
     * Cancels the rename operation that is in progress. This resolves the original promise with
     * a RENAME_CANCELLED value.
     */
    ProjectModel.prototype.cancelRename = function () {
        var renameInfo = this._selections.rename;
        if (!renameInfo) {
            return;
        }

        // File creation is a special case.
        if (renameInfo.type === FILE_CREATING) {
            this._cancelCreating();
            return;
        }

        this._viewModel.moveMarker("rename", this.makeProjectRelativeIfPossible(renameInfo.path), null);
        renameInfo.deferred.resolve(RENAME_CANCELLED);
        delete this._selections.rename;
        this.setContext(null);
    };

    /**
     * Rename a file/folder. This will update the project tree data structures
     * and send notifications about the rename.
     *
     * @param {string} oldName Old item name
     * @param {string} newName New item name
     * @param {boolean} isFolder True if item is a folder; False if it is a file.
     * @return {$.Promise} A promise object that will be resolved or rejected when
     *   the rename is finished.
     */
    function _renameItem(oldName, newName, isFolder) {
        var result = new $.Deferred();

        if (oldName === newName) {
            result.resolve();
        } else if (!isValidFilename(FileUtils.getBaseName(newName), _invalidChars)) {
            result.reject(ERROR_INVALID_FILENAME);
        } else {
            var entry = isFolder ? FileSystem.getDirectoryForPath(oldName) : FileSystem.getFileForPath(oldName);
            entry.rename(newName, function (err) {
                if (err) {
                    result.reject(err);
                } else {
                    result.resolve();
                }
            });
        }

        return result.promise();
    }

    /**
     * @private
     *
     * Renames the item at the old path to the new name provided.
     *
     * @param {string} oldPath full path to the current location of file or directory (should include trailing slash for directory)
     * @param {string} newName new name for the file or directory
     */
    ProjectModel.prototype._renameItem = function (oldPath, newName) {
        return _renameItem(oldPath, newName, !_pathIsFile(oldPath));
    };

    /**
     * Completes the rename operation that is in progress.
     */
    ProjectModel.prototype.performRename = function () {
        var renameInfo = this._selections.rename;
        if (!renameInfo) {
            return;
        }
        var oldPath         = renameInfo.path,
            isFolder        = renameInfo.isFolder || !_pathIsFile(oldPath),
            oldProjectPath  = this.makeProjectRelativeIfPossible(oldPath),

            // To get the parent directory, we need to strip off the trailing slash on a directory name
            parentDirectory = FileUtils.getDirectoryPath(isFolder ? FileUtils.stripTrailingSlash(oldPath) : oldPath),
            oldName         = FileUtils.getBaseName(oldPath),
            newName         = renameInfo.newName,
            newPath         = parentDirectory + newName,
            viewModel       = this._viewModel,
            self            = this;

        if (renameInfo.type !== FILE_CREATING && oldName === newName) {
            this.cancelRename();
            return;
        }

        if (isFolder) {
            newPath += "/";
        }

        delete this._selections.rename;
        delete this._selections.context;
        if (this._selections.selected === oldPath) {
            this._selections.selected = newPath;
        }

        viewModel.moveMarker("rename", oldProjectPath, null);
        viewModel.moveMarker("context", oldProjectPath, null);
        viewModel.moveMarker("creating", oldProjectPath, null);

        if (renameInfo.type === FILE_CREATING) {
            this.createAtPath(newPath).done(function (entry) {
                viewModel.renameItem(oldProjectPath, newName);

                renameInfo.deferred.resolve(entry);
            }).fail(function (error) {
                self._viewModel.deleteAtPath(self.makeProjectRelativeIfPossible(renameInfo.path));
                renameInfo.deferred.reject(error);
            });
        } else {
            this._renameItem(oldPath, newPath).then(function () {
                viewModel.renameItem(oldProjectPath, newName);
                renameInfo.deferred.resolve({
                    newPath: newPath
                });
            }).fail(function (errorType) {
                var errorInfo = {
                    type: errorType,
                    isFolder: isFolder,
                    fullPath: oldPath
                };
                renameInfo.deferred.reject(errorInfo);
            });
        }
    };

    /**
     * Creates a file or folder at the given path. Folder paths should have a trailing slash.
     *
     * If an error comes up during creation, the ERROR_CREATION event is triggered.
     *
     * @param {string} path full path to file or folder to create
     * @return {$.Promise} resolved when creation is complete
     */
    ProjectModel.prototype.createAtPath = function (path) {
        var isFolder  = !_pathIsFile(path),
            name      = FileUtils.getBaseName(path),
            self      = this;

        return doCreate(path, isFolder).done(function (entry) {
            if (!isFolder) {
                self.selectInWorkingSet(entry.fullPath);
            }
        }).fail(function (error) {
            $(self).trigger(ERROR_CREATION, {
                type: error,
                name: name,
                isFolder: isFolder
            });
        });
    };

    /**
     * Starts creating a file or folder with the given name in the given directory.
     *
     * The Promise returned is resolved with an object with a `newPath` property with the renamed path. If the user cancels the operation, the promise is resolved with the value RENAME_CANCELLED.
     *
     * @param {string} basedir directory that should contain the new entry
     * @param {string} newName initial name for the new entry (the user can rename it)
     * @param {boolean} isFolder `true` if the entry being created is a folder
     * @return {$.Promise} resolved when the user is done creating the entry.
     */
    ProjectModel.prototype.startCreating = function (basedir, newName, isFolder) {
        this.performRename();
        var d = new $.Deferred(),
            self = this;

        this.setDirectoryOpen(basedir, true).then(function () {
            self._viewModel.createPlaceholder(self.makeProjectRelativeIfPossible(basedir), newName, isFolder);
            var promise = self.startRename(basedir + newName);
            self._selections.rename.type = FILE_CREATING;
            if (isFolder) {
                self._selections.rename.isFolder = isFolder;
            }
            promise.then(d.resolve).fail(d.reject);
        }).fail(function (err) {
            d.reject(err);
        });
        return d.promise();
    };

    /**
     * Cancels the creation process that is underway. The original promise returned will be resolved with the
     * RENAME_CANCELLED value. The temporary entry added to the file tree will be deleted.
     */
    ProjectModel.prototype._cancelCreating = function () {
        var renameInfo = this._selections.rename;
        if (!renameInfo || renameInfo.type !== FILE_CREATING) {
            return;
        }
        this._viewModel.deleteAtPath(this.makeProjectRelativeIfPossible(renameInfo.path));
        renameInfo.deferred.resolve(RENAME_CANCELLED);
        delete this._selections.rename;
        this.setContext(null);
    };

    /**
     * Sets the `sortDirectoriesFirst` option for the file tree view.
     *
     * @param {boolean} True if directories should appear first
     */
    ProjectModel.prototype.setSortDirectoriesFirst = function (sortDirectoriesFirst) {
        this._viewModel.setSortDirectoriesFirst(sortDirectoriesFirst);
    };

    /**
     * Gets an array of arrays where each entry of the top-level array has an array
     * of paths that are at the same depth in the tree. All of the paths are full paths.
     *
     * @return {Array.<Array.<string>>} Array of array of full paths, organized by depth in the tree.
     */
    ProjectModel.prototype.getOpenNodes = function () {
        return this._viewModel.getOpenNodes(this.projectRoot.fullPath);
    };

    /**
     * Reopens a set of nodes in the tree by full path.
     * @param {Array.<Array.<string>>} nodesByDepth An array of arrays of node ids to reopen. The ids within
     *     each sub-array are reopened in parallel, and the sub-arrays are reopened in order, so they should
     *     be sorted by depth within the tree.
     * @return {$.Deferred} A promise that will be resolved when all nodes have been fully
     *     reopened.
     */
    ProjectModel.prototype.reopenNodes = function (nodesByDepth) {
        var deferred = new $.Deferred();

        if (!nodesByDepth || nodesByDepth.length === 0) {
            // All paths are opened and fully rendered.
            return deferred.resolve().promise();
        } else {
            var self = this;
            return Async.doSequentially(nodesByDepth, function (toOpenPaths) {
                return Async.doInParallel(
                    toOpenPaths,
                    function (path) {
                        return self._getDirectoryContents(path).then(function (contents) {
                            var relative = self.makeProjectRelativeIfPossible(path);
                            self._viewModel.setDirectoryContents(relative, contents);
                            self._viewModel.setDirectoryOpen(relative, true);
                        });
                    },
                    false
                );
            });
        }
    };

    /**
     * Refreshes the contents of the tree.
     *
     * @return {$.Promise} resolved when the tree has been refreshed
     */
    ProjectModel.prototype.refresh = function () {
        var projectRoot = this.projectRoot,
            openNodes   = this.getOpenNodes(),
            self        = this,
            selections  = this._selections,
            viewModel   = this._viewModel,
            deferred    = new $.Deferred();

        this.setProjectRoot(projectRoot).then(function () {
            self.reopenNodes(openNodes).then(function () {
                if (selections.selected) {
                    viewModel.moveMarker("selected", null, self.makeProjectRelativeIfPossible(selections.selected));
                }

                if (selections.context) {
                    viewModel.moveMarker("context", null, self.makeProjectRelativeIfPossible(selections.context));
                }

                if (selections.rename) {
                    viewModel.moveMarker("rename", null, self.makeProjectRelativeIfPossible(selections.rename));
                }

                deferred.resolve();
            });
        });

        return deferred.promise();
    };

    /**
     * Handles filesystem change events and prepares the update for the view model.
     *
     * @param {?(File|Directory)} entry File or Directory changed
     * @param {Array.<FileSystemEntry>=} added If entry is a Directory, contains zero or more added children
     * @param {Array.<FileSystemEntry>=} removed If entry is a Directory, contains zero or more removed
     */
    ProjectModel.prototype.handleFSEvent = function (entry, added, removed) {
        this._resetCache();

        if (!entry) {
            this.refresh();
            return;
        }

        if (!this.isWithinProject(entry)) {
            return;
        }

        var changes = {},
            self = this;

        if (entry.isFile) {
            changes.changed = [
                this.makeProjectRelativeIfPossible(entry.fullPath)
            ];
        } else {
            // Special case: a directory passed in without added and removed values
            // appears to be new.
            if (!added && !removed) {
                this._viewModel.ensureDirectoryExists(this.makeProjectRelativeIfPossible(entry.fullPath));
            }
        }

        if (added) {
            changes.added = added.map(function (entry) {
                return self.makeProjectRelativeIfPossible(entry.fullPath);
            });
        }

        if (removed) {
            if (this._selections.selected &&
                    _.find(removed, { fullPath: this._selections.selected })) {
                this.setSelected(null);
            }

            if (this._selections.rename &&
                    _.find(removed, { fullPath: this._selections.rename.path })) {
                this.cancelRename();
            }

            if (this._selections.context &&
                    _.find(removed, { fullPath: this._selections.context })) {
                this.setContext(null);
            }
            changes.removed = removed.map(function (entry) {
                return self.makeProjectRelativeIfPossible(entry.fullPath);
            });
        }

        this._viewModel.processChanges(changes);
    };

    /**
     * Closes the directory at path and recursively closes all of its children.
     *
     * @param {string} path Path of subtree to close
     */
    ProjectModel.prototype.closeSubtree = function (path) {
        this._viewModel.closeSubtree(this.makeProjectRelativeIfPossible(path));
    };

    /**
     * Toggle the open state of subdirectories.
     * @param {!string}  path        parent directory
     * @param {boolean} openOrClose  true to open directory, false to close
     * @return {$.Promise} promise resolved when the directories are open
     */
    ProjectModel.prototype.toggleSubdirectories = function (path, openOrClose) {
        var self = this,
            d = new $.Deferred();

        this.setDirectoryOpen(path, true).then(function () {
            var projectRelativePath = self.makeProjectRelativeIfPossible(path),
                childNodes = self._viewModel.getChildDirectories(projectRelativePath);

            Async.doInParallel(childNodes, function (node) {
                return self.setDirectoryOpen(path + node, openOrClose);
            }, true).then(function () {
                d.resolve();
            }, function (err) {
                d.reject(err);
            });
        });

        return d.promise();
    };

    /**
     * Although Brackets is generally standardized on folder paths with a trailing "/", some APIs here
     * receive project paths without "/" due to legacy preference storage formats, etc.
     * @param {!string} fullPath  Path that may or may not end in "/"
     * @return {!string} Path that ends in "/"
     */
    function _ensureTrailingSlash(fullPath) {
        if (_pathIsFile(fullPath)) {
            return fullPath + "/";
        }
        return fullPath;
    }

    /**
     * @private
     *
     * Returns the full path to the welcome project, which we open on first launch.
     *
     * @param {string} sampleUrl URL for getting started project
     * @param {string} initialPath Path to Brackets directory (see FileUtils.getNativeBracketsDirectoryPath())
     * @return {!string} fullPath reference
     */
    function _getWelcomeProjectPath(sampleUrl, initialPath) {
        if (sampleUrl) {
            // Back up one more folder. The samples folder is assumed to be at the same level as
            // the src folder, and the sampleUrl is relative to the samples folder.
            initialPath = initialPath.substr(0, initialPath.lastIndexOf("/")) + "/samples/" + sampleUrl;
        }

        return _ensureTrailingSlash(initialPath); // paths above weren't canonical
    }

    /**
     * @private
     *
     * Adds the path to the list of welcome projects we've ever seen, if not on the list already.
     *
     * @param {string} path Path to possibly add
     * @param {Array.<string>=} currentProjects Array of current welcome projects
     * @return {Array.<string>} New array of welcome projects with the additional project added
     */
    function _addWelcomeProjectPath(path, currentProjects) {
        var pathNoSlash = FileUtils.stripTrailingSlash(path);  // "welcomeProjects" pref has standardized on no trailing "/"

        var newProjects;

        if (currentProjects) {
            newProjects = _.clone(currentProjects);
        } else {
            newProjects = [];
        }

        if (newProjects.indexOf(pathNoSlash) === -1) {
            newProjects.push(pathNoSlash);
        }
        return newProjects;
    }

    /**
     * Returns true if the given path is the same as one of the welcome projects we've previously opened,
     * or the one for the current build.
     *
     * @param {string} path Path to check to see if it's a welcome project
     * @param {string} welcomeProjectPath Current welcome project path
     * @param {Array.<string>=} welcomeProjects All known welcome projects
     */
    function _isWelcomeProjectPath(path, welcomeProjectPath, welcomeProjects) {
        if (path === welcomeProjectPath) {
            return true;
        }

        // No match on the current path, and it's not a match if there are no previously known projects
        if (!welcomeProjects) {
            return false;
        }

        var pathNoSlash = FileUtils.stripTrailingSlash(path);  // "welcomeProjects" pref has standardized on no trailing "/"
        return welcomeProjects.indexOf(pathNoSlash) !== -1;
    }

    // Init invalid characters string
    if (brackets.platform === "mac") {
        _invalidChars = "?*|:";
    } else if (brackets.platform === "linux") {
        _invalidChars = "?*|/";
    } else {
        _invalidChars = "/?*:<>\\|\"";  // invalid characters on Windows
    }

    exports._getWelcomeProjectPath  = _getWelcomeProjectPath;
    exports._addWelcomeProjectPath  = _addWelcomeProjectPath;
    exports._isWelcomeProjectPath   = _isWelcomeProjectPath;
    exports._ensureTrailingSlash    = _ensureTrailingSlash;
    exports._shouldShowName         = _shouldShowName;
    exports._invalidChars           = _invalidChars;

    exports.shouldShow              = shouldShow;
    exports.isValidFilename         = isValidFilename;
    exports.EVENT_CHANGE            = EVENT_CHANGE;
    exports.EVENT_SHOULD_SELECT     = EVENT_SHOULD_SELECT;
    exports.EVENT_SHOULD_FOCUS      = EVENT_SHOULD_FOCUS;
    exports.ERROR_CREATION          = ERROR_CREATION;
    exports.ERROR_INVALID_FILENAME  = ERROR_INVALID_FILENAME;
    exports.FILE_RENAMING           = FILE_RENAMING;
    exports.FILE_CREATING           = FILE_CREATING;
    exports.RENAME_CANCELLED        = RENAME_CANCELLED;
    exports.doCreate                = doCreate;
    exports.ProjectModel            = ProjectModel;
});


//
// Close icons
// --------------------------------------------------

.close {
  float: right;
  font-size: 20px;
  font-weight: bold;
  line-height: @baseLineHeight;
  color: @black;
  text-shadow: 0 1px 0 rgba(255,255,255,1);
  .opacity(20);
  &:hover,
  &:focus {
    color: @black;
    text-decoration: none;
    cursor: pointer;
    .opacity(40);
  }
}

// Additional properties for button version
// iOS requires the button element instead of an anchor tag.
// If you want the anchor version, it requires `href="#"`.
button.close {
  padding: 0;
  cursor: pointer;
  background: transparent;
  border: 0;
  -webkit-appearance: none;
}