diff --git a/src/command/Commands.js b/src/command/Commands.js
index b0023e52bab..0a1b0ca195e 100644
--- a/src/command/Commands.js
+++ b/src/command/Commands.js
@@ -34,6 +34,7 @@ define(function (require, exports, module) {
// FILE
exports.FILE_NEW = "file.new";
+ exports.FILE_NEW_FOLDER = "file.newFolder";
exports.FILE_OPEN = "file.open";
exports.FILE_OPEN_FOLDER = "file.openFolder";
exports.FILE_SAVE = "file.save";
@@ -43,6 +44,7 @@ define(function (require, exports, module) {
exports.FILE_CLOSE_WINDOW = "file.close_window"; // string must MATCH string in native code (brackets_extensions)
exports.FILE_ADD_TO_WORKING_SET = "file.addToWorkingSet";
exports.FILE_LIVE_FILE_PREVIEW = "file.liveFilePreview";
+ exports.FILE_RENAME = "file.rename";
exports.FILE_QUIT = "file.quit"; // string must MATCH string in native code (brackets_extensions)
// EDIT
diff --git a/src/command/Menus.js b/src/command/Menus.js
index 04feaf3c9b4..5e59755b340 100644
--- a/src/command/Menus.js
+++ b/src/command/Menus.js
@@ -848,6 +848,7 @@ define(function (require, exports, module) {
var menu;
menu = addMenu(Strings.FILE_MENU, AppMenuBar.FILE_MENU);
menu.addMenuItem(Commands.FILE_NEW, "Ctrl-N");
+ menu.addMenuItem(Commands.FILE_NEW_FOLDER);
menu.addMenuItem(Commands.FILE_OPEN, "Ctrl-O");
menu.addMenuItem(Commands.FILE_OPEN_FOLDER);
menu.addMenuItem(Commands.FILE_CLOSE, "Ctrl-W");
@@ -962,6 +963,8 @@ define(function (require, exports, module) {
*/
var project_cmenu = registerContextMenu(ContextMenuIds.PROJECT_MENU);
project_cmenu.addMenuItem(Commands.FILE_NEW);
+ project_cmenu.addMenuItem(Commands.FILE_NEW_FOLDER);
+ project_cmenu.addMenuItem(Commands.FILE_RENAME);
var editor_cmenu = registerContextMenu(ContextMenuIds.EDITOR_MENU);
editor_cmenu.addMenuItem(Commands.TOGGLE_QUICK_EDIT);
diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js
index 6c59bc049ff..6995654c1f2 100644
--- a/src/document/DocumentCommandHandlers.js
+++ b/src/document/DocumentCommandHandlers.js
@@ -95,9 +95,13 @@ define(function (require, exports, module) {
}
}
- function handleCurrentDocumentChange() {
+ function updateDocumentTitle() {
var newDocument = DocumentManager.getCurrentDocument();
- var perfTimerName = PerfUtils.markStart("DocumentCommandHandlers._onCurrentDocumentChange():\t" + (!newDocument || newDocument.file.fullPath));
+
+ // TODO: This timer is causing a "Recursive tests with the same name are not supporte"
+ // exception. This code should be removed (if not needed), or updated with a unique
+ // timer name (if needed).
+ // var perfTimerName = PerfUtils.markStart("DocumentCommandHandlers._onCurrentDocumentChange():\t" + (!newDocument || newDocument.file.fullPath));
if (newDocument) {
var fullPath = newDocument.file.fullPath;
@@ -113,7 +117,7 @@ define(function (require, exports, module) {
// Update title text & "dirty dot" display
updateTitle();
- PerfUtils.addMeasurement(perfTimerName);
+ // PerfUtils.addMeasurement(perfTimerName);
}
function handleDirtyChange(event, changedDoc) {
@@ -253,10 +257,11 @@ define(function (require, exports, module) {
* @param {string} dir The directory to use
* @param {string} baseFileName The base to start with, "-n" will get appened to make unique
* @param {string} fileExt The file extension
+ * @param {boolean} isFolder True if the suggestion is for a folder name
* @return {$.Promise} a jQuery promise that will be resolved with a unique name starting with
* the given base name
*/
- function _getUntitledFileSuggestion(dir, baseFileName, fileExt) {
+ function _getUntitledFileSuggestion(dir, baseFileName, fileExt, isFolder) {
var result = new $.Deferred();
var suggestedName = baseFileName + fileExt;
var dirEntry = new NativeFileSystem.DirectoryEntry(dir);
@@ -269,18 +274,30 @@ define(function (require, exports, module) {
}
//check this name
- dirEntry.getFile(
- suggestedName,
- {},
- function successCallback(entry) {
- //file exists, notify to the next progress
- result.notify(baseFileName + "-" + nextIndexToUse + fileExt, nextIndexToUse + 1);
- },
- function errorCallback(error) {
- //most likely error is FNF, user is better equiped to handle the rest
- result.resolve(suggestedName);
- }
- );
+ var successCallback = function (entry) {
+ //file exists, notify to the next progress
+ result.notify(baseFileName + "-" + nextIndexToUse + fileExt, nextIndexToUse + 1);
+ };
+ var errorCallback = function (error) {
+ //most likely error is FNF, user is better equiped to handle the rest
+ result.resolve(suggestedName);
+ };
+
+ if (isFolder) {
+ dirEntry.getDirectory(
+ suggestedName,
+ {},
+ successCallback,
+ errorCallback
+ );
+ } else {
+ dirEntry.getFile(
+ suggestedName,
+ {},
+ successCallback,
+ errorCallback
+ );
+ }
});
//kick it off
@@ -297,9 +314,11 @@ define(function (require, exports, module) {
* file creation call is outstanding
*/
var fileNewInProgress = false;
-
- function handleFileNewInProject() {
-
+
+ /**
+ * Bottleneck function for creating new files and folders in the project tree.
+ */
+ function _handleNewItemInProject(isFolder) {
if (fileNewInProgress) {
ProjectManager.forceFinishRename();
return;
@@ -320,21 +339,37 @@ define(function (require, exports, module) {
// Create the new node. The createNewItem function does all the heavy work
// of validating file name, creating the new file and selecting.
- var deferred = _getUntitledFileSuggestion(baseDir, Strings.UNTITLED, ".js");
+ var deferred = _getUntitledFileSuggestion(baseDir, Strings.UNTITLED, isFolder ? "" : ".js", isFolder);
var createWithSuggestedName = function (suggestedName) {
- ProjectManager.createNewItem(baseDir, suggestedName, false)
+ ProjectManager.createNewItem(baseDir, suggestedName, false, isFolder)
.pipe(deferred.resolve, deferred.reject, deferred.notify)
.always(function () { fileNewInProgress = false; })
.done(function (entry) {
- FileViewController.addToWorkingSetAndSelect(entry.fullPath, FileViewController.PROJECT_MANAGER);
+ if (!isFolder) {
+ FileViewController.addToWorkingSetAndSelect(entry.fullPath, FileViewController.PROJECT_MANAGER);
+ }
});
};
deferred.done(createWithSuggestedName);
- deferred.fail(function createWithDefault() { createWithSuggestedName("Untitled.js"); });
+ deferred.fail(function createWithDefault() { createWithSuggestedName(isFolder ? "Untitled" : "Untitled.js"); });
return deferred;
}
+
+ /**
+ * Create a new file in the project tree.
+ */
+ function handleFileNewInProject() {
+ _handleNewItemInProject(false);
+ }
+ /**
+ * Create a new folder in the project tree.
+ */
+ function handleNewFolderInProject() {
+ _handleNewItemInProject(true);
+ }
+
function showSaveFileError(code, path) {
return Dialogs.showModalDialog(
Dialogs.DIALOG_ID_ERROR,
@@ -711,6 +746,10 @@ define(function (require, exports, module) {
);
}
+ function handleFileRename() {
+ ProjectManager.renameSelectedItem();
+ }
+
/** Closes the window, then quits the app */
function handleFileQuit(commandData) {
return _handleWindowGoingAway(
@@ -791,11 +830,13 @@ define(function (require, exports, module) {
// File > New should open a new blank tab, and handleFileNewInProject should
// be called from a "+" button in the project
CommandManager.register(Strings.CMD_FILE_NEW, Commands.FILE_NEW, handleFileNewInProject);
+ CommandManager.register(Strings.CMD_FILE_NEW_FOLDER, Commands.FILE_NEW_FOLDER, handleNewFolderInProject);
CommandManager.register(Strings.CMD_FILE_SAVE, Commands.FILE_SAVE, handleFileSave);
CommandManager.register(Strings.CMD_FILE_SAVE_ALL, Commands.FILE_SAVE_ALL, handleFileSaveAll);
CommandManager.register(Strings.CMD_FILE_CLOSE, Commands.FILE_CLOSE, handleFileClose);
CommandManager.register(Strings.CMD_FILE_CLOSE_ALL, Commands.FILE_CLOSE_ALL, handleFileCloseAll);
+ CommandManager.register(Strings.CMD_FILE_RENAME, Commands.FILE_RENAME, handleFileRename);
CommandManager.register(Strings.CMD_CLOSE_WINDOW, Commands.FILE_CLOSE_WINDOW, handleFileCloseWindow);
CommandManager.register(Strings.CMD_QUIT, Commands.FILE_QUIT, handleFileQuit);
CommandManager.register(Strings.CMD_REFRESH_WINDOW, Commands.DEBUG_REFRESH_WINDOW, handleFileReload);
@@ -805,7 +846,7 @@ define(function (require, exports, module) {
// Listen for changes that require updating the editor titlebar
$(DocumentManager).on("dirtyFlagChange", handleDirtyChange);
- $(DocumentManager).on("currentDocumentChange", handleCurrentDocumentChange);
+ $(DocumentManager).on("currentDocumentChange fileNameChange", updateDocumentTitle);
}
// Define public API
diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js
index 5980a2f8896..7c2abb8cb02 100644
--- a/src/document/DocumentManager.js
+++ b/src/document/DocumentManager.js
@@ -64,6 +64,8 @@
* 2nd arg to the listener is the removed FileEntry.
* - workingSetRemoveList -- When a list of files is to be removed from the working set (e.g. project close).
* The 2nd arg to the listener is the array of removed FileEntry objects.
+ * - fileNameChange -- When the name of a file or folder has changed. The 2nd arg is the old name.
+ * The 3rd arg is the new name.
*
* These are jQuery events, so to listen for them you do something like this:
* $(DocumentManager).on("eventname", handler);
@@ -1061,7 +1063,58 @@ define(function (require, exports, module) {
}
}
-
+ /**
+ * Called after a file or folder name has changed. This function is responsible
+ * for updating underlying model data and notifying all views of the change.
+ *
+ * @param {string} oldName The old name of the file/folder
+ * @param {string} newName The new name of the file/folder
+ * @param {boolean} isFolder True if path is a folder; False if it is a file.
+ */
+ function notifyPathNameChanged(oldName, newName, isFolder) {
+ var i, path;
+
+ // Update currentDocument
+ if (_currentDocument) {
+ FileUtils.updateFileEntryPath(_currentDocument.file, oldName, newName);
+ }
+
+ // Update open documents
+ var keysToDelete = [];
+ for (path in _openDocuments) {
+ if (_openDocuments.hasOwnProperty(path)) {
+ if (path.indexOf(oldName) === 0) {
+ // Copy value to new key
+ var newKey = path.replace(oldName, newName);
+
+ _openDocuments[newKey] = _openDocuments[path];
+ keysToDelete.push(path);
+
+ // Update document file
+ FileUtils.updateFileEntryPath(_openDocuments[newKey].file, oldName, newName);
+
+ if (!isFolder) {
+ // If the path name is a file, there can only be one matched entry in the open document
+ // list, which we just updated. Break out of the for .. in loop.
+ break;
+ }
+ }
+ }
+ }
+ // Delete the old keys
+ for (i = 0; i < keysToDelete.length; i++) {
+ delete _openDocuments[keysToDelete[i]];
+ }
+
+ // Update working set
+ for (i = 0; i < _workingSet.length; i++) {
+ FileUtils.updateFileEntryPath(_workingSet[i], oldName, newName);
+ }
+
+ // Send a "fileNameChanged" event. This will trigger the views to update.
+ $(exports).triggerHandler("fileNameChange", [oldName, newName]);
+ }
+
// Define public API
exports.Document = Document;
exports.getCurrentDocument = getCurrentDocument;
@@ -1080,10 +1133,11 @@ define(function (require, exports, module) {
exports.closeFullEditor = closeFullEditor;
exports.closeAll = closeAll;
exports.notifyFileDeleted = notifyFileDeleted;
+ exports.notifyPathNameChanged = notifyPathNameChanged;
// Setup preferences
_prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID);
- $(exports).bind("currentDocumentChange workingSetAdd workingSetAddList workingSetRemove workingSetRemoveList", _savePreferences);
+ $(exports).bind("currentDocumentChange workingSetAdd workingSetAddList workingSetRemove workingSetRemoveList fileNameChange", _savePreferences);
// Performance measurements
PerfUtils.createPerfMeasurement("DOCUMENT_MANAGER_GET_DOCUMENT_FOR_PATH", "DocumentManager.getDocumentForPath()");
diff --git a/src/file/FileUtils.js b/src/file/FileUtils.js
index 0070ca7d093..9685d2d447e 100644
--- a/src/file/FileUtils.js
+++ b/src/file/FileUtils.js
@@ -261,6 +261,36 @@ define(function (require, exports, module) {
}
return path;
}
+
+ /**
+ * Update a file entry path after a file/folder name change.
+ * @param {FileEntry} entry The FileEntry or DirectoryEntry to update
+ * @param {string} oldName The full path of the old name
+ * @param {string} newName The full path of the new name
+ * @return {boolean} Returns true if the file entry was updated
+ */
+ function updateFileEntryPath(entry, oldName, newName) {
+ if (entry.fullPath.indexOf(oldName) === 0) {
+ var fullPath = entry.fullPath.replace(oldName, newName);
+
+ entry.fullPath = fullPath;
+
+ // TODO: Should this be a method on Entry instead?
+ entry.name = null; // default if extraction fails
+ if (fullPath) {
+ var pathParts = fullPath.split("/");
+
+ // Extract name from the end of the fullPath (account for trailing slash(es))
+ while (!entry.name && pathParts.length) {
+ entry.name = pathParts.pop();
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
// Define public API
exports.LINE_ENDINGS_CRLF = LINE_ENDINGS_CRLF;
@@ -276,4 +306,5 @@ define(function (require, exports, module) {
exports.getNativeBracketsDirectoryPath = getNativeBracketsDirectoryPath;
exports.getNativeModuleDirectoryPath = getNativeModuleDirectoryPath;
exports.canonicalizeFolderPath = canonicalizeFolderPath;
+ exports.updateFileEntryPath = updateFileEntryPath;
});
diff --git a/src/file/NativeFileSystem.js b/src/file/NativeFileSystem.js
index 679c1f5b18e..770c9302bd3 100644
--- a/src/file/NativeFileSystem.js
+++ b/src/file/NativeFileSystem.js
@@ -517,8 +517,88 @@ define(function (require, exports, module) {
};
NativeFileSystem.DirectoryEntry.prototype.getDirectory = function (path, options, successCallback, errorCallback) {
- // TODO (issue #241)
- // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-DirectoryEntry-getDirectory
+ var directoryFullPath = path;
+
+ function isRelativePath(path) {
+ // If the path contains a colons it must be a full path on Windows (colons are
+ // not valid path characters on mac or in URIs)
+ if (path.indexOf(":") !== -1) {
+ return false;
+ }
+
+ // For everyone else, absolute paths start with a "/"
+ return path[0] !== "/";
+ }
+
+ // resolve relative paths relative to the DirectoryEntry
+ if (isRelativePath(path)) {
+ directoryFullPath = this.fullPath + path;
+ }
+
+ var createDirectoryEntry = function () {
+ if (successCallback) {
+ successCallback(new NativeFileSystem.DirectoryEntry(directoryFullPath));
+ }
+ };
+
+ var createDirectoryError = function (err) {
+ if (errorCallback) {
+ errorCallback(NativeFileSystem._nativeToFileError(err));
+ }
+ };
+
+ // Use stat() to check if file exists
+ brackets.fs.stat(directoryFullPath, function (err, stats) {
+ if ((err === brackets.fs.NO_ERROR)) {
+ // NO_ERROR implies the path already exists
+
+ // throw error if the file the path is not a directory
+ if (!stats.isDirectory()) {
+ if (errorCallback) {
+ errorCallback(new NativeFileSystem.FileError(FileError.TYPE_MISMATCH_ERR));
+ }
+
+ return;
+ }
+
+ // throw error if the file exists but create is exclusive
+ if (options.create && options.exclusive) {
+ if (errorCallback) {
+ errorCallback(new NativeFileSystem.FileError(FileError.PATH_EXISTS_ERR));
+ }
+
+ return;
+ }
+
+ // Create a file entry for the existing directory. If create == true,
+ // a file entry is created without error.
+ createDirectoryEntry();
+ } else if (err === brackets.fs.ERR_NOT_FOUND) {
+ // ERR_NOT_FOUND implies we write a new, empty file
+
+ // create the file
+ if (options.create) {
+ // TODO: Pass permissions. The current implementation of fs.makedir() always
+ // creates the directory with the full permissions available to the current user.
+ brackets.fs.makedir(directoryFullPath, 0, function (err) {
+ if (err) {
+ createDirectoryError(err);
+ } else {
+ createDirectoryEntry();
+ }
+ });
+ return;
+ }
+
+ // throw error if file not found and the create == false
+ if (errorCallback) {
+ errorCallback(new NativeFileSystem.FileError(FileError.NOT_FOUND_ERR));
+ }
+ } else {
+ // all other brackets.fs.stat() errors
+ createDirectoryError(err);
+ }
+ });
};
NativeFileSystem.DirectoryEntry.prototype.removeRecursively = function (successCallback, errorCallback) {
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index 9694f6ba3be..b502e3d46fd 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -35,6 +35,7 @@ define({
"NOT_READABLE_ERR" : "The file could not be read.",
"NO_MODIFICATION_ALLOWED_ERR" : "The target directory cannot be modified.",
"NO_MODIFICATION_ALLOWED_ERR_FILE" : "The permissions do not allow you to make modifications.",
+ "FILE_EXISTS_ERR" : "The file already exists.",
// Project error strings
"ERROR_LOADING_PROJECT" : "Error loading project",
@@ -49,6 +50,8 @@ define({
"ERROR_RELOADING_FILE" : "An error occurred when trying to reload the file {0}. {1}",
"ERROR_SAVING_FILE_TITLE" : "Error saving file",
"ERROR_SAVING_FILE" : "An error occurred when trying to save the file {0}. {1}",
+ "ERROR_RENAMING_FILE_TITLE" : "Error renaming file",
+ "ERROR_RENAMING_FILE" : "An error occurred when trying to rename the file {0}. {1}",
"INVALID_FILENAME_TITLE" : "Invalid file name",
"INVALID_FILENAME_MESSAGE" : "Filenames cannot contain the following characters: /?*:;{}<>\\|",
"FILE_ALREADY_EXISTS" : "The file {0} already exists.",
@@ -139,7 +142,8 @@ define({
// File menu commands
"FILE_MENU" : "File",
- "CMD_FILE_NEW" : "New",
+ "CMD_FILE_NEW" : "New File",
+ "CMD_FILE_NEW_FOLDER" : "New Folder",
"CMD_FILE_OPEN" : "Open\u2026",
"CMD_ADD_TO_WORKING_SET" : "Add To Working Set",
"CMD_OPEN_FOLDER" : "Open Folder\u2026",
@@ -148,6 +152,7 @@ define({
"CMD_FILE_SAVE" : "Save",
"CMD_FILE_SAVE_ALL" : "Save All",
"CMD_LIVE_FILE_PREVIEW" : "Live Preview",
+ "CMD_FILE_RENAME" : "Rename",
"CMD_QUIT" : "Quit",
// Edit menu commands
diff --git a/src/project/FileIndexManager.js b/src/project/FileIndexManager.js
index ca40902da2b..0e6e2dd78a5 100644
--- a/src/project/FileIndexManager.js
+++ b/src/project/FileIndexManager.js
@@ -395,7 +395,7 @@ define(function (require, exports, module) {
}
);
- $(ProjectManager).on("projectOpen", function (event, projectRoot) {
+ $(ProjectManager).on("projectOpen projectFilesChange", function (event, projectRoot) {
markDirty();
});
diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js
index cd9849e518a..29172c92029 100644
--- a/src/project/ProjectManager.js
+++ b/src/project/ProjectManager.js
@@ -32,6 +32,8 @@
* This module dispatches these events:
* - beforeProjectClose -- before _projectRoot changes
* - projectOpen -- after _projectRoot changes
+ * - projectFilesChange -- sent if one of the project files has changed--
+ * added, removed, renamed, etc.
*
* These are jQuery events, so to listen for them you do something like this:
* $(ProjectManager).on("eventname", handler);
@@ -134,6 +136,8 @@ define(function (require, exports, module) {
fullPathToIdMap : {} /* mapping of fullPath to tree node id attr */
};
+ var suppressToggleOpen = false;
+
/**
* @private
*/
@@ -318,8 +322,7 @@ define(function (require, exports, module) {
* http://www.jstree.com/documentation/json_data
*/
function _renderTree(treeDataProvider) {
- var result = new $.Deferred(),
- suppressToggleOpen = false;
+ var result = new $.Deferred();
// For #1542, make sure the tree is scrolled to the top before refreshing.
// If we try to do this later (e.g. after the tree has been refreshed), it
@@ -817,18 +820,40 @@ define(function (require, exports, module) {
return result.promise();
}
-
+ /**
+ * @private
+ *
+ * Check a filename for illegal characters. If any are found, show an error
+ * dialog and return false. If no illegal characters are found, return true.
+ */
+ function _checkForValidFilename(filename) {
+ // Validate file name
+ // TODO (issue #270): There are some filenames like COM1, LPT3, etc. that are not valid on Windows.
+ // We may want to add checks for those here.
+ // See http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
+ if (filename.search(/[\/?*:;\{\}<>\\|]+/) !== -1) {
+ Dialogs.showModalDialog(
+ Dialogs.DIALOG_ID_ERROR,
+ Strings.INVALID_FILENAME_TITLE,
+ Strings.INVALID_FILENAME_MESSAGE
+ );
+ return false;
+ }
+ return true;
+ }
+
/**
* Create a new item in the project tree.
*
* @param baseDir {string} Full path of the directory where the item should go
* @param initialName {string} Initial name for the item
* @param skipRename {boolean} If true, don't allow the user to rename the item
+ * @param isFolder {boolean} If true, create a folder instead of a file
* @return {$.Promise} A promise object that will be resolved with the FileEntry
* of the created object, or rejected if the user cancelled or entered an illegal
* filename.
*/
- function createNewItem(baseDir, initialName, skipRename) {
+ function createNewItem(baseDir, initialName, skipRename, isFolder) {
var node = null,
selection = _projectTree.jstree("get_selected"),
selectionEntry = null,
@@ -894,57 +919,80 @@ define(function (require, exports, module) {
if (!escapeKeyPressed) {
// Validate file name
- // TODO (issue #270): There are some filenames like COM1, LPT3, etc. that are not valid on Windows.
- // We may want to add checks for those here.
- // See http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
- if (data.rslt.name.search(/[\/?*:;\{\}<>\\|]+/) !== -1) {
- Dialogs.showModalDialog(
- Dialogs.DIALOG_ID_ERROR,
- Strings.INVALID_FILENAME_TITLE,
- Strings.INVALID_FILENAME_MESSAGE
- );
-
+ if (!_checkForValidFilename(data.rslt.name)) {
errorCleanup();
return;
}
- // Use getFile() to create the new file
- selectionEntry.getFile(
- data.rslt.name,
- {create: true, exclusive: true},
- function (entry) {
- data.rslt.obj.data("entry", entry);
- _projectTree.jstree("select_node", data.rslt.obj, true);
- result.resolve(entry);
- },
- function (error) {
- if ((error.code === FileError.PATH_EXISTS_ERR)
- || (error.code === FileError.TYPE_MISMATCH_ERR)) {
- Dialogs.showModalDialog(
- Dialogs.DIALOG_ID_ERROR,
- Strings.INVALID_FILENAME_TITLE,
- StringUtils.format(Strings.FILE_ALREADY_EXISTS,
- StringUtils.htmlEscape(data.rslt.name))
- );
- } else {
- var errString = error.code === FileError.NO_MODIFICATION_ALLOWED_ERR ?
- Strings.NO_MODIFICATION_ALLOWED_ERR :
- StringUtils.format(String.GENERIC_ERROR, error.code);
-
- var errMsg = StringUtils.format(Strings.ERROR_CREATING_FILE,
- StringUtils.htmlEscape(data.rslt.name),
- errString);
-
- Dialogs.showModalDialog(
- Dialogs.DIALOG_ID_ERROR,
- Strings.ERROR_CREATING_FILE_TITLE,
- errMsg
- );
- }
+ var successCallback = function (entry) {
+ data.rslt.obj.data("entry", entry);
+ if (isFolder) {
+ // If the new item is a folder, remove the leaf and folder related
+ // classes and add "jstree-closed". Selecting the item will open
+ // the folder.
+ data.rslt.obj.removeClass("jstree-leaf jstree-closed jstree-open")
+ .addClass("jstree-closed");
+ }
+
+ // If the new item is a folder, force a re-sort here. Windows sorts folders
+ // and files separately.
+ if (isFolder) {
+ _projectTree.jstree("sort", data.rslt.obj.parent());
+ }
+
+ _projectTree.jstree("select_node", data.rslt.obj, true);
- errorCleanup();
+ // Notify listeners that the project model has changed
+ $(exports).triggerHandler("projectFilesChange");
+
+ result.resolve(entry);
+ };
+
+ var errorCallback = function (error) {
+ if ((error.code === FileError.PATH_EXISTS_ERR)
+ || (error.code === FileError.TYPE_MISMATCH_ERR)) {
+ Dialogs.showModalDialog(
+ Dialogs.DIALOG_ID_ERROR,
+ Strings.INVALID_FILENAME_TITLE,
+ StringUtils.format(Strings.FILE_ALREADY_EXISTS,
+ StringUtils.htmlEscape(data.rslt.name))
+ );
+ } else {
+ var errString = error.code === FileError.NO_MODIFICATION_ALLOWED_ERR ?
+ Strings.NO_MODIFICATION_ALLOWED_ERR :
+ StringUtils.format(String.GENERIC_ERROR, error.code);
+
+ var errMsg = StringUtils.format(Strings.ERROR_CREATING_FILE,
+ StringUtils.htmlEscape(data.rslt.name),
+ errString);
+
+ Dialogs.showModalDialog(
+ Dialogs.DIALOG_ID_ERROR,
+ Strings.ERROR_CREATING_FILE_TITLE,
+ errMsg
+ );
}
- );
+
+ errorCleanup();
+ };
+
+ if (isFolder) {
+ // Use getDirectory() to create the new folder
+ selectionEntry.getDirectory(
+ data.rslt.name,
+ {create: true, exclusive: true},
+ successCallback,
+ errorCallback
+ );
+ } else {
+ // Use getFile() to create the new file
+ selectionEntry.getFile(
+ data.rslt.name,
+ {create: true, exclusive: true},
+ successCallback,
+ errorCallback
+ );
+ }
} else { //escapeKeyPressed
errorCleanup();
}
@@ -978,6 +1026,130 @@ define(function (require, exports, module) {
return result.promise();
}
+ /**
+ * Rename a file/folder. This will update the project tree data structures
+ * and send notifications about the rename.
+ *
+ * @prarm {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();
+ return result;
+ }
+
+ // TODO: This should call FileEntry.moveTo(), but that isn't implemented
+ // yet. For now, call directly to the low-level fs.rename()
+ brackets.fs.rename(oldName, newName, function (err) {
+ if (!err) {
+ // Update all nodes in the project tree.
+ // All other updating is done by DocumentManager.notifyPathNameChanged() below
+ var nodes = _projectTree.find(".jstree-leaf, .jstree-open, .jstree-closed"),
+ i;
+
+ for (i = 0; i < nodes.length; i++) {
+ var node = $(nodes[i]);
+ FileUtils.updateFileEntryPath(node.data("entry"), oldName, newName);
+ }
+
+ // Notify that one of the project files has changed
+ $(exports).triggerHandler("projectFilesChange");
+
+ // Tell the document manager about the name change. This will update
+ // all of the model information and send notification to all views
+ DocumentManager.notifyPathNameChanged(oldName, newName, isFolder);
+
+ // Finally, re-open the selected document
+ if (DocumentManager.getCurrentDocument()) {
+ FileViewController.openAndSelectDocument(
+ DocumentManager.getCurrentDocument().file.fullPath,
+ FileViewController.getFileSelectionFocus()
+ );
+ }
+
+ _redraw(true);
+
+ result.resolve();
+ } else {
+ // Show and error alert
+ Dialogs.showModalDialog(
+ Dialogs.DIALOG_ID_ERROR,
+ Strings.ERROR_RENAMING_FILE_TITLE,
+ StringUtils.format(
+ Strings.ERROR_RENAMING_FILE,
+ StringUtils.htmlEscape(newName),
+ err === brackets.fs.ERR_FILE_EXISTS ?
+ Strings.FILE_EXISTS_ERR :
+ FileUtils.getFileErrorString(err)
+ )
+ );
+
+ result.reject(err);
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Rename the selected item in the project tree
+ */
+ function renameSelectedItem() {
+ var selected = _projectTree.jstree("get_selected"),
+ isFolder = selected.hasClass("jstree-open") || selected.hasClass("jstree-closed");
+
+ if (selected) {
+ _projectTree.on("rename.jstree", function (event, data) {
+ $(event.target).off("rename.jstree");
+
+ // Make sure the file was actually renamed
+ if (data.rslt.old_name === data.rslt.new_name) {
+ return;
+ }
+
+ var _resetOldFilename = function () {
+ _projectTree.jstree("set_text", selected, data.rslt.old_name);
+ _projectTree.jstree("refresh", -1);
+ };
+
+ if (!_checkForValidFilename(data.rslt.new_name)) {
+ // Invalid filename. Reset the old name and bail.
+ _resetOldFilename();
+ return;
+ }
+
+ var oldName = selected.data("entry").fullPath;
+ var newName = oldName.replace(data.rslt.old_name, data.rslt.new_name);
+
+ renameItem(oldName, newName, isFolder)
+ .done(function () {
+
+ // If a folder was renamed, re-select it here, since openAndSelectDocument()
+ // changed the selection.
+ if (isFolder) {
+ var oldSuppressToggleOpen = suppressToggleOpen;
+
+ // Supress the open/close toggle
+ suppressToggleOpen = true;
+ _projectTree.jstree("select_node", selected, true);
+ suppressToggleOpen = oldSuppressToggleOpen;
+ }
+ })
+ .fail(function (err) {
+ // Error during rename. Reset to the old name and alert the user.
+ _resetOldFilename();
+ });
+ });
+ _projectTree.jstree("rename");
+ }
+ }
+
/**
* Forces createNewItem() to complete by removing focus from the rename field which causes
* the new file to be written to disk
@@ -1020,5 +1192,6 @@ define(function (require, exports, module) {
exports.isWelcomeProjectPath = isWelcomeProjectPath;
exports.updateWelcomeProjectPath = updateWelcomeProjectPath;
exports.createNewItem = createNewItem;
+ exports.renameSelectedItem = renameSelectedItem;
exports.forceFinishRename = forceFinishRename;
});
diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js
index 51b77727e27..f34a8b5ee80 100644
--- a/src/project/WorkingSetView.js
+++ b/src/project/WorkingSetView.js
@@ -333,6 +333,18 @@ define(function (require, exports, module) {
}
+ /**
+ * @private
+ * @param {string} oldName
+ * @param {string} newName
+ */
+ function _handleFileNameChanged(oldName, newName) {
+ // Rebuild the working set if any file or folder name changed.
+ // We could be smarter about this and only update the
+ // nodes that changed, if needed...
+ _rebuildWorkingSet();
+ }
+
function create(element) {
// Init DOM element
$openFilesContainer = element;
@@ -359,6 +371,10 @@ define(function (require, exports, module) {
_handleDirtyFlagChanged(doc);
});
+ $(DocumentManager).on("fileNameChange", function (event, oldName, newName) {
+ _handleFileNameChanged(oldName, newName);
+ });
+
$(FileViewController).on("documentSelectionFocusChange fileViewFocusChange", _handleDocumentSelectionChange);
// Show scroller shadows when open-files-container scrolls
diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js
index 8766de1cb76..03960a9bc86 100644
--- a/src/search/FindInFiles.js
+++ b/src/search/FindInFiles.js
@@ -52,6 +52,7 @@ define(function (require, exports, module) {
FileIndexManager = require("project/FileIndexManager"),
KeyEvent = require("utils/KeyEvent");
+ var searchResults = [];
var FIND_IN_FILES_MAX = 100;
@@ -287,11 +288,12 @@ define(function (require, exports, module) {
function doFindInFiles() {
var dialog = new FindInFilesDialog();
- var searchResults = [];
// Default to searching for the current selection
var currentEditor = EditorManager.getFocusedEditor();
var initialString = currentEditor && currentEditor.getSelectedText();
+
+ searchResults = [];
dialog.showDialog(initialString)
.done(function (query) {
@@ -333,5 +335,16 @@ define(function (require, exports, module) {
});
}
+ function _fileNameChangeHandler(event, oldName, newName) {
+ if ($("#search-results").is(":visible")) {
+ // Update the search results
+ searchResults.forEach(function (item) {
+ item.fullPath = item.fullPath.replace(oldName, newName);
+ });
+ _showSearchResults(searchResults);
+ }
+ }
+
+ $(DocumentManager).on("fileNameChange", _fileNameChangeHandler);
CommandManager.register(Strings.CMD_FIND_IN_FILES, Commands.EDIT_FIND_IN_FILES, doFindInFiles);
});
\ No newline at end of file
diff --git a/src/styles/brackets.less b/src/styles/brackets.less
index 66c05290ba1..2e1becfc5b2 100644
--- a/src/styles/brackets.less
+++ b/src/styles/brackets.less
@@ -464,6 +464,7 @@ ins.jstree-icon {
}
// TODO (Issue #1615): Remove this hack when we get a new CEF
+/*
@media all and (-webkit-min-device-pixel-ratio:2),(min-device-pixel-ratio:2) {
.inline-widget {
.shadow.top {
@@ -474,6 +475,7 @@ ins.jstree-icon {
}
}
}
+*/
/* CSSInlineEditor rule list */
.related-container {
diff --git a/test/spec/LowLevelFileIO-test-files/rename_me/hello.txt b/test/spec/LowLevelFileIO-test-files/rename_me/hello.txt
new file mode 100644
index 00000000000..349db2bfe13
--- /dev/null
+++ b/test/spec/LowLevelFileIO-test-files/rename_me/hello.txt
@@ -0,0 +1 @@
+Hello, World.
diff --git a/test/spec/LowLevelFileIO-test.js b/test/spec/LowLevelFileIO-test.js
index 3510b2fceaa..34f3f45032a 100644
--- a/test/spec/LowLevelFileIO-test.js
+++ b/test/spec/LowLevelFileIO-test.js
@@ -504,5 +504,176 @@ define(function (require, exports, module) {
});
}); // describe("unlink")
+
+ describe("mkdir", function () {
+ it("should make a new directory", function () {
+ // TODO: Write this test once we have a function to delete the directory
+ });
+ });
+
+ describe("rename", function () {
+ var error, complete;
+
+ it("should rename a file", function () {
+ var oldName = baseDir + "file_one.txt",
+ newName = baseDir + "file_one_renamed.txt";
+
+ complete = false;
+
+ brackets.fs.rename(oldName, newName, function (err) {
+ error = err;
+ complete = true;
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.NO_ERROR);
+ });
+
+ // Verify new file is found and old one is missing
+ runs(function () {
+ complete = false;
+ brackets.fs.stat(oldName, function (err, stat) {
+ complete = true;
+ error = err;
+ });
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.ERR_NOT_FOUND);
+ });
+
+ runs(function () {
+ complete = false;
+ brackets.fs.stat(newName, function (err, stat) {
+ complete = true;
+ error = err;
+ });
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.NO_ERROR);
+ });
+
+ // Rename the file back to the old name
+ runs(function () {
+ complete = false;
+ brackets.fs.rename(newName, oldName, function (err) {
+ complete = true;
+ error = err;
+ });
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.NO_ERROR);
+ });
+
+ });
+ it("should rename a folder", function () {
+ var oldName = baseDir + "rename_me",
+ newName = baseDir + "renamed_folder";
+
+ complete = false;
+
+ brackets.fs.rename(oldName, newName, function (err) {
+ error = err;
+ complete = true;
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.NO_ERROR);
+ });
+
+ // Verify new folder is found and old one is missing
+ runs(function () {
+ complete = false;
+ brackets.fs.stat(oldName, function (err, stat) {
+ complete = true;
+ error = err;
+ });
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.ERR_NOT_FOUND);
+ });
+
+ runs(function () {
+ complete = false;
+ brackets.fs.stat(newName, function (err, stat) {
+ complete = true;
+ error = err;
+ });
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.NO_ERROR);
+ });
+
+ // Rename the folder back to the old name
+ runs(function () {
+ complete = false;
+ brackets.fs.rename(newName, oldName, function (err) {
+ complete = true;
+ error = err;
+ });
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.NO_ERROR);
+ });
+ });
+ it("should return an error if the new name already exists", function () {
+ var oldName = baseDir + "file_one.txt",
+ newName = baseDir + "file_two.txt";
+
+ complete = false;
+
+ brackets.fs.rename(oldName, newName, function (err) {
+ error = err;
+ complete = true;
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.ERR_FILE_EXISTS);
+ });
+ });
+ it("should return an error if the parent folder is read only (Mac only)", function () {
+ if (brackets.platform === "mac") {
+ var oldName = baseDir + "cant_write_here/readme.txt",
+ newName = baseDir + "cant_write_here/readme_renamed.txt";
+
+ complete = false;
+
+ brackets.fs.rename(oldName, newName, function (err) {
+ error = err;
+ complete = true;
+ });
+
+ waitsFor(function () { return complete; }, 1000);
+
+ runs(function () {
+ expect(error).toBe(brackets.fs.ERR_CANT_WRITE);
+ });
+ }
+ });
+ // TODO: More testing of error cases?
+ });
});
});