keep keep keep keep add keep keep keep keep keep

if (this._selections.selected === oldPath) {

/*
 * 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;
        }

        this.performRename();

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

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

        if (path) {
            $(this).trigger(EVENT_SHOULD_FOCUS);

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

    /**
     * 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.
     */
    ProjectModel.prototype.setContext = function (path, _doNotRename) {
        // 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 (!path) {
            this._selections.previousContext = this._selections.context;
        } else {
            this._selections.previousContext = path;
        }

        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;
        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.setSelected(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;
});


// Load modules

var Lab = require('lab');
var Hawk = require('../lib');
var Package = require('../package.json');

// Declare internals

var internals = {};

// Test shortcuts

var expect = Lab.expect;
var before = Lab.before;
var after = Lab.after;
var describe = Lab.experiment;
var it = Lab.test;

describe('Hawk', function () {

    describe('Utils', function () {

        describe('#parseHost', function () {

            it('returns port 80 for non tls node request', function (done) {

                var req = {
                    method: 'POST',
                    url: '/resource/4?filter=a',
                    headers: {
                        host: 'example.com',
                        'content-type': 'text/plain;x=y'
                    }
                };

                expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(80);
                done();
            });

            it('returns port 443 for non tls node request', function (done) {

                var req = {
                    method: 'POST',
                    url: '/resource/4?filter=a',
                    headers: {
                        host: 'example.com',
                        'content-type': 'text/plain;x=y'
                    },
                    connection: {
                        encrypted: true
                    }
                };

                expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(443);
                done();
            });

            it('returns port 443 for non tls node request (IPv6)', function (done) {

                var req = {
                    method: 'POST',
                    url: '/resource/4?filter=a',
                    headers: {
                        host: '[123:123:123]',
                        'content-type': 'text/plain;x=y'
                    },
                    connection: {
                        encrypted: true
                    }
                };

                expect(Hawk.utils.parseHost(req, 'Host').port).to.equal(443);
                done();
            });

            it('parses IPv6 headers', function (done) {

                var req = {
                    method: 'POST',
                    url: '/resource/4?filter=a',
                    headers: {
                        host: '[123:123:123]:8000',
                        'content-type': 'text/plain;x=y'
                    },
                    connection: {
                        encrypted: true
                    }
                };

                var host = Hawk.utils.parseHost(req, 'Host');
                expect(host.port).to.equal('8000');
                expect(host.name).to.equal('[123:123:123]');
                done();
            });
        });

        describe('#version', function () {

            it('returns the correct package version number', function (done) {

                expect(Hawk.utils.version()).to.equal(Package.version);
                done();
            });
        });

        describe('#unauthorized', function () {

            it('returns a hawk 401', function (done) {

                expect(Hawk.utils.unauthorized('kaboom').response.headers['WWW-Authenticate']).to.equal('Hawk error="kaboom"');
                done();
            });
        });
    });
});


<!DOCTYPE html>

	<meta charset="utf-8">
	<title>jQuery UI Slider - Range with fixed maximum</title>
	<link rel="stylesheet" href="../../themes/base/jquery.ui.all.css">

	<link rel="stylesheet" href="../demos.css">

	Minimum number of bedrooms:
	<input type="text" id="amount" style="border:0; color:#f6931f; font-weight:bold;" />

Fix the maximum value of the range slider so that the user can only select a minimum.  Set the range option to "max."


"use strict"; // run code in ES5 strict mode

var setterSrc = require("../../__set__.js").toString(),
    getterSrc = require("../../__get__.js").toString(),
    path = require("path"),
    injectRewire = require("../injectRewire.js"),
    getRewireRegExp = require("../getRewireRegExp.js"),

    rewireLib = path.resolve(__dirname, "../../"),
    projectBasePath = path.resolve(__dirname, "../../../../../"),
    nodeModulesPath = path.join(projectBasePath, "node_modules"),
    webpackPath = path.join("node_modules", "webpack"),
    settersAndGettersSrc;

/**
 * Injects special code so rewire gains access to the module's private scope.
 *
 * Furthermore it changes all calls of rewire("some/path") to rewire("some/path", require("some/path")) so webpack
 * recognizes the additional dependency. This also enables rewire to resolve the module because webpack replaces all
 * paths to numeric ids.
 *
 * @param {!String} src
 * @return {String} src
 */
function webpackPostLoader(src) {
    var filename = this.request.split("!").pop(),
        rewireRegExp = getRewireRegExp();

    if (isRewireableModule(filename)) {
        // replaces rewire("some/path") into rewire("some/path", require("some/path"))
        src = src.replace(rewireRegExp, '$1rewire("$2", require("$2"))');

        // Inject special code
        src = injectRewire(src, settersAndGettersSrc);
    }

    return src;
}

webpackPostLoader.loader = __filename;
webpackPostLoader.test = /\.js$/;

