Modules (181)

ProjectModel

Description

Provides the data source for a project and manages the view model for the FileTreeView.

Dependencies

Variables

Private

_exclusionListRegEx

Type
RegExp
Private
    var _exclusionListRegEx = /\.pyc$|^\.git$|^\.gitmodules$|^\.svn$|^\.DS_Store$|^Icon\r|^Thumbs\.db$|^\.hg$|^CVS$|^\.hgtags$|^\.idea$|^\.c9revisions$|^\.SyncArchive$|^\.SyncID$|^\.SyncIgnore$|\~$/;
Private

_illegalFilenamesRegEx

Private

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

_invalidChars

Private
    var _invalidChars;
Public API

defaultIgnoreGlobs

Glob definition of files and folders that should be excluded directly inside node domain watching with chokidar

    var defaultIgnoreGlobs = [
        "**/(.pyc|.git|.gitmodules|.svn|.DS_Store|Thumbs.db|.hg|CVS|.hgtags|.idea|.c9revisions|.SyncArchive|.SyncID|.SyncIgnore)",
        "**/bower_components",
        "**/node_modules"
    ];

Functions

Private Public API

_addWelcomeProjectPath

path string
Path to possibly add
currentProjects optional Array.<string>
Array of current welcome projects
Returns: 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;
    }
Private Public API

_ensureTrailingSlash

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.

fullPath non-nullable string
Path that may or may not end in "/"
Returns: !string
Path that ends in "/"
    function _ensureTrailingSlash(fullPath) {
        if (_pathIsFile(fullPath)) {
            return fullPath + "/";
        }
        return fullPath;
    }
Private

_getFSObject

path string
Path to retrieve
    function _getFSObject(path) {
        if (!path) {
            return path;
        } else if (_pathIsFile(path)) {
            return FileSystem.getFileForPath(path);
        }
        return FileSystem.getDirectoryForPath(path);
    }
Private

_getPathFromFSObject

fsobj FileSystemEntry
Object from which the path should be extracted
    function _getPathFromFSObject(fsobj) {
        if (fsobj && fsobj.fullPath) {
            return fsobj.fullPath;
        }
        return fsobj;
    }
Private Public API

_getWelcomeProjectPath

sampleUrl string
URL for getting started project
initialPath string
Path to Brackets directory (see {@link FileUtils::#getNativeBracketsDirectoryPath})
Returns: !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 Public API

_isWelcomeProjectPath

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.

path string
Path to check to see if it's a welcome project
welcomeProjectPath string
Current welcome project path
welcomeProjects optional Array.<string>
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.defaultIgnoreGlobs      = defaultIgnoreGlobs;
    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.ERROR_NOT_IN_PROJECT    = ERROR_NOT_IN_PROJECT;
    exports.FILE_RENAMING           = FILE_RENAMING;
    exports.FILE_CREATING           = FILE_CREATING;
    exports.RENAME_CANCELLED        = RENAME_CANCELLED;
    exports.doCreate                = doCreate;
    exports.ProjectModel            = ProjectModel;
});
Private

_pathIsFile

path string
Path to test.
    function _pathIsFile(path) {
        return _.last(path) !== "/";
    }
Private

_renameItem

Rename a file/folder. This will update the project tree data structures and send notifications about the rename.

oldPath string
Old name of the item with the path
newPath string
New name of the item with the path
newName string
New name of the item
isFolder boolean
True if item is a folder; False if it is a file.
Returns: $.Promise
A promise object that will be resolved or rejected when the rename is finished.
    function _renameItem(oldPath, newPath, newName, isFolder) {
        var result = new $.Deferred();

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

        return result.promise();
    }
Private Public API

_shouldShowName

    function _shouldShowName(name) {
        return !_exclusionListRegEx.test(name);
    }
Public API

doCreate

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.

path string
path to create
isFolder boolean
true if the new entry is a folder
Returns: $.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();
    }
Public API

isValidFilename

