Merge branch 'master' of ssh://git.eclipse.org/gitroot/orion/org.eclipse.orion.client
diff --git a/bundles/org.eclipse.orion.client.ui/web/css/ide.css b/bundles/org.eclipse.orion.client.ui/web/css/ide.css
index 5fff21d..21beaee 100644
--- a/bundles/org.eclipse.orion.client.ui/web/css/ide.css
+++ b/bundles/org.eclipse.orion.client.ui/web/css/ide.css
@@ -866,8 +866,10 @@
 .delegatedUI {
 	position: absolute; 
 	min-width: 600px; 
-	min-height: 400px; 
-	left: 150px; 
-	top: 150px; 
+	min-height: 400px;
+	/* width, height may be overridden by the provider */
+	width: 600px;
+	height: 400px;
 	z-index: 150;
+	border: 1px inset;
 }
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/edit/nls/root/messages.js b/bundles/org.eclipse.orion.client.ui/web/orion/edit/nls/root/messages.js
index 529b84a..334c128 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/edit/nls/root/messages.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/edit/nls/root/messages.js
@@ -30,6 +30,6 @@
 	"Redo": "Redo",
 	"Find": "Find",
 	"No response from server.  Check your internet connection and try again.": "No response from server.  Check your internet connection and try again.",
-	"Saving file {0}": "Saving file {0}",
-	"Running {0}": "Running {0}"
+	"Saving file {0}": "Saving file ${0}",
+	"Running {0}": "Running ${0}"
 });
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/editorCommands.js b/bundles/org.eclipse.orion.client.ui/web/orion/editorCommands.js
index a87b730..fa02bcd 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/editorCommands.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/editorCommands.js
@@ -63,8 +63,8 @@
 				return keyBinding;
 			}
 
-			function handleStatus(status) {
-				if (status && typeof status.HTML !== "undefined") { //$NON-NLS-0$
+			function handleStatus(status, allowHTML) {
+				if (!allowHTML && status && typeof status.HTML !== "undefined") { //$NON-NLS-0$
 					delete status.HTML;
 				}
 				var statusService = serviceRegistry.getService("orion.page.message"); //$NON-NLS-0$
@@ -83,7 +83,7 @@
 				} else {
 					errorToDisplay = error;
 				}
-				handleStatus(errorToDisplay);
+				handleStatus(errorToDisplay, true /*allow HTML for auth errors*/);
 			}
 
 			// create commands common to all editors
@@ -329,7 +329,7 @@
 							}
 						};
 
