Provides the data source for a project and manages the view model for the FileTreeView.
var _exclusionListRegEx = /\.pyc$|^\.git$|^\.gitmodules$|^\.svn$|^\.DS_Store$|^Icon\r|^Thumbs\.db$|^\.hg$|^CVS$|^\.hgtags$|^\.idea$|^\.c9revisions$|^\.SyncArchive$|^\.SyncID$|^\.SyncIgnore$|\~$/;
var _illegalFilenamesRegEx = /((\b(com[0-9]+|lpt[0-9]+|nul|con|prn|aux)\b)|\.+$|\/+|\\+|\:)/i;
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"
];
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;
}
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.
function _ensureTrailingSlash(fullPath) {
if (_pathIsFile(fullPath)) {
return fullPath + "/";
}
return fullPath;
}
function _getFSObject(path) {
if (!path) {
return path;
} else if (_pathIsFile(path)) {
return FileSystem.getFileForPath(path);
}
return FileSystem.getDirectoryForPath(path);
}
function _getPathFromFSObject(fsobj) {
if (fsobj && fsobj.fullPath) {
return fsobj.fullPath;
}
return fsobj;
}
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
}
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.
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;
}
exports._getWelcomeProjectPath = _getWelcomeProjectPath;
exports._addWelcomeProjectPath = _addWelcomeProjectPath;
exports._isWelcomeProjectPath = _isWelcomeProjectPath;
exports._ensureTrailingSlash = _ensureTrailingSlash;
exports._shouldShowName = _shouldShowName;
exports._invalidChars = "? * | : / < > \\ | \" ..";
exports.shouldShow = shouldShow;
exports.defaultIgnoreGlobs = defaultIgnoreGlobs;
exports.isValidFilename = isValidFilename;
exports.isValidPath = isValidPath;
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;
});
function _pathIsFile(path) {
return _.last(path) !== "/";
}
Rename a file/folder. This will update the project tree data structures and send notifications about the rename.
function _renameItem(oldPath, newPath, newName, isFolder) {
var result = new $.Deferred();
if (oldPath === newPath) {
result.resolve();
} else if (!isValidFilename(newName)) {
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();
}
function _shouldShowName(name) {
return !_exclusionListRegEx.test(name);
}
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.
function doCreate(path, isFolder) {
var d = new $.Deferred();
var filename = FileUtils.getBaseName(path);
// Check if filename
if (!isValidFilename(filename)){
return d.reject(ERROR_INVALID_FILENAME).promise();
}
// Check if fullpath with filename is valid
// This check is used to circumvent directory jumps (Like ../..)
if (!isValidPath(path)) {
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();
}
Returns true if this matches valid filename specifications. See http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
TODO: This likely belongs in FileUtils.
function isValidFilename(filename) {
// Fix issue adobe#13099
// See https://github.com/adobe/brackets/issues/13099
return !(
filename.match(_invalidChars)|| filename.match(_illegalFilenamesRegEx)
);
}
Returns true if given path is valid.
function isValidPath(path) {
// Fix issue adobe#13099
// See https://github.com/adobe/brackets/issues/13099
return !(path.match(_invalidChars));
}
Returns false for files and directories that are not commonly useful to display.
function shouldShow(entry) {
return _shouldShowName(entry.name);
}
// Constants used by the ProjectModel
var FILE_RENAMING = 0,
FILE_CREATING = 1,
RENAME_CANCELLED = 2;
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);
ProjectModel.prototype._allFilesCachePromise = null;
ProjectModel.prototype._projectBaseUrl = "";
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);
};
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;
};
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();
};
ProjectModel.prototype._renameItem = function (oldPath, newPath, newName) {
return _renameItem(oldPath, newPath, newName, !_pathIsFile(oldPath));
};
ProjectModel.prototype._resetCache = function _resetCache() {
this._allFilesCachePromise = null;
};
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);
};
Closes the directory at path and recursively closes all of its children.
ProjectModel.prototype.closeSubtree = function (path) {
this._viewModel.closeSubtree(this.makeProjectRelativeIfPossible(path));
};
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.
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
});
});
};
Returns an Array of all files for this project, optionally including files additional files provided. Files are filtered out by shouldShow().
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();
};
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).
ProjectModel.prototype.getBaseUrl = function getBaseUrl() {
return this._projectBaseUrl;
};
Gets the currently selected context.
ProjectModel.prototype.getContext = function () {
return _getFSObject(this._selections.context);
};
Returns a valid directory within the project, either the path (or Directory object) provided or the project root.
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;
};
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.
ProjectModel.prototype.getOpenNodes = function () {
return this._viewModel.getOpenNodes(this.projectRoot.fullPath);
};
Gets the currently selected file or directory.
ProjectModel.prototype.getSelected = function () {
return _getFSObject(this._selections.selected);
};
Handles filesystem change events and prepares the update for the view model.
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);
};
Returns true if absPath lies within the project, false otherwise. Does not support paths containing ".."
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 ".."
ProjectModel.prototype.makeProjectRelativeIfPossible = function makeProjectRelativeIfPossible(absPath) {
if (absPath && this.isWithinProject(absPath)) {
return absPath.slice(this.projectRoot.fullPath.length);
}
return absPath;
};
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),
newPath = renameInfo.newPath,
newName = FileUtils.getBaseName(newPath),
viewModel = this._viewModel,
self = this;
if (renameInfo.type !== FILE_CREATING && oldPath === newPath) {
this.cancelRename();
return;
}
if (isFolder && _.last(newPath) !== "/") {
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, self.makeProjectRelativeIfPossible(newPath));
if (self._selections.selected && self._selections.selected.indexOf(oldPath) === 0) {
self._selections.selected = newPath + self._selections.selected.slice(oldPath.length);
self.setCurrentFile(newPath);
}
}
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);
});
}
};
Clears caches and refreshes the contents of the tree.
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();
};
Reopens a set of nodes in the tree by full path.
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
);
});
}
};
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);
}
};
Adds the file at the given path to the Working Set and selects it there.
ProjectModel.prototype.selectInWorkingSet = function (path) {
this.performRename();
this.trigger(EVENT_SHOULD_SELECT, {
path: path,
add: true
});
};
Sets the encoded Base URL of the currently loaded project.
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;
};
Sets the context (for context menu operations) to the given path. This is independent from the open/selected file.
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));
};
Keeps track of which file is currently being edited.
ProjectModel.prototype.setCurrentFile = function (curFile) {
this._currentPath = _getPathFromFSObject(curFile);
};
Opens or closes the given directory in the file tree.
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();
};
Sets whether the file tree is focused or not.
ProjectModel.prototype.setFocused = function (focused) {
this._focused = focused;
if (!focused) {
this.setSelected(null);
}
};
Sets the project root (effectively resetting this ProjectModel).
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();
};
Sets the new value for the rename operation that is in progress (started previously with a call
to startRename
).
ProjectModel.prototype.setRenameValue = function (newPath) {
if (!this._selections.rename) {
return;
}
this._selections.rename.newPath = newPath;
};
Tracks the scroller position.
ProjectModel.prototype.setScrollerInfo = function (scrollWidth, scrollTop, scrollLeft, offsetTop) {
this._viewModel.setSelectionScrollerInfo(scrollWidth, scrollTop, scrollLeft, offsetTop);
};
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.
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);
}
};
Sets the width of the selection bar.
ProjectModel.prototype.setSelectionWidth = function (width) {
this._viewModel.setSelectionWidth(width);
};
Sets the sortDirectoriesFirst
option for the file tree view.
ProjectModel.prototype.setSortDirectoriesFirst = function (sortDirectoriesFirst) {
this._viewModel.setSortDirectoriesFirst(sortDirectoriesFirst);
};
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.
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();
};
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.
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();
};
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.
ProjectModel.prototype.startRename = function (path, isMoved) {
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 d.resolve().promise();
}
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 (!isMoved) {
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,
newPath: path
};
return d.promise();
};
Toggle the open state of subdirectories.
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();
};