/**
 * Returns true if the module is rewireable. Rewireable are all modules of the project.
 *
 * Example:
 * Imagine rewire lies under "~/myProject/node_modules/rewire". All files in "~/myProject" are rewireable except
 * the "~/myProject/node_modules"-path.
 *
 * @param {!String} path
 * @return {Boolean}
 */
function isRewireableModule(path) {
    return path.indexOf(projectBasePath) !== -1 &&
        path.indexOf(nodeModulesPath) === -1 &&

        // "rewire/lib" and "node_modules/webpack" are explicitly excluded to make the tests work
        path.indexOf(rewireLib) === -1 &&
        path.indexOf(webpackPath) === -1;
}

/**
 * This string gets injected at the beginning of every module. Its purpose is to
 * - register the setters and getters according to the module's filename
 *
 * @private
 * @type {String}
 */
settersAndGettersSrc = (
    'var rewire = require("rewire"); ' +

    // Registers the setters and getters of every module according to their filename. The setters and getters must be
    // injected as string here to gain access to the private scope of the module.
    'rewire.register(module, ' + setterSrc + ', ' + getterSrc + '); ' +

    // Cleaning up
    'rewire = undefined;'
).replace(/\s+/g, " ");   // strip out unnecessary spaces to be unobtrusive in the debug view

module.exports = webpackPostLoader;


/* Polish initialisation for the jQuery UI date picker plugin. */
/* Written by Jacek Wysocki (jacek.wysocki@gmail.com). */
jQuery(function($){
	$.datepicker.regional['pl'] = {
		closeText: 'Zamknij',
		prevText: '<Poprzedni',
		nextText: 'Następny>',
		currentText: 'Dziś',
		monthNames: ['Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec',
		'Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień'],
		monthNamesShort: ['Sty','Lu','Mar','Kw','Maj','Cze',
		'Lip','Sie','Wrz','Pa','Lis','Gru'],
		dayNames: ['Niedziela','Poniedziałek','Wtorek','Środa','Czwartek','Piątek','Sobota'],
		dayNamesShort: ['Nie','Pn','Wt','Śr','Czw','Pt','So'],
		dayNamesMin: ['N','Pn','Wt','Śr','Cz','Pt','So'],
		weekHeader: 'Tydz',
		dateFormat: 'dd.mm.yy',
		firstDay: 1,
		isRTL: false,
		showMonthAfterYear: false,
		yearSuffix: ''};
	$.datepicker.setDefaults($.datepicker.regional['pl']);
});


