Bug 426407 - New File/Folder commands should create a file/folder immediately and allow user to rename it
- Extended implementation to top level folders
- No longer immediately creating file/folder. Rather creating a place holder dom node and prompting for name of file/folder first. This greatly simplifies the implementation and allows the user to cancel the creation by pressing "ESC".
- Refactored uiutils.js->getUserText() to take an options object and modified callers. Also removed unused promptMessage parameter and added insertAsChild option.

--Signed-off-by: Elijah El-Haddad <elijahe@ca.ibm.com>
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/fileClient.js b/bundles/org.eclipse.orion.client.core/web/orion/fileClient.js
index 84b91c0..63266ad 100644
--- a/bundles/org.eclipse.orion.client.core/web/orion/fileClient.js
+++ b/bundles/org.eclipse.orion.client.core/web/orion/fileClient.js
@@ -342,7 +342,7 @@
 			});
 			
 		},
-		 
+				
 		/**
 		 * Copies a file or directory.
 		 * @param {String} sourceLocation The location of the file or directory to copy.
diff --git a/bundles/org.eclipse.orion.client.ui/web/edit/content/jsonExplorer.js b/bundles/org.eclipse.orion.client.ui/web/edit/content/jsonExplorer.js
index cd9d3ff..01a3c2f 100644
--- a/bundles/org.eclipse.orion.client.ui/web/edit/content/jsonExplorer.js
+++ b/bundles/org.eclipse.orion.client.ui/web/edit/content/jsonExplorer.js
@@ -177,7 +177,14 @@
 						}
 					}
 				}
-				mUIUtils.getUserText(id, target, true, text, doChange, null, null, ""); //$NON-NLS-0$
+
+				mUIUtils.getUserText({
+					id: id,
+					refNode: target, 
+					shouldHideRefNode: true, 
+					initialText: text, 
+					onComplete: doChange
+				});
 			}
 		});
 	}
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/explorers/explorer.js b/bundles/org.eclipse.orion.client.ui/web/orion/explorers/explorer.js
index 338821c..8c96480 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/explorers/explorer.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/explorers/explorer.js
@@ -68,7 +68,7 @@
 			}
 		},
 		
-		makeNewItemPlaceHolder: function(item, domId, column_no) {
+		makeNewItemPlaceHolder: function(item, domId, column_no, insertAfter) {
 			// we want to popup the name prompt underneath the parent item.
 			var refNode = this.getRow(item);
 			var tempNode;
@@ -88,7 +88,17 @@
 				var td = document.createElement("td"); //$NON-NLS-0$
 				td.id = domId+"placeHolderCol"; //$NON-NLS-0$
 				tr.appendChild(td);
-				refNode.appendChild(tr);
+				if (insertAfter) {
+					// insert tr after refNode, i.e. right before refNode's nextSibling in the parent
+					var parentNode = refNode.parentNode;
+					var nextSibling = refNode.nextSibling;
+					parentNode.insertBefore(tr, nextSibling);
+					
+					var parentIndentation = parseInt(refNode.firstChild.style.paddingLeft); //refNode is a <tr>, we want the indentation of its <td>
+					td.style.paddingLeft = (this.myTree.getIndent() + parentIndentation) + "px";
+				} else {
+					refNode.appendChild(tr);
+				}
 				tempNode = lib.node(domId+"placeHolderRow"); //$NON-NLS-0$
 				refNode = lib.node(domId+"placeHolderCol"); //$NON-NLS-0$
 				if (tempNode && refNode) {
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/fileCommands.js b/bundles/org.eclipse.orion.client.ui/web/orion/fileCommands.js
index 8734ff1..fd02e9c 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/fileCommands.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/fileCommands.js
@@ -174,27 +174,45 @@
 	
 	function getNewItemName(explorer, item, domId, defaultName, onDone) {
 		var refNode, name, tempNode;
-		if (item.Location === explorer.treeRoot.Location) {
-			refNode = lib.node(domId);
+		var shouldHideRefNode = true;
+		var insertAsChild = false;
+		
+		var nodes = explorer.makeNewItemPlaceHolder(item, domId, null, true);
+		if (nodes) {
+			refNode = nodes.refNode;
+			tempNode = nodes.tempNode;
+			shouldHideRefNode = false;
+			insertAsChild = true;
 		} else {
-			var nodes = explorer.makeNewItemPlaceHolder(item, domId);
-			if (nodes) {
-				refNode = nodes.refNode;
-				tempNode = nodes.tempNode;
-			} else {
-				refNode = lib.node(domId);
-			}
+			refNode = lib.node(domId);
 		}
+
 		if (refNode) {
-			mUIUtils.getUserText(domId+"EditBox", refNode, false, defaultName,  //$NON-NLS-0$
-				function(name) { 
-					if (name) {
-						if (tempNode && tempNode.parentNode) {
-							tempNode.parentNode.removeChild(tempNode);
-						}
-						onDone(name);
-					}
-				}); 
+			var done = function(name) { 
+				if (name) {
+					onDone(name);
+				}
+			};
+			var destroy = function() {
+				try {
+					if (tempNode && tempNode.parentNode) {
+						tempNode.parentNode.removeChild(tempNode);
+					}	
+				} catch (err) {
+					// tempNode already removed, do nothing
+				}
+			};
+			
+			mUIUtils.getUserText({
+				id: domId+"EditBox", //$NON-NLS-0$
+				refNode: refNode,
+				shouldHideRefNode: shouldHideRefNode,
+				initialText: defaultName,
+				onComplete: done,
+				onEditDestroy: destroy,
+				isInitialValid: true,
+				insertAsChild: insertAsChild
+			});
 		} else {
 			name = window.prompt(defaultName);
 			if (name) {
@@ -542,6 +560,59 @@
 		function checkFolderSelection(item) {
 			return getTargetFolder(explorer.selection.getSelections()) || getTargetFolder(item);
 		}
+		
+		function doMove(item, newText) {
+			var moveLocation = item.Location;
+			Deferred.when(getLogicalModelItems(item), function(logicalItems) {
+				item = logicalItems.item;
+				var parent = logicalItems.parent;
+				if (parent && parent.Projects) {
+					//special case for moving a project. We want to move the project rather than move the project's content
+					parent.Projects.some(function(project) {
+						if (project.Id === item.Id) {
+							moveLocation = project.Location;
+							return true;
+						}
+						return false;
+					});
+				}
+				var deferred = fileClient.moveFile(moveLocation, parent.Location, newText);
+				progressService.showWhile(deferred, i18nUtil.formatMessage(messages["Renaming ${0}"], moveLocation)).then(
+					function(newItem) {
+						if (!item.parent) {
+							item.parent = parent;
+						}
+						dispatchModelEvent({ type: "move", oldValue: item, newValue: newItem, parent: parent }); //$NON-NLS-0$
+					},
+					errorHandler
+				);
+			});
+		}
+		
+		var editAndRename = function(item, data) {
+			// we want to popup the edit box over the name in the explorer.
+			// if we can't find it, we'll pop it up over the command dom element.
+			var refNode = explorer.getNameNode(item);
+			if (!refNode) {
+				if (!data) {
+					return;
+				}
+				refNode = data.domParent || data.domNode;
+			}
+			var id = refNode.id+"EditBox"; //$NON-NLS-0$
+			if (lib.node(id)) {
+				return;
+			}
+			
+			mUIUtils.getUserText({
+				id: id, 
+				refNode: refNode, 
+				shouldHideRefNode: true, 
+				initialText: item.Name,
+				onComplete: doMove.bind(null, item), 
+				selectTo: item.Directory ? "" : "." //$NON-NLS-1$ //$NON-NLS-0$
+			});
+		};
 
 		var renameCommand = new mCommands.Command({
 				name: messages["Rename"],
@@ -568,45 +639,12 @@
 					}),
 				callback: function(data) {
 					var item = forceSingleItem(data.items);
-					function doMove(newText) {
-						var moveLocation = item.Location;
-						Deferred.when(getLogicalModelItems(item), function(logicalItems) {
-							item = logicalItems.item;
-							var parent = logicalItems.parent;
-							if (parent && parent.Projects) {
-								//special case for moving a project. We want to move the project rather than move the project's content
-								parent.Projects.some(function(project) {
-									if (project.Id === item.Id) {
-										moveLocation = project.Location;
-										return true;
-									}
-									return false;
-								});
-							}
-							var deferred = fileClient.moveFile(moveLocation, parent.Location, newText);
-							progressService.showWhile(deferred, i18nUtil.formatMessage(messages["Renaming ${0}"], moveLocation)).then(
-								function(newItem) {
-									dispatchModelEvent({ type: "move", oldValue: item, newValue: newItem, parent: parent }); //$NON-NLS-0$
-								},
-								errorHandler
-							);
-						});
-					}
+					
 					var name;
 					if (data.parameters.hasParameters() && (name = data.parameters.valueFor("name")) !== null) { //$NON-NLS-0$
-						doMove(name);
+						doMove(item, name);
 					} else {
-						// we want to popup the edit box over the name in the explorer.
-						// if we can't find it, we'll pop it up over the command dom element.
-						var refNode = explorer.getNameNode(item);
-						if (!refNode) {
-							refNode = data.domParent || data.domNode;
-						}
-						var id = refNode.id+"EditBox"; //$NON-NLS-0$
-						if (lib.node(id)) {
-							return;
-						}
-						mUIUtils.getUserText(id, refNode, true, item.Name, doMove, null, null, item.Directory ? "" : ".");  //$NON-NLS-1$ //$NON-NLS-0$
+						editAndRename(item, data);
 					}
 				}
 			});
@@ -742,67 +780,95 @@
 			}
 		});
 		commandService.addCommand(downloadCommand);
+
+		function createUniqueNameArtifact(parentItem, prefix, createFunction) {
+			// get the list of files that already exists in the selected directory and ensure 
+			// that the new file's initial name is unique within that directory
+			var location = parentItem.ChildrenLocation;
+			progressService.progress(fileClient.fetchChildren(location), messages["Fetching children of "] + parentItem.Name).then( //$NON-NLS-0$
+				function(children) {
+					var attempt = 0;
+					var uniqueName = prefix;
+					
+					// find a unique name for the new artifact
+					var possiblyCollidingNames = children.filter(function(child){
+						return 0 === child.Name.indexOf(prefix);
+					}).map(function(child){
+						return child.Name;
+					});
+					
+					while (-1 !== possiblyCollidingNames.indexOf(uniqueName)){
+						attempt++;
+						uniqueName = prefix.concat(" (").concat(attempt).concat(")");  //$NON-NLS-1$ //$NON-NLS-0$
+					}
+					
+					// create the artifact
+					createFunction(uniqueName);
+				},
+				errorHandler);
+		}		
 		
-		var newFileNameParameters = new mCommandRegistry.ParametersDescription([new mCommandRegistry.CommandParameter('name', 'text', messages['Name:'], messages['New File'])]); //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
+		/**
+		 * Creates a new file or folder as a child of the specified parentItem.
+		 */
+		function createNewArtifact(namePrefix, parentItem, isDirectory) {
+			var createFunction = function(name) {
+				if (name) {
+					var location = parentItem.Location;
+					var functionName = isDirectory ? "createFolder" : "createFile";
+					var deferred = fileClient[functionName](location, name);
+					progressService.showWhile(deferred, i18nUtil.formatMessage(messages["Creating ${0}"], name)).then(
+						function(newArtifact) {
+							dispatchModelEvent({ type: "create", parent: parentItem, newValue: newArtifact }); //$NON-NLS-0$
+						},
+						errorHandler);
+				}
+			};
+			
+			createUniqueNameArtifact(parentItem, namePrefix, function(uniqueName){
+				getNewItemName(explorer, parentItem, explorer.getRow(parentItem), uniqueName, function(name) {
+					createFunction(name);
+				});
+			});
+		}
+		
+		var getParentItem = function(selections, items){
+			var item = getTargetFolder(selections);
+			if (!item) {
+				item = explorer.treeRoot;
+				if (item.Project) {
+					item = item.children[0];
+				}
+			}
+			return item;
+		};
 		
 		var newFileCommand = new mCommands.Command({
 			name: messages["New File"],
 			tooltip: messages["Create a new file"],
 			imageClass: "core-sprite-new_file", //$NON-NLS-0$
 			id: "eclipse.newFile" + idSuffix, //$NON-NLS-0$
-			parameters: newFileNameParameters,
 			callback: function(data) {
 				// Check selection service first, then use the provided item
 				explorer.selection.getSelections(function(selections) {
-					var item = getTargetFolder(selections) || getTargetFolder(data.items);
-					var createFunction = function(name) {
-						if (name) {
-							var deferred = fileClient.createFile(item.Location, name);
-							progressService.showWhile(deferred, i18nUtil.formatMessage(messages["Creating ${0}"], name)).then(
-								function(newFile) {
-									dispatchModelEvent({ type: "create", parent: item, newValue: newFile }); //$NON-NLS-0$
-								},
-								errorHandler);
-						}
-					};
-					if (data.parameters && data.parameters.valueFor('name')) { //$NON-NLS-0$
-						createFunction(data.parameters.valueFor('name')); //$NON-NLS-0$
-					} else {
-						getNewItemName(explorer, item, data.domNode.id, messages['New File'], createFunction);
-					}
+					var item = getParentItem(selections);
+					createNewArtifact(messages["New File"], item, false);
 				});
 			},
 			visibleWhen: checkFolderSelection
 		});
 		commandService.addCommand(newFileCommand);
 		