Returns true if this matches valid filename specifications.

TODO: This likely belongs in FileUtils.

filename string
to check
invalidChars string
List of characters that are disallowed
Returns: 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 !(
            new RegExp("[" + invalidChars + "]+").test(filename) ||
            _illegalFilenamesRegEx.test(filename)
        );
    }
Public API

shouldShow

Returns false for files and directories that are not commonly useful to display.

entry non-nullable FileSystemEntry
File or directory to filter
Returns: 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;

Classes

Constructor

ProjectModel

    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 = {};
    }
    EventDispatcher.makeEventDispatcher(ProjectModel.prototype);

Properties

Private

_allFilesCachePromise

Type
?$.Promise.<Array<File>>
Private
    ProjectModel.prototype._allFilesCachePromise = null;
Private

_currentPath

Type
string
Private
    ProjectModel.prototype._currentPath = null;
Private

_focused

Type
boolean
Private
    ProjectModel.prototype._focused = true;
Private

_projectBaseUrl

Type
string
See
{@link ProjectModel#getBaseUrl}, {@link ProjectModel#setBaseUrl}
Private
    ProjectModel.prototype._projectBaseUrl = "";
Private

_selections

Type
{selected: ?string, context: ?string, previousContext: ?string, rename: ?Object}
Private
    ProjectModel.prototype._selections = null;
Private

_viewModel

Type
FileTreeViewModel
Private
    ProjectModel.prototype._viewModel = null;

projectRoot

Type
Directory
    ProjectModel.prototype.projectRoot = null;

Methods

Private

_cancelCreating

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);
    };
Private

_getAllFilesCache

true boolean
to sort files by their paths
Returns: $.Promise.<Array.<File>>
    ProjectModel.prototype._getAllFilesCache = function _getAllFilesCache(sort) {
        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();

            var projectIndexTimer = PerfUtils.markStart("Creating project files cache: " +
                                                        this.projectRoot.fullPath),
                options = {
                    sortList : sort
                };

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

        return this._allFilesCachePromise;
    };
Private

_getDirectoryContents

path string
path to retrieve
Returns: $.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();
    };
Private

_renameItem

oldPath string
full path to the current location of file or directory (should include trailing slash for directory)
newPath string
full path to the new location of the file or directory
newName string
new name for the file or directory
    ProjectModel.prototype._renameItem = function (oldPath, newPath, newName) {
        return _renameItem(oldPath, newPath, newName, !_pathIsFile(oldPath));
    };
Private

_resetCache

    ProjectModel.prototype._resetCache = function _resetCache() {
        this._allFilesCachePromise = null;
    };

cancelRename

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);
    };

closeSubtree

Closes the directory at path and recursively closes all of its children.

path string
Path of subtree to close
    ProjectModel.prototype.closeSubtree = function (path) {
        this._viewModel.closeSubtree(this.makeProjectRelativeIfPossible(path));
    };

createAtPath

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.

path string
full path to file or folder to create
Returns: $.Promise
resolved when creation is complete
    ProjectModel.prototype.createAtPath = function (path) {
        var isFolder  = !_pathIsFile(path),
            name      = FileUtils.getBaseName(path),
            self      = this;

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

getAllFiles

Returns an Array of all files for this project, optionally including files additional files provided. Files are filtered out by shouldShow().

filter optional function (File, number):boolean
Optional function to filter the file list (does not filter directory traversal). API matches Array.filter().
additionalFiles optional Array.<File>
Additional files to include (for example, the WorkingSet) Only adds files that are *not* under the project root or untitled documents.
true boolean
to sort files by their paths
Returns: $.Promise
Promise that is resolved with an Array of File objects.
    ProjectModel.prototype.getAllFiles = function getAllFiles(filter, additionalFiles, sort) {
        // 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(sort).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.error("Unhandled exception in getAllFiles handler: " + e, e.stack);
            }
        }).fail(function (err) {
            try {
                filteredFilesDeferred.reject(err);
            } catch (e) {
                console.error("Unhandled exception in getAllFiles handler: " + e, e.stack);
            }
        });

        return filteredFilesDeferred.promise();
    };

