From a582f8b95d2f959114273b1da20e57b42a214053 Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Mon, 27 Aug 2012 08:46:45 -0700 Subject: [PATCH 1/9] Add 'New Folder' menu item and implementation. --- src/command/Commands.js | 1 + src/command/Menus.js | 2 + src/document/DocumentCommandHandlers.js | 23 ++++-- src/file/NativeFileSystem.js | 82 ++++++++++++++++++++- src/nls/root/strings.js | 1 + src/project/ProjectManager.js | 96 +++++++++++++++---------- 6 files changed, 160 insertions(+), 45 deletions(-) diff --git a/src/command/Commands.js b/src/command/Commands.js index b0023e52bab..a94d7c18b98 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"; diff --git a/src/command/Menus.js b/src/command/Menus.js index 04feaf3c9b4..6b4a595d10c 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,7 @@ 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); 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..17ea8bc9829 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -299,7 +299,15 @@ define(function (require, exports, module) { var fileNewInProgress = false; function handleFileNewInProject() { - + handleNewItemInProject(false); + } + + function handleNewFolderInProject() { + handleNewItemInProject(true); + } + + function handleNewItemInProject(isFolder) + { if (fileNewInProgress) { ProjectManager.forceFinishRename(); return; @@ -320,21 +328,23 @@ 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"); 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; } - + function showSaveFileError(code, path) { return Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, @@ -791,6 +801,7 @@ 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); diff --git a/src/file/NativeFileSystem.js b/src/file/NativeFileSystem.js index 679c1f5b18e..2dda3124e52 100644 --- a/src/file/NativeFileSystem.js +++ b/src/file/NativeFileSystem.js @@ -517,8 +517,86 @@ 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) { + brackets.fs.makedir(directoryFullPath, 0 /* TODO - should be 0777 */, 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..7e0c86e624f 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -140,6 +140,7 @@ define({ // File menu commands "FILE_MENU" : "File", "CMD_FILE_NEW" : "New", + "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", diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index cd9849e518a..c064df039d4 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -824,11 +824,12 @@ define(function (require, exports, module) { * @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, @@ -908,43 +909,64 @@ define(function (require, exports, module) { 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 - ); - } - - errorCleanup(); + 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"); } - ); + _projectTree.jstree("select_node", data.rslt.obj, true); + result.resolve(entry); + }; + + var errorCallback = function (err) { + 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(); } From 36ca3dd835f80be0915180d2c631380690308631 Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Mon, 27 Aug 2012 09:43:31 -0700 Subject: [PATCH 2/9] Add 'New Folder' command. Add 'Rename' Command --- src/command/Commands.js | 1 + src/command/Menus.js | 1 + src/document/DocumentCommandHandlers.js | 41 ++++--- src/document/DocumentManager.js | 49 ++++++++- src/file/FileUtils.js | 26 +++++ src/file/NativeFileSystem.js | 2 +- src/nls/root/strings.js | 4 + src/project/FileIndexManager.js | 2 +- src/project/ProjectManager.js | 139 +++++++++++++++++++++--- src/project/WorkingSetView.js | 16 +++ 10 files changed, 249 insertions(+), 32 deletions(-) diff --git a/src/command/Commands.js b/src/command/Commands.js index a94d7c18b98..0a1b0ca195e 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -44,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 6b4a595d10c..5e59755b340 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -964,6 +964,7 @@ 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 17ea8bc9829..98ee4baf16a 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -95,9 +95,9 @@ define(function (require, exports, module) { } } - function handleCurrentDocumentChange() { + function resetDocumentTitle() { var newDocument = DocumentManager.getCurrentDocument(); - var perfTimerName = PerfUtils.markStart("DocumentCommandHandlers._onCurrentDocumentChange():\t" + (!newDocument || newDocument.file.fullPath)); +// var perfTimerName = PerfUtils.markStart("DocumentCommandHandlers._onCurrentDocumentChange():\t" + (!newDocument || newDocument.file.fullPath)); if (newDocument) { var fullPath = newDocument.file.fullPath; @@ -113,7 +113,7 @@ define(function (require, exports, module) { // Update title text & "dirty dot" display updateTitle(); - PerfUtils.addMeasurement(perfTimerName); +// PerfUtils.addMeasurement(perfTimerName); } function handleDirtyChange(event, changedDoc) { @@ -297,17 +297,11 @@ define(function (require, exports, module) { * file creation call is outstanding */ var fileNewInProgress = false; - - function handleFileNewInProject() { - handleNewItemInProject(false); - } - - function handleNewFolderInProject() { - handleNewItemInProject(true); - } - function handleNewItemInProject(isFolder) - { + /** + * Bottleneck function for creating new files and folders in the project tree. + */ + function _handleNewItemInProject(isFolder) { if (fileNewInProgress) { ProjectManager.forceFinishRename(); return; @@ -345,6 +339,20 @@ define(function (require, exports, module) { 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, @@ -721,6 +729,10 @@ define(function (require, exports, module) { ); } + function handleFileRename() { + ProjectManager.renameSelectedItem(); + } + /** Closes the window, then quits the app */ function handleFileQuit(commandData) { return _handleWindowGoingAway( @@ -807,6 +819,7 @@ define(function (require, exports, module) { 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); @@ -816,7 +829,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", resetDocumentTitle); } // Define public API diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 5980a2f8896..e5a1283c0f1 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,49 @@ 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 + */ + function notifyFileNameChanged(oldName, newName) { + var i, path; + + // Update 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); + } + } + } + // 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 +1124,11 @@ define(function (require, exports, module) { exports.closeFullEditor = closeFullEditor; exports.closeAll = closeAll; exports.notifyFileDeleted = notifyFileDeleted; + exports.notifyFileNameChanged = notifyFileNameChanged; // 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..e46a66e3477 100644 --- a/src/file/FileUtils.js +++ b/src/file/FileUtils.js @@ -261,6 +261,31 @@ 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 + */ + 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(); + } + } + } + } // Define public API exports.LINE_ENDINGS_CRLF = LINE_ENDINGS_CRLF; @@ -276,4 +301,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 2dda3124e52..d1660f46cc4 100644 --- a/src/file/NativeFileSystem.js +++ b/src/file/NativeFileSystem.js @@ -584,7 +584,7 @@ define(function (require, exports, module) { } else { createDirectoryEntry(); } - }); + }); return; } diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 7e0c86e624f..169ffca04ef 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.", @@ -149,6 +152,7 @@ define({ "CMD_FILE_SAVE" : "Save", "CMD_FILE_SAVE_ALL" : "Save All", "CMD_LIVE_FILE_PREVIEW" : "Live Preview", + "CMD_FILE_RENAME" : "Rename\u2026", "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 c064df039d4..b92f54a8f9b 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,7 +820,28 @@ 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. * @@ -895,16 +919,7 @@ 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; } @@ -919,10 +934,14 @@ define(function (require, exports, module) { .addClass("jstree-closed"); } _projectTree.jstree("select_node", data.rslt.obj, true); + + // Notify listeners that the project model has changed + $(exports).triggerHandler("projectFilesChange"); + result.resolve(entry); }; - var errorCallback = function (err) { + var errorCallback = function (error) { if ((error.code === FileError.PATH_EXISTS_ERR) || (error.code === FileError.TYPE_MISMATCH_ERR)) { Dialogs.showModalDialog( @@ -1000,6 +1019,97 @@ define(function (require, exports, module) { return result.promise(); } + /** + * Rename the selected item in the project tree + */ + function renameSelectedItem() { + var selected = _projectTree.jstree("get_selected"); + 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); + + // 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.notifyFileNameChanged() 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.notifyFileNameChanged(oldName, newName); + + // Finally, re-open the selected document + FileViewController.openAndSelectDocument( + DocumentManager.getCurrentDocument().file.fullPath, + FileViewController.getFileSelectionFocus() + ); + + // If a folder was renamed, re-select it here, since openAndSelectDocument() + // changed the selection. + if (selected.hasClass("jstree-open") || selected.hasClass("jstree-closed")) { + var oldSuppressToggleOpen = suppressToggleOpen; + + // Supress the open/close toggle + suppressToggleOpen = true; + _projectTree.jstree("select_node", selected, true); + suppressToggleOpen = oldSuppressToggleOpen; + } + + _redraw(true); + + } else { + // Error during rename. Reset to the old name and alert the user. + _resetOldFilename(); + + // 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) + ) + ); + } + }); + }); + _projectTree.jstree("rename"); + } + } + /** * Forces createNewItem() to complete by removing focus from the rename field which causes * the new file to be written to disk @@ -1042,5 +1152,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 From 2e38d6d01478a4ab7ad5db36627c3797c94075da Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Tue, 25 Sep 2012 09:32:52 -0700 Subject: [PATCH 3/9] Add unit tests for brackets.fs.rename. --- .../rename_me/hello.txt | 1 + test/spec/LowLevelFileIO-test.js | 171 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 test/spec/LowLevelFileIO-test-files/rename_me/hello.txt 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? + }); }); }); From 7d97275d8c3f02c6ad8239c2ba215f430b7badaa Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Wed, 26 Sep 2012 07:11:54 -0700 Subject: [PATCH 4/9] Cleanup. --- src/document/DocumentCommandHandlers.js | 4 ++-- src/file/NativeFileSystem.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 98ee4baf16a..5a7c932083e 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -97,7 +97,7 @@ define(function (require, exports, module) { function resetDocumentTitle() { var newDocument = DocumentManager.getCurrentDocument(); -// var perfTimerName = PerfUtils.markStart("DocumentCommandHandlers._onCurrentDocumentChange():\t" + (!newDocument || newDocument.file.fullPath)); + var perfTimerName = PerfUtils.markStart("DocumentCommandHandlers._onCurrentDocumentChange():\t" + (!newDocument || newDocument.file.fullPath)); if (newDocument) { var fullPath = newDocument.file.fullPath; @@ -113,7 +113,7 @@ define(function (require, exports, module) { // Update title text & "dirty dot" display updateTitle(); -// PerfUtils.addMeasurement(perfTimerName); + PerfUtils.addMeasurement(perfTimerName); } function handleDirtyChange(event, changedDoc) { diff --git a/src/file/NativeFileSystem.js b/src/file/NativeFileSystem.js index d1660f46cc4..770c9302bd3 100644 --- a/src/file/NativeFileSystem.js +++ b/src/file/NativeFileSystem.js @@ -578,7 +578,9 @@ define(function (require, exports, module) { // create the file if (options.create) { - brackets.fs.makedir(directoryFullPath, 0 /* TODO - should be 0777 */, function (err) { + // 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 { From 3291e055af9349a779a2f3dcace9e9ea26384a24 Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Wed, 26 Sep 2012 10:17:00 -0700 Subject: [PATCH 5/9] Comment out code that was throwing an exception. --- src/document/DocumentCommandHandlers.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 5a7c932083e..d2bc6e278e9 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -97,7 +97,11 @@ define(function (require, exports, module) { function resetDocumentTitle() { 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) { From 5db70c863b8c327efdb6b1393c5d28a5c6afa766 Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Thu, 27 Sep 2012 18:34:22 -0700 Subject: [PATCH 6/9] Refactor renaming. Fix bugs. --- src/document/DocumentManager.js | 4 +- src/project/ProjectManager.js | 120 ++++++++++++++++++++------------ src/search/FindInFiles.js | 15 +++- 3 files changed, 92 insertions(+), 47 deletions(-) diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index e5a1283c0f1..695f5bb6555 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -1074,7 +1074,9 @@ define(function (require, exports, module) { var i, path; // Update currentDocument - FileUtils.updateFileEntryPath(_currentDocument.file, oldName, newName); + if (_currentDocument) { + FileUtils.updateFileEntryPath(_currentDocument.file, oldName, newName); + } // Update open documents var keysToDelete = []; diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index b92f54a8f9b..43aa3fa6405 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -1019,6 +1019,76 @@ 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 + * @return {$.Promise} A promise object that will be resolved or rejected when + * the rename is finished. + */ + function renameItem(oldName, newName) { + 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.notifyFileNameChanged() 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.notifyFileNameChanged(oldName, newName); + + // 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 */ @@ -1047,32 +1117,8 @@ define(function (require, exports, module) { var oldName = selected.data("entry").fullPath; var newName = oldName.replace(data.rslt.old_name, data.rslt.new_name); - // 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.notifyFileNameChanged() 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.notifyFileNameChanged(oldName, newName); - - // Finally, re-open the selected document - FileViewController.openAndSelectDocument( - DocumentManager.getCurrentDocument().file.fullPath, - FileViewController.getFileSelectionFocus() - ); + renameItem(oldName, newName) + .done(function () { // If a folder was renamed, re-select it here, since openAndSelectDocument() // changed the selection. @@ -1084,27 +1130,11 @@ define(function (require, exports, module) { _projectTree.jstree("select_node", selected, true); suppressToggleOpen = oldSuppressToggleOpen; } - - _redraw(true); - - } else { + }) + .fail(function (err) { // Error during rename. Reset to the old name and alert the user. _resetOldFilename(); - - // 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) - ) - ); - } - }); + }); }); _projectTree.jstree("rename"); } 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 From cd0199c9b840c615bac20d44b6b3d834ec7af70c Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Mon, 1 Oct 2012 22:56:29 -0700 Subject: [PATCH 7/9] Respond to review comments. --- src/document/DocumentCommandHandlers.js | 4 ++-- src/document/DocumentManager.js | 11 +++++++++-- src/file/FileUtils.js | 5 +++++ src/nls/root/strings.js | 4 ++-- src/project/ProjectManager.js | 21 +++++++++++++++------ 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index d2bc6e278e9..41549ad7739 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -95,7 +95,7 @@ define(function (require, exports, module) { } } - function resetDocumentTitle() { + function updateDocumentTitle() { var newDocument = DocumentManager.getCurrentDocument(); // TODO: This timer is causing a "Recursive tests with the same name are not supporte" @@ -833,7 +833,7 @@ define(function (require, exports, module) { // Listen for changes that require updating the editor titlebar $(DocumentManager).on("dirtyFlagChange", handleDirtyChange); - $(DocumentManager).on("currentDocumentChange fileNameChange", resetDocumentTitle); + $(DocumentManager).on("currentDocumentChange fileNameChange", updateDocumentTitle); } // Define public API diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 695f5bb6555..7c2abb8cb02 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -1069,8 +1069,9 @@ define(function (require, exports, module) { * * @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 notifyFileNameChanged(oldName, newName) { + function notifyPathNameChanged(oldName, newName, isFolder) { var i, path; // Update currentDocument @@ -1091,6 +1092,12 @@ define(function (require, exports, module) { // 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; + } } } } @@ -1126,7 +1133,7 @@ define(function (require, exports, module) { exports.closeFullEditor = closeFullEditor; exports.closeAll = closeAll; exports.notifyFileDeleted = notifyFileDeleted; - exports.notifyFileNameChanged = notifyFileNameChanged; + exports.notifyPathNameChanged = notifyPathNameChanged; // Setup preferences _prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID); diff --git a/src/file/FileUtils.js b/src/file/FileUtils.js index e46a66e3477..9685d2d447e 100644 --- a/src/file/FileUtils.js +++ b/src/file/FileUtils.js @@ -267,6 +267,7 @@ define(function (require, exports, module) { * @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) { @@ -284,7 +285,11 @@ define(function (require, exports, module) { entry.name = pathParts.pop(); } } + + return true; } + + return false; } // Define public API diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 169ffca04ef..b502e3d46fd 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -142,7 +142,7 @@ 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", @@ -152,7 +152,7 @@ define({ "CMD_FILE_SAVE" : "Save", "CMD_FILE_SAVE_ALL" : "Save All", "CMD_LIVE_FILE_PREVIEW" : "Live Preview", - "CMD_FILE_RENAME" : "Rename\u2026", + "CMD_FILE_RENAME" : "Rename", "CMD_QUIT" : "Quit", // Edit menu commands diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 43aa3fa6405..ca0e11c6992 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -935,6 +935,12 @@ define(function (require, exports, module) { } _projectTree.jstree("select_node", data.rslt.obj, true); + // If the new item is a folder, force a re-sort here. Windows sorts folders + // and files separately. + if (isFolder) { + _projectTree.jstree("sort", _projectTree); + } + // Notify listeners that the project model has changed $(exports).triggerHandler("projectFilesChange"); @@ -1025,10 +1031,11 @@ define(function (require, exports, module) { * * @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) { + function renameItem(oldName, newName, isFolder) { var result = new $.Deferred(); if (oldName === newName) { @@ -1041,7 +1048,7 @@ define(function (require, exports, module) { brackets.fs.rename(oldName, newName, function (err) { if (!err) { // Update all nodes in the project tree. - // All other updating is done by DocumentManager.notifyFileNameChanged() below + // All other updating is done by DocumentManager.notifyPathNameChanged() below var nodes = _projectTree.find(".jstree-leaf, .jstree-open, .jstree-closed"), i; @@ -1055,7 +1062,7 @@ define(function (require, exports, module) { // Tell the document manager about the name change. This will update // all of the model information and send notification to all views - DocumentManager.notifyFileNameChanged(oldName, newName); + DocumentManager.notifyPathNameChanged(oldName, newName, isFolder); // Finally, re-open the selected document if (DocumentManager.getCurrentDocument()) { @@ -1093,7 +1100,9 @@ define(function (require, exports, module) { * Rename the selected item in the project tree */ function renameSelectedItem() { - var selected = _projectTree.jstree("get_selected"); + 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"); @@ -1117,12 +1126,12 @@ define(function (require, exports, module) { var oldName = selected.data("entry").fullPath; var newName = oldName.replace(data.rslt.old_name, data.rslt.new_name); - renameItem(oldName, newName) + renameItem(oldName, newName, isFolder) .done(function () { // If a folder was renamed, re-select it here, since openAndSelectDocument() // changed the selection. - if (selected.hasClass("jstree-open") || selected.hasClass("jstree-closed")) { + if (isFolder) { var oldSuppressToggleOpen = suppressToggleOpen; // Supress the open/close toggle From c16e87ce4476c5695d514cb7876bbba3972ac1ac Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Tue, 2 Oct 2012 21:54:42 -0700 Subject: [PATCH 8/9] More review comments. --- src/document/DocumentCommandHandlers.js | 41 ++++++++++++++++--------- src/project/ProjectManager.js | 2 +- src/styles/brackets.less | 2 ++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 41549ad7739..6995654c1f2 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -257,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); @@ -273,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 @@ -326,7 +339,7 @@ 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, isFolder ? "" : ".js"); + var deferred = _getUntitledFileSuggestion(baseDir, Strings.UNTITLED, isFolder ? "" : ".js", isFolder); var createWithSuggestedName = function (suggestedName) { ProjectManager.createNewItem(baseDir, suggestedName, false, isFolder) .pipe(deferred.resolve, deferred.reject, deferred.notify) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index ca0e11c6992..a1b2e06f326 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -938,7 +938,7 @@ define(function (require, exports, module) { // If the new item is a folder, force a re-sort here. Windows sorts folders // and files separately. if (isFolder) { - _projectTree.jstree("sort", _projectTree); + _projectTree.jstree("sort", data.rslt.obj.parent()); } // Notify listeners that the project model has changed 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 { From edda6b53a4e74a3e4c28a14a82c3836707ba9f1b Mon Sep 17 00:00:00 2001 From: Glenn Ruehle Date: Wed, 3 Oct 2012 10:08:26 -0700 Subject: [PATCH 9/9] Select after sorting. --- src/project/ProjectManager.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index a1b2e06f326..29172c92029 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -933,14 +933,15 @@ define(function (require, exports, module) { data.rslt.obj.removeClass("jstree-leaf jstree-closed jstree-open") .addClass("jstree-closed"); } - _projectTree.jstree("select_node", data.rslt.obj, true); // 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); + // Notify listeners that the project model has changed $(exports).triggerHandler("projectFilesChange");