-						progress.progress(service.run(model.getText(selection.start,selection.end),text,selection, input.getInput()), i18nUtil.formatMessage(messages['Running {0}'], info.name)).then(function(result){
+						progress.showWhile(service.run(model.getText(selection.start,selection.end),text,selection, input.getInput()), i18nUtil.formatMessage(messages['Running {0}'], info.name)).then(function(result){
 							if (result && result.uriTemplate) {
 								var uriTemplate = new URITemplate(result.uriTemplate);
 								var href = uriTemplate.expand(input.getFileMetadata());
@@ -347,7 +347,11 @@
 								if (result.height) {
 									iframe.style.height = result.height;
 								}
+								iframe.style.visibility = 'hidden';
 								window.document.body.appendChild(iframe);
+								iframe.style.left = (window.innerWidth - parseInt(iframe.clientWidth, 10))/2 + "px";
+								iframe.style.top = (window.innerHeight - parseInt(iframe.clientHeight, 10))/2 + "px";
+								iframe.style.visibility = '';
 								// Listen for notification from the iframe.  We expect either a "result" or a "cancelled" property.
 								window.addEventListener("message", function _messageHandler(event) { //$NON-NLS-0$
 									if (event.source !== iframe.contentWindow) {
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js b/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js
index aea61af..a7b340f 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/webui/dropdown.js
@@ -95,8 +95,9 @@
 			var items = this.getItems();
 			if (items.length > 0) {
 				if (!this._hookedAutoDismiss) {
-					// add auto dismiss.  Clicking anywhere but trigger and dropdown means close.
-					lib.addAutoDismiss([this._triggerNode, this._dropdownNode], this.close.bind(this));
+					// add auto dismiss.  Clicking anywhere but trigger or a submenu item means close.
+					var submenuNodes = lib.$$(".dropdownSubMenu", this._dropdownNode);
+					lib.addAutoDismiss([this._triggerNode].concat(Array.prototype.slice.call(submenuNodes)), this.close.bind(this));
 					this._hookedAutoDismiss = true;
 				}
 				this._positionDropdown();
diff --git a/modules/orionode/lib/file.js b/modules/orionode/lib/file.js
index d249b30..a2209bc 100644
--- a/modules/orionode/lib/file.js
+++ b/modules/orionode/lib/file.js
@@ -61,27 +61,7 @@
 	function getSafeFilePath(res, wwwpath) {
 		return fileUtil.safeFilePath(workspaceDir, wwwpath);
 	}
-
-	function getParents(filepath, wwwpath) {
-		var segs = wwwpath.split('/');
-		if(segs && segs.length > 0 && segs[segs.length-1] === ""){// pop the last segment if it is empty. In this case wwwpath ends with "/".
-			segs.pop();
-		}
-		segs.pop();//The last segment now is the directory itself. We do not need it in the parents array.
-		var loc = fileRoot;
-		var parents = [];
-		for (var i=0; i < segs.length; i++) {
-			var seg = segs[i];
-			loc = api.join(loc, seg);
-			parents.push({
-				Name: decodeURIComponent(seg),
-				ChildrenLocation: loc + '?depth=1', 
-				Location: loc
-			});
-		}
-		return parents.reverse();
-	}
-
+	
 	function writeFileMetadata(res, rest, filepath, stats, etag, includeChildren) {
 		var isDir = stats.isDirectory();
 		var metaObj = {
@@ -89,7 +69,7 @@
 			Location: api.join(fileRoot, rest) + (isDir ? '/' : ''),
 			Directory: isDir,
 			LocalTimeStamp: stats.mtime.getTime(),
-			Parents: getParents(filepath, rest),
+			Parents: fileUtil.getParents(fileRoot, rest),
 			//Charset: "UTF-8",
 			Attributes: {
 				// TODO fix this
diff --git a/modules/orionode/lib/fileUtil.js b/modules/orionode/lib/fileUtil.js
index 778c2d0..807af5b 100644
--- a/modules/orionode/lib/fileUtil.js
+++ b/modules/orionode/lib/fileUtil.js
@@ -112,6 +112,26 @@
 		});
 };
 
+exports.getParents = function(fileRoot, relativePath) {
+	var segs = relativePath.split('/');
+	if(segs && segs.length > 0 && segs[segs.length-1] === ""){// pop the last segment if it is empty. In this case wwwpath ends with "/".
+		segs.pop();
+	}
+	segs.pop();//The last segment now is the directory itself. We do not need it in the parents array.
+	var loc = fileRoot;
+	var parents = [];
+	for (var i=0; i < segs.length; i++) {
+		var seg = segs[i];
+		loc = api.join(loc, seg);
+		parents.push({
+			Name: decodeURIComponent(seg),
+			ChildrenLocation: loc + '?depth=1', 
+			Location: loc
+		});
+	}
+	return parents.reverse();
+};
+
 /**
  * Performs the equivalent of rm -rf on a directory.
  * @param {Function} callback Invoked as callback(error)
diff --git a/modules/orionode/lib/node_app_socket.js b/modules/orionode/lib/node_app_socket.js
index c3e83fa..a515de1 100644
--- a/modules/orionode/lib/node_app_socket.js
+++ b/modules/orionode/lib/node_app_socket.js
@@ -62,16 +62,17 @@
 		var handshakeData = socket.handshake;
 		socket.on('npm', function(data) {
 			try {
-				var result = appContext.startNPM(data.args, data.context);
-				if(result.app){
-					pipeStreams(result.app, socket);
-					result.app.on('exit', function(c) {
-						socket.emit('stopped', result.app.toJson());
-					});
-					socket.emit('started', result.app.toJson());
-				} else if(result.error) {
-					socket.emit('stopped', {error: result.error});
-				}
+				appContext.startNPM(data.args, data.context).then (function (result) {
+					if(result.app){
+						pipeStreams(result.app, socket);
+						result.app.on('exit', function(c) {
+							socket.emit('stopped', result.app.toJson());
+						});
+						socket.emit('started', result.app.toJson());
+					} else if(result.error) {
+						socket.emit('stopped', {error: result.error});
+					}
+				});
 			} catch (error) {
 				console.log(error && error.stack);
 				emitError(socket, error);
diff --git a/modules/orionode/lib/node_apps.js b/modules/orionode/lib/node_apps.js
index 90e6450..b3e7cd9 100644
--- a/modules/orionode/lib/node_apps.js
+++ b/modules/orionode/lib/node_apps.js
@@ -15,6 +15,8 @@
 var path = require('path');
 var util = require('util');
 var url = require('url');
+var dfs = require('deferred-fs'), Deferred = dfs.Deferred;
+var async = require('./async');
 
 var api = require('./api');
 var fileUtil = require('./fileUtil');
@@ -145,8 +147,8 @@
 	 * @param {String} cwdPath The cwd, as a www path (eg. /file/whatever/myfolder)
 	 * @throws {Error} If modulePath is unsafe
 	 */
-	function _resolveCWD(fileRoot, workspaceDir, cwdPath) {
-		var filePath = api.rest(fileRoot, cwdPath);
+	function _resolveWWWPath(fileRoot, workspaceDir, wwwPath) {
+		var filePath = api.rest(fileRoot, wwwPath);
 		if(!filePath){
 			filePath = api.rest(fileRoot, fileRoot);
 		}
@@ -219,6 +221,43 @@
 		}
 	}.bind(this);
 	/**
+	 * @function
+	 * @private
+	 */
+	var _prepareNPMFolder = function(fileRoot, workspaceDir, cwdPath){
+		var relativePath = api.rest(fileRoot, cwdPath);
+		if(!relativePath){
+			relativePath = api.rest(fileRoot, fileRoot);
+		}
+		var dirlist = fileUtil.getParents(fileRoot, relativePath);
+		dirlist.unshift({Location: cwdPath});
+		var found = null;
+	    var promises = [];
+		dirlist.forEach(function(item) {
+			promises.push(function(){
+				if(!found){
+					item.Location = path.join(_resolveWWWPath(fileRoot, workspaceDir, item.Location), "node_modules");
+					return dfs.exists(item.Location).then(function(exists){
+						if(exists){
+							found = item.Location;
+						} 
+						return found;
+					});
+				} else {
+					return found;
+				}
+			});
+		});
+		return async.sequence(promises).then(function(existingPath){
+			if(!existingPath){
+				var absPath = dirlist[0].Location;
+				return dfs.mkdir(absPath);
+			} else {
+				return new Deferred().resolve();
+			}
+		});
+	}.bind(this);
+	/**
 	 * @name orionode.AppContext#startNPM
 	 * @function
 	 * @param {Array} [args] The args passed to NPM
@@ -228,13 +267,17 @@
 		var npmPath = configParams.npm_path;
 		var app = null;
 		if(npmPath){
-			var cwdPath = _resolveCWD(fileRoot, workspaceDir, context.cwd);
-			app = _startApp([npmPath].concat(args || []), cwdPath);
-			app.on('exit', function(code) {
-				console.log('App # ' + app.pid + ' exited, code=' + code);
-			});
+			return _prepareNPMFolder(fileRoot, workspaceDir, context.cwd).then(function() {
+				var cwdPath = _resolveWWWPath(fileRoot, workspaceDir, context.cwd);
+				app = _startApp([npmPath].concat(args || []), cwdPath);
+				app.on('exit', function(code) {
+					console.log('App # ' + app.pid + ' exited, code=' + code);
+				});
+				return new Deferred().resolve({app: app, error: configParams.npm_error_message});
+			}.bind(this));
+		} else {
+			return new Deferred().resolve({app: app, error: configParams.npm_error_message});
 		}
-		return {app: app, error: configParams.npm_error_message};
 	};
 	/**
 	 * @name orionode.AppContext#startApp
@@ -245,7 +288,7 @@
 	 * @param {Boolean} [hidden]
 	 */
 	this.startApp = function(modulePath, args, context, hidden) {
-		var cwdPath = _resolveCWD(fileRoot, workspaceDir, context.cwd);
+		var cwdPath = _resolveWWWPath(fileRoot, workspaceDir, context.cwd);
 		modulePath = resolveModulePath(workspaceDir, cwdPath, modulePath);
 		var app = _startApp([modulePath].concat(args || []), cwdPath, hidden);
 		app.on('exit', function(code) {
@@ -259,7 +302,7 @@
 	 * @param {Number} port
 	 */
 	this.debugApp = function(modulePath, port, args, context, headers, requestUrl) {
-		var cwdPath = _resolveCWD(fileRoot, workspaceDir, context.cwd);
+		var cwdPath = _resolveWWWPath(fileRoot, workspaceDir, context.cwd);
 		var resolvedPath = resolveModulePath(workspaceDir, cwdPath, modulePath);
 		var app = _startApp(["--debug-brk=" + port, resolvedPath].concat(args || []), cwdPath);
 		var parsedRequestUrl = url.parse(requestUrl);