getBaseUrl

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).

Returns: String
    ProjectModel.prototype.getBaseUrl = function getBaseUrl() {
        return this._projectBaseUrl;
    };

getContext

Gets the currently selected context.

Returns: FileSystemEntry
filesystem object for the context file or directory
    ProjectModel.prototype.getContext = function () {
        return _getFSObject(this._selections.context);
    };

getDirectoryInProject

Returns a valid directory within the project, either the path (or Directory object) provided or the project root.

path string,Directory
Directory path to verify against the project
Returns: string
A directory path within the project.
    ProjectModel.prototype.getDirectoryInProject = function (path) {
        if (path && typeof path === "string") {
            if (_.last(path) !== "/") {
                path += "/";
            }
        } else if (path && path.isDirectory) {
            path = path.fullPath;
        } else {
            path = null;
        }

        if (!path || (typeof path !== "string") || !this.isWithinProject(path)) {
            path = this.projectRoot.fullPath;
        }
        return path;
    };

getOpenNodes

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.

Returns: 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);
    };

getSelected

Gets the currently selected file or directory.

Returns: FileSystemEntry
the filesystem object for the currently selected file
    ProjectModel.prototype.getSelected = function () {
        return _getFSObject(this._selections.selected);
    };

handleFSEvent

Handles filesystem change events and prepares the update for the view model.

entry nullable (File, Directory)
File or Directory changed
added optional Array.<FileSystemEntry>
If entry is a Directory, contains zero or more added children
removed optional Array.<FileSystemEntry>
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
            // needs to be updated.
            if (!added && !removed) {
                entry.getContents(function (err, contents) {
                    if (err) {
                        console.error("Unexpected error refreshing file tree for directory " + entry.fullPath + ": " + err, err.stack);
                        return;
                    }
                    self._viewModel.setDirectoryContents(self.makeProjectRelativeIfPossible(entry.fullPath), contents);
                });

                // Exit early because we can't update the viewModel until we get the directory contents.
                return;
            }
        }

        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);
    };

isWithinProject

Returns true if absPath lies within the project, false otherwise. Does not support paths containing ".."

absPathOrEntry string,FileSystemEntry
Returns: boolean
    ProjectModel.prototype.isWithinProject = function isWithinProject(absPathOrEntry) {
        var absPath = absPathOrEntry.fullPath || absPathOrEntry;
        return (this.projectRoot && absPath.indexOf(this.projectRoot.fullPath) === 0);
    };

makeProjectRelativeIfPossible

If absPath lies within the project, returns a project-relative path. Else returns absPath unmodified. Does not support paths containing ".."

absPath non-nullable string
Returns: !string
    ProjectModel.prototype.makeProjectRelativeIfPossible = function makeProjectRelativeIfPossible(absPath) {
        if (absPath && this.isWithinProject(absPath)) {
            return absPath.slice(this.projectRoot.fullPath.length);
        }
        return absPath;
    };

performRename

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);

        function finalizeRename() {
            viewModel.renameItem(oldProjectPath, newName);
            if (self._selections.selected && self._selections.selected.indexOf(oldPath) === 0) {
                self._selections.selected = newPath + self._selections.selected.slice(oldPath.length);
            }
        }

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

refresh

Clears caches and refreshes the contents of the tree.

Returns: $.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();
    };

reopenNodes

Reopens a set of nodes in the tree by full path.

nodesByDepth Array.<Array.<string>>
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.
Returns: $.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
                );
            });
        }
    };

restoreContext

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);
        }
    };

selectInWorkingSet

Adds the file at the given path to the Working Set and selects it there.

path string
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
        });
    };

setBaseUrl

Sets the encoded Base URL of the currently loaded project.

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

setContext

Sets the context (for context menu operations) to the given path. This is independent from the open/selected file.

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

        path = _getPathFromFSObject(path);

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

