Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

Add "New Folder" and "Rename" features #1719

Merged
merged 9 commits into from
Oct 3, 2012
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/command/Commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/command/Menus.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
50 changes: 39 additions & 11 deletions src/document/DocumentCommandHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,13 @@ define(function (require, exports, module) {
}
}

function handleCurrentDocumentChange() {
function resetDocumentTitle() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"reset" sounds like it's being changed back to a previous state. How about updateDocumentTitle() or changeDocumentTitle() ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. Done.

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;
Expand All @@ -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) {
Expand Down Expand Up @@ -297,9 +301,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;
Expand All @@ -320,21 +326,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");
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,
Expand Down Expand Up @@ -711,6 +733,10 @@ define(function (require, exports, module) {
);
}

function handleFileRename() {
ProjectManager.renameSelectedItem();
}

/** Closes the window, then quits the app */
function handleFileQuit(commandData) {
return _handleWindowGoingAway(
Expand Down Expand Up @@ -791,11 +817,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);
Expand All @@ -805,7 +833,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
Expand Down
49 changes: 47 additions & 2 deletions src/document/DocumentManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of a file (as opposed to a folder), we can exit this loop after we find the first item because we know there will be at most 1. So, it might be worth adding an isFolder parameter to this function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// 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;
Expand All @@ -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()");
Expand Down
26 changes: 26 additions & 0 deletions src/file/FileUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -276,4 +301,5 @@ define(function (require, exports, module) {
exports.getNativeBracketsDirectoryPath = getNativeBracketsDirectoryPath;
exports.getNativeModuleDirectoryPath = getNativeModuleDirectoryPath;
exports.canonicalizeFolderPath = canonicalizeFolderPath;
exports.updateFileEntryPath = updateFileEntryPath;
});
84 changes: 82 additions & 2 deletions src/file/NativeFileSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -49,6 +50,8 @@ define({
"ERROR_RELOADING_FILE" : "An error occurred when trying to reload the file <span class='dialog-filename'>{0}</span>. {1}",
"ERROR_SAVING_FILE_TITLE" : "Error saving file",
"ERROR_SAVING_FILE" : "An error occurred when trying to save the file <span class='dialog-filename'>{0}</span>. {1}",
"ERROR_RENAMING_FILE_TITLE" : "Error renaming file",
"ERROR_RENAMING_FILE" : "An error occurred when trying to rename the file <span class='dialog-filename'>{0}</span>. {1}",
"INVALID_FILENAME_TITLE" : "Invalid file name",
"INVALID_FILENAME_MESSAGE" : "Filenames cannot contain the following characters: /?*:;{}<>\\|",
"FILE_ALREADY_EXISTS" : "The file <span class='dialog-filename'>{0}</span> already exists.",
Expand Down Expand Up @@ -140,6 +143,7 @@ define({
// File menu commands
"FILE_MENU" : "File",
"CMD_FILE_NEW" : "New",
"CMD_FILE_NEW_FOLDER" : "New Folder",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have "New Folder", I think "New" should be changed to "New File".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"CMD_FILE_OPEN" : "Open\u2026",
"CMD_ADD_TO_WORKING_SET" : "Add To Working Set",
"CMD_OPEN_FOLDER" : "Open Folder\u2026",
Expand All @@ -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\u2026",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think "Rename..." should have ellipses, since it uses inline editing (i.e. it does not invoke a modal dialog).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we used ellipses when we require more input.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always thought the distinction was to use ellipses if you're invoking a modal dialog. I don't see ellipses after Rename anywhere else (including Windows File Explorer, DW, VS, Xcode). Also, "New" and "New Folder" require the same additional input as "Rename", and I don't think that we want to add ellipses to those.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, since we don't have ellipses for "New File" or "New Folder", we should probably leave it off "Rename" too. Done.

"CMD_QUIT" : "Quit",

// Edit menu commands
Expand Down
2 changes: 1 addition & 1 deletion src/project/FileIndexManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ define(function (require, exports, module) {
}
);

$(ProjectManager).on("projectOpen", function (event, projectRoot) {
$(ProjectManager).on("projectOpen projectFilesChange", function (event, projectRoot) {
markDirty();
});

Expand Down
Loading