it("does nothing if setRenameValue is called when there's no rename in progress", function () {

it("adjusts the selection if the renamed file was selected", function () {


/*
 * 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.
 *
 */

.brackets-css-hints .matched-hint {
    font-weight: 500;
}


/*
 * 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: FileTreeViewModel*/

/**
 * The view model (or a Store in the Flux terminology) used by the file tree.
 *
 * Many of the view model's methods are implemented by pure functions, which can be
 * helpful for composability. Many of the methods commit the new treeData and send a
 * change event when they're done whereas the functions do not do this.
 */
define(function (require, exports, module) {
    "use strict";

    var Immutable           = require("thirdparty/immutable"),
        _                   = require("thirdparty/lodash"),
        FileUtils           = require("file/FileUtils");

    // Constants
    var EVENT_CHANGE = "change";

    /**
     * Determine if an entry from the treeData map is a file.
     *
     * @param {Immutable.Map} entry entry to test
     * @return {boolean} true if this is a file and not a directory
     */
    function isFile(entry) {
        return entry.get("children") === undefined;
    }

    /**
     * @constructor
     *
     * Contains the treeData used to generate the file tree and methods used to update that
     * treeData.
     */
    function FileTreeViewModel() {
        // For convenience in callbacks, make a bound version of this method so that we can
        // just refer to it as this._commitTreeData when passing in a callback.
        this._commitTreeData = this._commitTreeData.bind(this);
    }

    /**
     * @type {boolean}
     *
     * Preference for whether directories should all be sorted to the top of listings
     */
    FileTreeViewModel.prototype.sortDirectoriesFirst = false;

    /**
     * @type {Immutable.Map}
     * @private
     *
     * The data for the tree. Some notes about its structure:
     *
     * * It starts with a Map for the project root's contents.
     * * Each directory entry has a `children` key.
     *     * `children` will be null if the directory has not been loaded
     *     * An `open` key denotes whether the directory is open
     * * Most file entries are just empty maps
     *     * They can have flags like selected, context, rename, create with state information for the tree
     */
    FileTreeViewModel.prototype._treeData = Immutable.Map();

    Object.defineProperty(FileTreeViewModel.prototype, "treeData", {
        get: function () {
            return this._treeData;
        }
    });

    /**
     * @private
     * @type {Immutable.Map}
     * Selection view information determines how the selection bar appears.
     *
     * * width: visible width of the selection area
     * * scrollTop: current scroll position.
     * * scrollLeft: current horizontal scroll position
     * * offsetTop: top of the scroller element
     * * hasSelection: is the selection bar visible?
     * * hasContext: is the context bar visible?
     */
    FileTreeViewModel.prototype._selectionViewInfo = Immutable.Map({
        width: 0,
        scrollTop: 0,
        scrollLeft: 0,
        offsetTop: 0,
        hasContext: false,
        hasSelection: false
    });

    Object.defineProperty(FileTreeViewModel.prototype, "selectionViewInfo", {
        get: function () {
            return this._selectionViewInfo;
        }
    });

    /**
     * @private
     *
     * If the project root changes, we reset the tree data so that everything will be re-read.
     */
    FileTreeViewModel.prototype._rootChanged = function () {
        this._treeData = Immutable.Map();
    };

    /**
     * @private
     *
     * The FileTreeViewModel is like a database for storing the directory contents and its
     * state moves atomically from one value to the next. This method stores the next version
     * of the state, if it has changed, and triggers a change event so that the UI can update.
     *
     * @param {Immutable.Map} treeData new treeData state
     */
    FileTreeViewModel.prototype._commitTreeData = function (treeData) {
        if (treeData && treeData !== this._treeData) {
            this._treeData = treeData;
            $(this).trigger(EVENT_CHANGE);
        }
    };

    /**
     * @private
     *
     * Converts a project-relative file path into an object path array suitable for
     * `Immutable.Map.getIn` and `Immutable.Map.updateIn`.
     *
     * The root path is "".
     *
     * @param {Immutable.Map} treeData
     * @param {string} path project relative path to the file or directory. Can include trailing slash.
     * @return {Array.<string>|null} Returns null if the path can't be found in the tree, otherwise an array of strings representing the path through the object.
     */
    function _filePathToObjectPath(treeData, path) {
        if (path === null) {
            return null;
        } else if (path === "") {
            return [];
        }

        var parts = path.split("/"),
            part = parts.shift(),
            result = [],
            node;

        // Step through the parts of the path and the treeData object simultaneously
        while (part) {
            // We hit the end of the tree without finding our object, so return null
            if (!treeData) {
                return null;
            }

            node = treeData.get(part);

            // The name represented by `part` isn't in the tree, so return null.
            if (node === undefined) {
                return null;
            }

            // We've verified this part, so store it.
            result.push(part);

            // Pull the next part of the path
            part = parts.shift();

            // If we haven't passed the end of the path string, then the object we've got in hand
            // *should* be a directory. Confirm that and add `children` to the path to move down
            // to the next directory level.
            if (part) {
                treeData = node.get("children");
                if (treeData) {
                    result.push("children");
                }
            }
        }

        return result;
    }

    /**
     * @private
     *
     * See `FileTreeViewModel.isFilePathVisible`
     */
    function _isFilePathVisible(treeData, path) {
        if (path === null) {
            return null;
        } else if (path === "") {
            return true;
        }

        var parts = path.split("/"),
            part = parts.shift(),
            result = [],
            node;

        while (part) {
            if (treeData === null) {
                return false;
            }
            node = treeData.get(part);
            if (node === undefined) {
                return null;
            }
            result.push(part);
            part = parts.shift();
            if (part) {
                if (!node.get("open")) {
                    return false;
                }
                treeData = node.get("children");
                if (treeData) {
                    result.push("children");
                }
            }
        }

        return true;
    }

    /**
     * Determines if a given file path is visible within the tree.
     *
     * For detailed documentation on how the loop works, see `_filePathToObjectPath` which
     * follows the same pattern. This differs from that function in that this one checks for
     * the open state of directories and has a different return value.
     *
     * @param {string} path project relative file path
     * @return {boolean|null} true if the given path is currently visible in the tree, null if the given path is not present in the tree.
     */
    FileTreeViewModel.prototype.isFilePathVisible = function (path) {
        return _isFilePathVisible(this._treeData, path);
    };

    /**
     * Determines if a given path has been loaded.
     *
     * @param {string} path project relative file or directory path
     * @return {boolean} true if the path has been loaded
     */
    FileTreeViewModel.prototype.isPathLoaded = function (path) {
        var objectPath = _filePathToObjectPath(this._treeData, path);

        if (!objectPath) {
            return false;
        }

        // If it's a directory, make sure that its children are loaded
        if (_.last(path) === "/") {
            objectPath.push("children");
            if (!this._treeData.getIn(objectPath)) {
                return false;
            }
        }

        return true;
    };

    /**
     * @private
     *
     * See `FileTreeViewModel.getOpenNodes`.
     */
    function _getOpenNodes(treeData, projectRootPath) {
        var openNodes = [];

        function addNodesAtDepth(treeData, parent, depth) {
            if (!treeData) {
                return;
            }

            treeData.forEach(function (value, key) {
                if (isFile(value)) {
                    return;
                }

                var directoryPath = parent + key + "/";

                if (value.get("open")) {
                    var nodeList = openNodes[depth];
                    if (!nodeList) {
                        nodeList = openNodes[depth] = [];
                    }
                    nodeList.push(directoryPath);
                    addNodesAtDepth(value.get("children"), directoryPath, depth + 1);
                }
            });
        }

        // start at the top of the tree and the first array element
        addNodesAtDepth(treeData, projectRootPath, 0);
        return openNodes;
    }

    /**
     * @private
     * TODO: merge with _getOpenNodes?!
     * See `FileTreeViewModel.getChildNodes`.
     */
    function _getChildDirectories(treeData, projectRootPath) {
        var childDirectories = [];

        function addNodesAtDepth(treeData, parent, depth) {
            if (!treeData) {
                return;
            }

            treeData.forEach(function (value, key) {
                if (!isFile(value)) {
                    var directoryPath = key + "/";

                    childDirectories.push(directoryPath);
                }
            });
        }

        // start at the top of the tree and the first array element
        addNodesAtDepth(treeData, projectRootPath, 0);
        return childDirectories;
    }

    /**
     * Creates 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.
     *
     * This is used for saving the current set of open nodes to the preferences system
     * for restoring on project open.
     *
     * @param {string} projectRootPath Full path to the project root
     * @return {Array.<Array.<string>>} Array of array of full paths, organized by depth in the tree.
     */
    FileTreeViewModel.prototype.getOpenNodes = function (projectRootPath) {
        return _getOpenNodes(this._treeData, projectRootPath);
    };

    FileTreeViewModel.prototype.getChildDirectories = function (parent) {
        var treeData = this._treeData,
            objectPath = _filePathToObjectPath(treeData, parent);

        if (!objectPath) {
            return;
        }

        var children;
        if (objectPath.length === 0) {
            // this is the root of the tree
            children = this._treeData;
        } else {
            objectPath.push("children");
            children = this._treeData.getIn(objectPath);
        }

        return _getChildDirectories(children, parent);
    };

    /**
     * @private
     *
     * The Immutable package does not have a `setIn` method, which is what this effectively
     * provides. This is a simple function that does an `updateIn` on treeData, replacing
     * the current value with the new one.
     *
     * @param {Immutable.Map} treeData
     * @param {Array.<string>} objectPath path to object that should be replaced
     * @param {Immutable.Map} newValue new value to provide at that path
     * @return {Immutable.Map} updated treeData
     */
    function _setIn(treeData, objectPath, newValue) {
        return treeData.updateIn(objectPath, function (oldValue) {
            return newValue;
        });
    }

    /**
     * @private
     *
     * See `FileTreeViewModel.moveMarker`
     */
    function _moveMarker(treeData, markerName, oldPath, newPath) {
        var objectPath;

        if (newPath) {
            objectPath = _filePathToObjectPath(treeData, newPath);
        }

        var newTreeData = treeData;

        if (oldPath && oldPath !== newPath) {
            var lastObjectPath = _filePathToObjectPath(treeData, oldPath);
            if (lastObjectPath) {
                newTreeData = newTreeData.updateIn(lastObjectPath, function (entry) {
                    return entry.delete(markerName);
                });
            }
        }

        if (newPath && objectPath && objectPath.length !== 0) {
            newTreeData = newTreeData.updateIn(objectPath, function (entry) {
                return entry.set(markerName, true);
            });
        }

        return newTreeData;
    }

    /**
     * Moves a boolean marker flag from one file path to another.
     *
     * @param {string} markerName Name of the flag to set (for example, "selected")
     * @param {string|null} oldPath Project relative file path with the location of the marker to move, or null if it's not being moved from elsewhere in the tree
     * @param {string|null} newPath Project relative file path with where to place the marker, or null if the marker is being removed from the tree
     */
    FileTreeViewModel.prototype.moveMarker = function (markerName, oldPath, newPath) {
        var newTreeData = _moveMarker(this._treeData, markerName, oldPath, newPath);

        if (markerName === "selected") {
            this._selectionViewInfo = this._selectionViewInfo.set("hasSelection", !!newPath);
        } else if (markerName === "context") {
            this._selectionViewInfo = this._selectionViewInfo.set("hasContext", !!newPath);
        }
        this._commitTreeData(newTreeData);
    };

    /**
     * Changes the name of the item at the `currentPath` to `newName`.
     *
     * @param {string} currentPath project relative file path to the current item
     * @param {string} newName Name to give the item
     */
    FileTreeViewModel.prototype.renameItem = function (currentPath, newName) {
        var treeData = this._treeData,
            objectPath = _filePathToObjectPath(treeData, currentPath);

        if (!objectPath) {
            return;
        }

        var originalName = _.last(objectPath),
            currentObject = treeData.getIn(objectPath);

        // Back up to the parent directory
        objectPath.pop();

        treeData = treeData.updateIn(objectPath, function (directory) {
            directory = directory.delete(originalName);
            directory = directory.set(newName, currentObject);
            return directory;
        });

        this._commitTreeData(treeData);
    };

    /**
     * @private
     *
     * See `FileTreeViewModel.setDirectoryOpen`
     */
    function _setDirectoryOpen(treeData, path, open) {
        var objectPath = _filePathToObjectPath(treeData, path),
            directory = treeData.getIn(objectPath);

        if (!objectPath) {
            return {
                needsLoading: true,
                treeData: treeData
            };
        }

        if (isFile(directory)) {
            return;
        }

        var alreadyOpen = directory.get("open") === true;

        if ((alreadyOpen && open) || (!alreadyOpen && !open)) {
            return;
        }

        treeData = treeData.updateIn(objectPath, function (directory) {
            if (open) {
                return directory.set("open", true);
            } else {
                return directory.delete("open");
            }
        });

        if (open && (directory.get("children") === null || directory.get("notFullyLoaded"))) {
            return {
                needsLoading: true,
                treeData: treeData
            };
        }
        return {
            needsLoading: false,
            treeData: treeData
        };
    }

    /**
     * Sets the directory at the given path to open or closed. Returns true if the directory
     * contents need to be loaded.
     *
     * @param {string} path Project relative file path to the directory
     * @param {boolean} open True to open the directory
     * @return {boolean} true if the directory contents need to be loaded.
     */
    FileTreeViewModel.prototype.setDirectoryOpen = function (path, open) {
        var result = _setDirectoryOpen(this._treeData, path, open);
        if (result && result.treeData) {
            this._commitTreeData(result.treeData);
        }
        return result ? result.needsLoading : false;
    };

    /**
     * Returns the object at the given file path.
     *
     * @param {string} path Path to the object
     * @return {Immutable.Map=} directory or file object from the tree. Null if it's not found.
     */
    FileTreeViewModel.prototype._getObject = function (path) {
        var objectPath = _filePathToObjectPath(this._treeData, path);
        if (!objectPath) {
            return null;
        }
        return this._treeData.getIn(objectPath);
    };

    /**
     * Closes a subtree path, given by an object path.
     *
     * @param {Immutable.Map} directory Current directory
     * @return {Immutable.Map} new directory
     */
    function _closeSubtree(directory) {
        directory = directory.delete("open");

        var children = directory.get("children");
        if (children) {
            children.keySeq().forEach(function (name) {
                var subdir = children.get(name);
                if (!isFile(subdir)) {
                    subdir = _closeSubtree(subdir);
                    children = children.set(name, subdir);
                }
            });
        }

        directory = directory.set("children", children);
        return directory;
    }

    /**
     * Closes the directory at path and recursively closes all of its children.
     *
     * @param {string} path Path of subtree to close
     */
    FileTreeViewModel.prototype.closeSubtree = function (path) {
        var treeData = this._treeData,
            subtreePath = _filePathToObjectPath(treeData, path);

        if (!subtreePath) {
            return;
        }

        var directory = treeData.getIn(subtreePath);

        directory = _closeSubtree(directory);
        treeData = _setIn(treeData, subtreePath, directory);
        this._commitTreeData(treeData);
    };

    /**
     * @private
     *
     * Takes an array of file system entries and merges them into the children map
     * of a directory in the view model treeData.
     *
     * @param {Immutable.Map} children current children in the directory
     * @param {Array.<FileSystemEntry>} contents FileSystemEntry objects currently in the directory
     * @return {Immutable.Map} updated children
     */
    function _mergeContentsIntoChildren(children, contents) {

        // We keep track of the names we've seen among the current directory entries to make
        // it easy to spot the names that we *haven't* seen (in other words, files that have
        // been deleted).
        var keysSeen = [];

        children = children.withMutations(function (children) {

            // Loop through the directory entries
            contents.forEach(function (entry) {
                keysSeen.push(entry.name);

                var match = children.get(entry.name);
                if (match) {
                    // Confirm that a name that used to represent a file and now represents a
                    // directory (or vice versa) isn't what we've encountered here. If we have
                    // hit this situation, pretend the current child of treeData doesn't exist
                    // so we can replace it.
                    var matchIsFile = isFile(match);
                    if (matchIsFile !== entry.isFile) {
                        match = undefined;
                    }
                }

                // We've got a new entry that we need to add.
                if (!match) {
                    if (entry.isFile) {
                        children.set(entry.name, Immutable.Map());
                    } else {
                        children.set(entry.name, Immutable.Map({
                            children: null
                        }));
                    }
                }
            });

            // Look at the list of names that we currently have in the treeData that no longer
            // appear in the directory and delete those.
            var currentEntries = children.keySeq().toJS(),
                deletedEntries = _.difference(currentEntries, keysSeen);

            deletedEntries.forEach(function (name) {
                children.delete(name);
            });
        });
        return children;
    }

    /**
     * @private
     *
     * Creates a directory object (or updates an existing directory object) to look like one
     * that has not yet been loaded, but in which we want to start displaying entries.
     * @param {Immutable.Map=} directory Directory entry to update
     * @return {Immutable.Map} New or updated directory object
     */
    function _createNotFullyLoadedDirectory(directory) {
        if (!directory) {
            return Immutable.Map({
                notFullyLoaded: true,
                children: Immutable.Map()
            });
        }
        return directory.merge({
            notFullyLoaded: true,
            children: Immutable.Map()
        });
    }

    /**
     * @private
     *
     * Creates the directories necessary to display the given path, even if those directories
     * do not yet exist in the tree and have not been loaded.
     *
     * @param {Immutable.Map} treeData
     * @param {string} path Path to the final directory to be added in the tree
     * @return {{treeData: Immutable.Map, objectPath: Array.<string>}} updated treeData and object path to the created object
     */
    function _createIntermediateDirectories(treeData, path) {
        var objectPath = [],
            result = {
                objectPath: objectPath,
                treeData: treeData
            },
            treePointer = treeData;

        if (path === "") {
            return result;
        }

        var parts = path.split("/"),
            part = parts.shift(),
            node;

        while (part) {
            if (treePointer === null) {
                return null;
            }
            node = treePointer.get(part);
            objectPath.push(part);

            // This directory is missing, so create it.
            if (node === undefined) {
                treeData = treeData.updateIn(objectPath, _createNotFullyLoadedDirectory);
                node = treeData.getIn(objectPath);
            }

            part = parts.shift();
            if (part) {
                treePointer = node.get("children");

                if (treePointer) {
                    objectPath.push("children");
                } else {

                    // The directory is there, but the directory hasn't been loaded.
                    // Update the directory to be a `notFullyLoaded` directory
                    treeData = treeData.updateIn(objectPath, _createNotFullyLoadedDirectory);
                    objectPath.push("children");
                    treePointer = treeData.getIn(objectPath);
                }
            }
        }

        result.treeData = treeData;
        return result;
    }

    /**
     * Updates the directory at the given path with the new contents. If the parent directories
     * of this directory have not been loaded yet, they will be created. This allows directories
     * to be loaded in any order.
     *
     * @param {string} Project relative path to the directory that is being updated.
     * @param {Array.<FileSystemEntry>} Current contents of the directory
     */
    FileTreeViewModel.prototype.setDirectoryContents = function (path, contents) {
        path = FileUtils.stripTrailingSlash(path);

        var intermediate = _createIntermediateDirectories(this._treeData, path),
            objectPath = intermediate.objectPath,
            treeData = intermediate.treeData;

        if (objectPath === null) {
            return;
        }

        var directory = treeData.getIn(objectPath),
            children = directory;

        // The root directory doesn't need this special handling.
        if (path !== "") {

            // The user of this API passed in a path to a file rather than a directory.
            // Perhaps this should be an exception?
            if (isFile(directory)) {
                return;
            }

            // If the directory had been created previously as `notFullyLoaded`, we can
            // remove that flag now because this is the step that is loading the directory.
            if (directory.get("notFullyLoaded")) {
                directory = directory.delete("notFullyLoaded");
            }

            if (!directory.get("children")) {
                directory = directory.set("children", Immutable.Map());
            }

            treeData = _setIn(treeData, objectPath, directory);

            objectPath.push("children");
            children = directory.get("children");
        }

        children = _mergeContentsIntoChildren(children, contents);
        treeData = _setIn(treeData, objectPath, children);
        this._commitTreeData(treeData);
    };

    /**
     * @private
     *
     * Opens the directories along the provided path.
     *
     * @param {Immutable.Map} treeData
     * @param {string} path Path to open
     */
    function _openPath(treeData, path) {
        var objectPath = _filePathToObjectPath(treeData, path);

        function setOpen(node) {
            return node.set("open", true);
        }

        while (objectPath && objectPath.length) {
            var node = treeData.getIn(objectPath);
            if (isFile(node)) {
                objectPath.pop();
            } else {
                if (!node.get("open")) {
                    treeData = treeData.updateIn(objectPath, setOpen);
                }
                objectPath.pop();
                if (objectPath.length) {
                    objectPath.pop();
                }
            }
        }

        return treeData;
    }

    /**
     * Opens the directories along the given path.
     *
     * @param {string} path Project-relative path
     */
    FileTreeViewModel.prototype.openPath = function (path) {
        this._commitTreeData(_openPath(this._treeData, path));
    };

    /**
     * @private
     *
     * See FileTreeViewModel.createPlaceholder
     */
    function _createPlaceholder(treeData, basedir, name, isFolder, options) {
        options = options || {};
        var parentPath = _filePathToObjectPath(treeData, basedir);

        if (!parentPath) {
            return;
        }

        var newObject = {
        };

        if (!options.notInCreateMode) {
            newObject.creating = true;
        }

        if (isFolder) {
            // If we're creating a folder, then we know it's empty.
            // But if we're not in create mode, (we're adding a folder based on an
            // FS event), we don't know anything about the new directory's children.
            if (options.notInCreateMode) {
                newObject.children = null;
            } else {
                newObject.children = Immutable.Map();
            }
        }

        var newFile = Immutable.Map(newObject);

        if (!options.doNotOpen) {
            treeData = _openPath(treeData, basedir);
        }
        if (parentPath.length > 0) {
            var childrenPath = _.clone(parentPath);
            childrenPath.push("children");

            treeData = treeData.updateIn(childrenPath, function (children) {
                return children.set(name, newFile);
            });
        } else {
            treeData = treeData.set(name, newFile);
        }
        return treeData;
    }

    /**
     * Creates a placeholder file or directory that appears in the tree so that the user
     * can provide a name for the new entry.
     *
     * @param {string} basedir Directory that contains the new file or folder
     * @param {string} name Initial name to give the new entry
     * @param {boolean} isFolder true if the entry being created is a folder
     */
    FileTreeViewModel.prototype.createPlaceholder = function (basedir, name, isFolder) {
        var treeData = _createPlaceholder(this._treeData, basedir, name, isFolder);
        this._commitTreeData(treeData);
    };

    /**
     * @private
     *
     * See FileTreeViewModel.deleteAtPath
     */
    function _deleteAtPath(treeData, path) {
        var objectPath = _filePathToObjectPath(treeData, path);

        if (!objectPath) {
            return;
        }

        var originalName = _.last(objectPath);

        // Back up to the parent directory
        objectPath.pop();

        treeData = treeData.updateIn(objectPath, function (directory) {
            directory = directory.delete(originalName);
            return directory;
        });

        return treeData;
    }

    /**
     * Deletes the entry at the given path.
     *
     * @param {string} path Project-relative path to delete
     */
    FileTreeViewModel.prototype.deleteAtPath = function (path) {
        var treeData = _deleteAtPath(this._treeData, path);
        if (treeData) {
            this._commitTreeData(treeData);
        }
    };

    /**
     * @private
     *
     * Adds a timestamp to an entry (much like the "touch" command) to force a given entry
     * to rerender.
     */
    function _addTimestamp(item) {
        return item.set("_timestamp", new Date().getTime());
    }

    /**
     * @private
     *
     * Sets/updates the timestamp on the file paths listed in the `changed` array.
     *
     * @param {Immutable.Map} treeData
     * @param {Array.<string>} changed list of changed project-relative file paths
     * @return {Immutable.Map} revised treeData
     */
    function _markAsChanged(treeData, changed) {
        changed.forEach(function (filePath) {
            var objectPath = _filePathToObjectPath(treeData, filePath);
            if (objectPath) {
                treeData = treeData.updateIn(objectPath, _addTimestamp);
            }
        });
        return treeData;
    }

    /**
     * @private
     *
     * Adds entries at the paths listed in the `added` array. Directories should have a trailing slash.
     *
     * @param {Immutable.Map} treeData
     * @param {Array.<string>} added list of new project-relative file paths
     * @return {Immutable.Map} revised treeData
     */
    function _addNewEntries(treeData, added) {
        added.forEach(function (filePath) {
            var isFolder = _.last(filePath) === "/";

            filePath = isFolder ? filePath.substr(0, filePath.length - 1) : filePath;

            var parentPath = FileUtils.getDirectoryPath(filePath),
                parentObjectPath = _filePathToObjectPath(treeData, parentPath),
                basename = FileUtils.getBaseName(filePath);

            if (parentObjectPath) {
                // Verify that the children are loaded
                var childrenPath = _.clone(parentObjectPath);
                childrenPath.push("children");
                if (treeData.getIn(childrenPath) === null) {
                    return;
                }

                treeData = _createPlaceholder(treeData, parentPath, basename, isFolder, {
                    notInCreateMode: true,
                    doNotOpen: true
                });
            }
        });

        return treeData;
    }

    /**
     * Applies changes to the tree. The `changes` object can have one or more of the following keys which all
     * have arrays of project-relative paths as their values:
     *
     * * `changed`: entries that have changed in some way that should be re-rendered
     * * `added`: new entries that need to appear in the tree
     * * `removed`: entries that have been deleted from the tree
     *
     * @param {{changed: Array.<string>=, added: Array.<string>=, removed: Array.<string>=}}
     */
    FileTreeViewModel.prototype.processChanges = function (changes) {
        var treeData = this._treeData;

        if (changes.changed) {
            treeData = _markAsChanged(treeData, changes.changed);
        }

        if (changes.added) {
            treeData = _addNewEntries(treeData, changes.added);
        }

        if (changes.removed) {
            changes.removed.forEach(function (path) {
                treeData = _deleteAtPath(treeData, path);
            });
        }

        this._commitTreeData(treeData);
    };

    /**
     * Makes sure that the directory exists. This will create a directory object (unloaded)
     * if the directory does not already exist. A change message is also fired in that case.
     *
     * This is useful for file system events which can refer to a directory that we don't
     * know about already.
     *
     * @param {string} path Project-relative path to the directory
     */
    FileTreeViewModel.prototype.ensureDirectoryExists = function (path) {
        var treeData          = this._treeData,
            pathWithoutSlash  = FileUtils.stripTrailingSlash(path),
            parentPath        = FileUtils.getDirectoryPath(pathWithoutSlash),
            name              = pathWithoutSlash.substr(parentPath.length),
            targetPath        = [];

        if (parentPath) {
            targetPath = _filePathToObjectPath(treeData, parentPath);
            if (!targetPath) {
                return;
            }
            targetPath.push("children");
            if (!treeData.getIn(targetPath)) {
                return;
            }
        }

        targetPath.push(name);

        if (treeData.getIn(targetPath)) {
            return;
        }

        treeData = _setIn(treeData, targetPath, Immutable.Map({
            children: null
        }));

        this._commitTreeData(treeData);
    };

    /**
     * Sets the value of the `sortDirectoriesFirst` flag which tells to view that directories
     * should be listed before the alphabetical listing of files.
     *
     * @param {boolean} sortDirectoriesFirst True if directories should be displayed first
     */
    FileTreeViewModel.prototype.setSortDirectoriesFirst = function (sortDirectoriesFirst) {
        if (sortDirectoriesFirst !== this.sortDirectoriesFirst) {
            this.sortDirectoriesFirst = sortDirectoriesFirst;
            $(this).trigger(EVENT_CHANGE);
        }
    };

    /**
     * Sets the width of the selection bar.
     *
     * @param {int} width New width
     */
    FileTreeViewModel.prototype.setSelectionWidth = function (width) {
        this._selectionViewInfo = this._selectionViewInfo.set("width", width);
        $(this).trigger(EVENT_CHANGE);
    };

    /**
     * Sets the scroll position of the file tree to help position the selection bar.
     *
     * @param {int} scrollTop Scroll position
     * @param {int=} scrollLeft Horizontal scroll position
     * @param {int=} offsetTop top of the scroller
     */
    FileTreeViewModel.prototype.setSelectionScrollerInfo = function (scrollTop, scrollLeft, offsetTop) {
        this._selectionViewInfo = this._selectionViewInfo.set("scrollTop", scrollTop);

        if (scrollLeft !== undefined) {
            this._selectionViewInfo = this._selectionViewInfo.set("scrollLeft", scrollLeft);
        }

        if (offsetTop !== undefined) {
            this._selectionViewInfo = this._selectionViewInfo.set("offsetTop", offsetTop);
        }

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

    /**
     * Listen for events. Currently a wrapper around jQuery's event system. See jQuery's .on
     * documentation.
     *
     * Events published by the FileTreeViewModel:
     *
     * * change (EVENT_CHANGE constant in the module): fired any time there's a change that should be reflected in the view.
     */
    FileTreeViewModel.prototype.on = function (event, handler) {
        $(this).on(event, handler);
    };

    /**
     * Stop listening for events. See jQuery's .off documentation.
     */
    FileTreeViewModel.prototype.off = function (event, handler) {
        $(this).off(event, handler);
    };

    // Private API
    exports.EVENT_CHANGE          = EVENT_CHANGE;
    exports._filePathToObjectPath = _filePathToObjectPath;
    exports._isFilePathVisible    = _isFilePathVisible;
    exports._createPlaceholder    = _createPlaceholder;

    // Public API
    exports.isFile            = isFile;
    exports.FileTreeViewModel = FileTreeViewModel;
});


forever-agent
=============

HTTP Agent that keeps socket connections alive between keep-alive requests. Formerly part of mikeal/request, now a standalone module.


it("should save the item when done creating", function () {

keep keep keep keep keep replace keep keep keep keep keep

it("should save the item and open it in working set when done creating", function () {


var common = require('../common');
var assert = common.assert;
var CombinedStream = common.CombinedStream;
var fs = require('fs');

var FILE1 = common.dir.fixture + '/file1.txt';
var FILE2 = common.dir.fixture + '/file2.txt';
var EXPECTED = fs.readFileSync(FILE1) + fs.readFileSync(FILE2);

(function testDelayedStreams() {
  var combinedStream = CombinedStream.create();
  combinedStream.append(function(next) {
    next(fs.createReadStream(FILE1));
  });
  combinedStream.append(function(next) {
    next(fs.createReadStream(FILE2));
  });

  var tmpFile = common.dir.tmp + '/combined.txt';
  var dest = fs.createWriteStream(tmpFile);
  combinedStream.pipe(dest);

  dest.on('end', function() {
    var written = fs.readFileSync(tmpFile, 'utf8');
    assert.strictEqual(written, EXPECTED);
  });
})();