-		var newFolderNameParameters = new mCommandRegistry.ParametersDescription([new mCommandRegistry.CommandParameter('name', 'text', messages['Folder name:'], messages['New Folder'])]); //$NON-NLS-1$ //$NON-NLS-0$
-
 		var newFolderCommand = new mCommands.Command({
 			name: messages['New Folder'],
 			tooltip: messages["Create a new folder"],
 			imageClass: "core-sprite-new_folder", //$NON-NLS-0$
 			id: "eclipse.newFolder" + idSuffix, //$NON-NLS-0$
-			parameters: newFolderNameParameters,
 			callback: function(data) {
 				// Check selection service first, then use the provided item
 				explorer.selection.getSelections(function(selections) {
-					var item = getTargetFolder(selections) || getTargetFolder(data.items);
-					var createFunction = function(name) {
-						if (name) {
-							var deferred = fileClient.createFolder(item.Location, name);
-							progressService.showWhile(deferred, i18nUtil.formatMessage(messages["Creating ${0}"], name)).then(
-								function(newFolder) {
-									dispatchModelEvent({ type: "create", parent: item, newValue: newFolder }); //$NON-NLS-0$
-								},
-								errorHandler);
-						}
-					};
-					if (data.parameters && data.parameters.valueFor('name')) { //$NON-NLS-0$
-						createFunction(data.parameters.valueFor('name')); //$NON-NLS-0$
-					} else {
-						getNewItemName(explorer, item, data.domNode.id, messages['New Folder'], createFunction);
-					}
+					var item = getParentItem(selections);
+					createNewArtifact(messages["New Folder"], item, true);
 				});
 			},
 			visibleWhen: function(item) {
@@ -840,7 +906,6 @@
 		
 		var newProjectCommand = new mCommands.Command({
 			name: messages["New Folder"],
-			parameters: newFolderNameParameters,
 			imageClass: "core-sprite-new_folder", //$NON-NLS-0$
 			tooltip: messages["Create an empty folder"],
 			description: messages["Create an empty folder on the Orion server.  You can import, upload, or create content in the editor."],
@@ -853,13 +918,12 @@
 						newFolderCommand.callback(data);
 					} else {
 						item = forceSingleItem(data.items);
-						if (data.parameters && data.parameters.valueFor('name')) { //$NON-NLS-0$
-							createProject(explorer, fileClient, progressService, data.parameters.valueFor('name')); //$NON-NLS-0$
-						} else {
-							getNewItemName(data.items, data.domNode.id, messages['New Folder'], function(name) {
+						var defaultName = messages['New Folder']; //$NON-NLS-0$
+						createUniqueNameArtifact(item, defaultName, function(uniqueName){
+							getNewItemName(explorer, item, explorer.getRow(item), uniqueName, function(name) {
 								createProject(explorer, fileClient, progressService, name);
 							});
-						}
+						});
 					} 
 				});
 			},
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/uiUtils.js b/bundles/org.eclipse.orion.client.ui/web/orion/uiUtils.js
index a979501..032ea97 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/uiUtils.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/uiUtils.js
@@ -108,18 +108,28 @@
 	/**
 	 * @name orion.uiUtils.getUserText
 	 * @function
-	 * @param {String} id
-	 * @param {Element} refNode
-	 * @param {Boolean} shouldHideRefNode
-	 * @param {String} initialText
-	 * @param {Function} onComplete
-	 * @param {Function} onEditDestroy
-	 * @param {String} promptMessage
-	 * @param {String} selectTo
-	 * @param {Boolean} isInitialValid
+	 * @param {Object} options The options object
+	 * @param {String} options.id
+	 * @param {Element} options.refNode
+	 * @param {Boolean} options.shouldHideRefNode
+	 * @param {String} options.initialText
+	 * @param {Function} options.onComplete
+	 * @param {Function} options.onEditDestroy
+	 * @param {String} options.selectTo
+	 * @param {Boolean} options.isInitialValid
+	 * @param {Boolean} options.insertAsChild
 	 */
-	function getUserText(id, refNode, shouldHideRefNode, initialText, onComplete, onEditDestroy, promptMessage, selectTo, isInitialValid) {
-		/** @return {Function} function(event) */
+	function getUserText(options) {
+		var id = options.id;
+		var refNode = options.refNode;
+		var shouldHideRefNode = options.shouldHideRefNode;
+		var initialText = options.initialText;
+		var onComplete = options.onComplete;
+		var onEditDestroy = options.onEditDestroy;
+		var selectTo = options.selectTo;
+		var isInitialValid = options.isInitialValid;
+		var insertAsChild = options.insertAsChild;
+		
 		var done = false;
 		var handler = function(isKeyEvent) {
 			return function(event) {
@@ -170,7 +180,11 @@
 		var editBox = document.createElement("input"); //$NON-NLS-0$
 		editBox.id = id;
 		editBox.value = initialText || "";
-		refNode.parentNode.insertBefore(editBox, refNode.nextSibling);
+		if (insertAsChild) {
+			refNode.appendChild(editBox);
+		} else {
+			refNode.parentNode.insertBefore(editBox, refNode.nextSibling);
+		}
 		editBox.classList.add("userEditBoxPrompt"); //$NON-NLS-0$
 		if (shouldHideRefNode) {
 			refNode.style.display = "none"; //$NON-NLS-0$
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/webui/treetable.js b/bundles/org.eclipse.orion.client.ui/web/orion/webui/treetable.js
index fac58ca..809f0a4 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/webui/treetable.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/webui/treetable.js
@@ -319,6 +319,13 @@
 			if(this._onCollapse){
 				this._onCollapse(row._item);
 			}
+		},
+		
+		/**
+		 * Returns this tree's indentation increment
+		 */
+		getIndent: function() {
+			return this._indent;
 		}
 	};  // end prototype
 	TableTree.prototype.constructor = TableTree;