setCurrentFile

Keeps track of which file is currently being edited.

curFile File,string
Currently edited file.
    ProjectModel.prototype.setCurrentFile = function (curFile) {
        this._currentPath = _getPathFromFSObject(curFile);
    };

setDirectoryOpen

Opens or closes the given directory in the file tree.

path string
Path to open
open boolean
`true` to open the path
Returns: $.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();
    };

setFocused

Sets whether the file tree is focused or not.

focused boolean
True if the file tree has the focus.
    ProjectModel.prototype.setFocused = function (focused) {
        this._focused = focused;
        if (!focused) {
            this.setSelected(null);
        }
    };

setProjectRoot

Sets the project root (effectively resetting this ProjectModel).

projectRoot Directory
new project root
Returns: $.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();
    };

setRenameValue

Sets the new value for the rename operation that is in progress (started previously with a call to startRename).

name string
new name for the file or directory being renamed
    ProjectModel.prototype.setRenameValue = function (name) {
        if (!this._selections.rename) {
            return;
        }
        this._selections.rename.newName = name;
    };

setScrollerInfo

Tracks the scroller position.

scrollWidth int
Width of the tree container
scrollTop int
Top of scroll position
scrollLeft int
Left of scroll position
offsetTop int
Top of scroller element
    ProjectModel.prototype.setScrollerInfo = function (scrollWidth, scrollTop, scrollLeft, offsetTop) {
        this._viewModel.setSelectionScrollerInfo(scrollWidth, scrollTop, scrollLeft, offsetTop);
    };

setSelected

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.

path string
full path to the file being selected
doNotOpen boolean
`true` if the file should not be opened.
    ProjectModel.prototype.setSelected = function (path, doNotOpen) {
        path = _getPathFromFSObject(path);

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

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

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

        this.performRename();

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

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

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

            this.trigger(EVENT_SHOULD_FOCUS);
        }
    };

setSelectionWidth

Sets the width of the selection bar.

width int
New width
    ProjectModel.prototype.setSelectionWidth = function (width) {
        this._viewModel.setSelectionWidth(width);
    };

setSortDirectoriesFirst

Sets the sortDirectoriesFirst option for the file tree view.

True boolean
if directories should appear first
    ProjectModel.prototype.setSortDirectoriesFirst = function (sortDirectoriesFirst) {
        this._viewModel.setSortDirectoriesFirst(sortDirectoriesFirst);
    };

showInTree

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.

path string,File,Directory
full path to the file or directory
Returns: $.Promise
promise resolved when the path is shown
    ProjectModel.prototype.showInTree = function (path) {
        var d = new $.Deferred();
        path = _getPathFromFSObject(path);

        if (!this.isWithinProject(path)) {
            return d.resolve().promise();
        }

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

startCreating

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.

basedir string
directory that should contain the new entry
newName string
initial name for the new entry (the user can rename it)
isFolder boolean
`true` if the entry being created is a folder
Returns: $.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();
    };

startRename

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.

path optional string
optional path to start renaming
Returns: $.Promise
resolved when the operation is complete.
    ProjectModel.prototype.startRename = function (path) {
        var d = new $.Deferred();
        path = _getPathFromFSObject(path);
        if (!path) {
            path = this._selections.context;
            if (!path) {
                return d.resolve().promise();
            }
        }

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

        if (!this.isWithinProject(path)) {
            return d.reject({
                type: ERROR_NOT_IN_PROJECT,
                isFolder: !_pathIsFile(path),
                fullPath: path
            }).promise();
        }

        var projectRelativePath = this.makeProjectRelativeIfPossible(path);

        if (!this._viewModel.isFilePathVisible(projectRelativePath)) {
            this.showInTree(path);
        }

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

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

toggleSubdirectories

Toggle the open state of subdirectories.

path non-nullable string
parent directory
openOrClose boolean
true to open directory, false to close
Returns: $.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();
    };