merge m-c to fx-team
authorTim Taubert <tim.taubert@gmx.de>
Fri, 09 Dec 2011 05:57:05 +0100
changeset 84256 9e7239c0f557ddafdca5bcf9598294c52f42d08d
parent 84243 5c8405e6226eda1629be41b51a044eab8ac52740 (current diff)
parent 84255 06c0a1711186a5e6945df9751300720a4e75f247 (diff)
child 84257 141fe205fb73cbbf0263bdd44bc61a46ca51f032
child 84976 5707dd62841423a9bf2428f15f96b870b2d52f95
push id114
push userffxbld
push dateFri, 09 Mar 2012 01:01:18 +0000
treeherdermozilla-release@c081ebf13261 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone11.0a1
first release with
nightly linux32
9e7239c0f557 / 11.0a1 / 20111209031218 / files
nightly linux64
9e7239c0f557 / 11.0a1 / 20111209031218 / files
nightly mac
9e7239c0f557 / 11.0a1 / 20111209031218 / files
nightly win32
9e7239c0f557 / 11.0a1 / 20111209031218 / files
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
merge m-c to fx-team
--- a/browser/components/tabview/test/Makefile.in
+++ b/browser/components/tabview/test/Makefile.in
@@ -164,16 +164,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_tabview_bug685476.js \
                  browser_tabview_bug685692.js \
                  browser_tabview_bug686654.js \
                  browser_tabview_bug696602.js \
                  browser_tabview_bug697390.js \
                  browser_tabview_bug705621.js \
                  browser_tabview_bug706430.js \
                  browser_tabview_bug706736.js \
+                 browser_tabview_bug707466.js \
                  browser_tabview_click_group.js \
                  browser_tabview_dragdrop.js \
                  browser_tabview_exit_button.js \
                  browser_tabview_expander.js \
                  browser_tabview_firstrun_pref.js \
                  browser_tabview_group.js \
                  browser_tabview_launch.js \
                  browser_tabview_multiwindow_search.js \
new file mode 100644
--- /dev/null
+++ b/browser/components/tabview/test/browser_tabview_bug707466.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  waitForExplicitFinish();
+
+  // create two groups and each group has one tab item
+  let newState = {
+    windows: [{
+      tabs: [{
+        entries: [{ url: "about:blank" }],
+        hidden: true,
+        attributes: {},
+        extData: {
+          "tabview-tab":
+            '{"bounds":{"left":21,"top":29,"width":204,"height":153},' +
+            '"userSize":null,"url":"about:blank","groupID":1,' +
+            '"imageData":null,"title":null}'
+        }
+      },{
+        entries: [{ url: "about:blank" }],
+        hidden: false,
+        attributes: {},
+        extData: {
+          "tabview-tab":
+            '{"bounds":{"left":315,"top":29,"width":111,"height":84},' +
+            '"userSize":null,"url":"about:blank","groupID":2,' +
+            '"imageData":null,"title":null}'
+        },
+      }],
+      selected:2,
+      _closedTabs: [],
+      extData: {
+        "tabview-groups": '{"nextID":3,"activeGroupId":2}',
+        "tabview-group":
+          '{"1":{"bounds":{"left":15,"top":5,"width":280,"height":232},' +
+          '"userSize":null,"title":"","id":1},' +
+          '"2":{"bounds":{"left":309,"top":5,"width":267,"height":226},' +
+          '"userSize":null,"title":"","id":2}}',
+        "tabview-ui": '{"pageBounds":{"left":0,"top":0,"width":788,"height":548}}'
+      }, sizemode:"normal"
+    }]
+  };
+
+  newWindowWithState(newState, function(win) {
+    registerCleanupFunction(function () win.close());
+
+    whenTabViewIsShown(function() {
+      let cw = win.TabView.getContentWindow();
+
+      is(cw.GroupItems.groupItems.length, 2, "There are still two groups");
+      is(win.gBrowser.tabs.length, 1, "There is only one tab");
+      is(cw.UI.getActiveTab(), win.gBrowser.selectedTab._tabViewTabItem, "The last tab is selected");
+
+      finish();
+    }, win);
+    win.gBrowser.removeTab(win.gBrowser.selectedTab);
+  });
+}
+
--- a/browser/components/tabview/ui.js
+++ b/browser/components/tabview/ui.js
@@ -470,17 +470,17 @@ let UI = {
     if (item.isATabItem) {
       if (item.parent)
         GroupItems.setActiveGroupItem(item.parent);
       if (!options || !options.dontSetActiveTabInGroup)
         this._setActiveTab(item);
     } else {
       GroupItems.setActiveGroupItem(item);
       if (!options || !options.dontSetActiveTabInGroup) {
-        let activeTab = item.getActiveTab()
+        let activeTab = item.getActiveTab();
         if (activeTab)
           this._setActiveTab(activeTab);
       }
     }
   },
 
   // ----------
   // Function: clearActiveTab
@@ -569,17 +569,18 @@ let UI = {
         dispatchEvent(event);
 
         // Flush pending updates
         GroupItems.flushAppTabUpdates();
 
         TabItems.resumePainting();
       });
     } else {
-      self.clearActiveTab();
+      if (!currentTab || !currentTab._tabViewTabItem)
+        self.clearActiveTab();
       self._isChangingVisibility = false;
       dispatchEvent(event);
 
       // Flush pending updates
       GroupItems.flushAppTabUpdates();
 
       TabItems.resumePainting();
     }
--- a/browser/devtools/shared/Templater.jsm
+++ b/browser/devtools/shared/Templater.jsm
@@ -32,31 +32,72 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 
-var EXPORTED_SYMBOLS = [ "Templater" ];
+var EXPORTED_SYMBOLS = [ "Templater", "template" ];
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 const Node = Components.interfaces.nsIDOMNode;
 
 // WARNING: do not 'use_strict' without reading the notes in _envEval();
 
 /**
- * A templater that allows one to quickly template DOM nodes.
+ * Begin a new templating process.
+ * @param node A DOM element or string referring to an element's id
+ * @param data Data to use in filling out the template
+ * @param options Options to customize the template processing. One of:
+ * - allowEval: boolean (default false) Basic template interpolations are
+ * either property paths (e.g. ${a.b.c.d}), however if allowEval=true then we
+ * allow arbitrary JavaScript
  */
-function Templater() {
+function template(node, data, options) {
+  var template = new Templater(options || {});
+  template.processNode(node, data);
+  return template;
+}
+
+/**
+ * Construct a Templater object. Use template() in preference to this ctor.
+ * @deprecated Use template(node, data, options);
+ */
+function Templater(options) {
+  if (options == null) {
+    options = { allowEval: true };
+  }
+  this.options = options;
   this.stack = [];
 }
 
 /**
+ * Cached regex used to find ${...} sections in some text.
+ * Performance note: This regex uses ( and ) to capture the 'script' for
+ * further processing. Not all of the uses of this regex use this feature so
+ * if use of the capturing group is a performance drain then we should split
+ * this regex in two.
+ */
+Templater.prototype._templateRegion = /\$\{([^}]*)\}/g;
+
+/**
+ * Cached regex used to split a string using the unicode chars F001 and F002.
+ * See Templater._processTextNode() for details.
+ */
+Templater.prototype._splitSpecial = /\uF001|\uF002/;
+
+/**
+ * Cached regex used to detect if a script is capable of being interpreted
+ * using Template._property() or if we need to use Template._envEval()
+ */
+Templater.prototype._isPropertyScript = /^[a-zA-Z0-9.]*$/;
+
+/**
  * Recursive function to walk the tree processing the attributes as it goes.
  * @param node the node to process. If you pass a string in instead of a DOM
  * element, it is assumed to be an id for use with document.getElementById()
  * @param data the data to use for node processing.
  */
 Templater.prototype.processNode = function(node, data) {
   if (typeof node === 'string') {
     node = document.getElementById(node);
@@ -106,17 +147,17 @@ Templater.prototype.processNode = functi
             node.removeAttribute(name);
             var capture = node.hasAttribute('capture' + name.substring(2));
             node.addEventListener(name.substring(2), func, capture);
             if (capture) {
               node.removeAttribute('capture' + name.substring(2));
             }
           } else {
             // Replace references in all other attributes
-            var newValue = value.replace(/\$\{[^}]*\}/g, function(path) {
+            var newValue = value.replace(this._templateRegion, function(path) {
               return this._envEval(path.slice(2, -1), data, value);
             }.bind(this));
             // Remove '_' prefix of attribute names so the DOM won't try
             // to use them before we've processed the template
             if (name.charAt(0) === '_') {
               node.removeAttribute(name);
               node.setAttribute(name.substring(1), newValue);
             } else if (value !== newValue) {
@@ -290,18 +331,18 @@ Templater.prototype._processTextNode = f
   // attribute processing in processNode()) because we need to support
   // functions that return DOM nodes, so we can't have the conversion to a
   // string.
   // Instead we process the string as an array of parts. In order to split
   // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
   // We can then split using \uF001 or \uF002 to get an array of strings
   // where scripts are prefixed with $.
   // \uF001 and \uF002 are just unicode chars reserved for private use.
-  value = value.replace(/\$\{([^}]*)\}/g, '\uF001$$$1\uF002');
-  var parts = value.split(/\uF001|\uF002/);
+  value = value.replace(this._templateRegion, '\uF001$$$1\uF002');
+  var parts = value.split(this._splitSpecial);
   if (parts.length > 1) {
     parts.forEach(function(part) {
       if (part === null || part === undefined || part === '') {
         return;
       }
       if (part.charAt(0) === '$') {
         part = this._envEval(part.slice(1), data, node.data);
       }
@@ -358,17 +399,17 @@ Templater.prototype._handleAsync = funct
 };
 
 /**
  * Warn of string does not begin '${' and end '}'
  * @param str the string to check.
  * @return The string stripped of ${ and }, or untouched if it does not match
  */
 Templater.prototype._stripBraces = function(str) {
-  if (!str.match(/\$\{.*\}/g)) {
+  if (!str.match(this._templateRegion)) {
     this._handleError('Expected ' + str + ' to match ${...}');
     return str;
   }
   return str.slice(2, -1);
 };
 
 /**
  * Combined getter and setter that works with a path through some data set.
@@ -422,27 +463,36 @@ Templater.prototype._property = function
  * the values in the env object. This is likely to be slow, but workable.
  * @param script The string to be evaluated.
  * @param data The environment in which to eval the script.
  * @param frame Optional debugging string in case of failure.
  * @return The return value of the script, or the error message if the script
  * execution failed.
  */
 Templater.prototype._envEval = function(script, data, frame) {
-  with (data) {
-    try {
-      this.stack.push(frame);
-      return eval(script);
-    } catch (ex) {
-      this._handleError('Template error evaluating \'' + script + '\'' +
-          ' environment=' + Object.keys(data).join(', '), ex);
-      return script;
-    } finally {
-      this.stack.pop();
+  try {
+    this.stack.push(frame);
+    if (this._isPropertyScript.test(script)) {
+      return this._property(script, data);
+    } else {
+      if (!this.options.allowEval) {
+        this._handleError('allowEval is not set, however \'' + script + '\'' +
+            ' can not be resolved using a simple property path.');
+        return '${' + script + '}';
+      }
+      with (data) {
+        return eval(script);
+      }
     }
+  } catch (ex) {
+    this._handleError('Template error evaluating \'' + script + '\'' +
+        ' environment=' + Object.keys(data).join(', '), ex);
+    return '${' + script + '}';
+  } finally {
+    this.stack.pop();
   }
 };
 
 /**
  * A generic way of reporting errors, for easy overloading in different
  * environments.
  * @param message the error message to report.
  * @param ex optional associated exception.
--- a/browser/devtools/shared/test/browser_templater_basic.js
+++ b/browser/devtools/shared/test/browser_templater_basic.js
@@ -17,17 +17,17 @@ function runTest(index) {
   var options = tests[index] = tests[index]();
   var holder = content.document.createElement('div');
   holder.id = options.name;
   var body = content.document.body;
   body.appendChild(holder);
   holder.innerHTML = options.template;
 
   info('Running ' + options.name);
-  new Templater().processNode(holder, options.data);
+  template(holder, options.data, options.options);
 
   if (typeof options.result == 'string') {
     is(holder.innerHTML, options.result, options.name);
   }
   else {
     ok(holder.innerHTML.match(options.result), options.name);
   }
 
@@ -83,55 +83,60 @@ var tests = [
     template: '<div id="ex1">${nested.value}</div>',
     data: { nested:{ value:'pass 1' } },
     result: '<div id="ex1">pass 1</div>'
   };},
 
   function() { return {
     name: 'returnDom',
     template: '<div id="ex2">${__element.ownerDocument.createTextNode(\'pass 2\')}</div>',
+    options: { allowEval: true },
     data: {},
     result: '<div id="ex2">pass 2</div>'
   };},
 
   function() { return {
     name: 'srcChange',
     template: '<img _src="${fred}" id="ex3">',
     data: { fred:'green.png' },
     result: /<img( id="ex3")? src="green.png"( id="ex3")?>/
   };},
 
   function() { return {
     name: 'ifTrue',
     template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+    options: { allowEval: true },
     data: { name: 'fred' },
     result: '<p>hello fred</p>'
   };},
 
   function() { return {
     name: 'ifFalse',
     template: '<p if="${name !== \'jim\'}">hello ${name}</p>',
+    options: { allowEval: true },
     data: { name: 'jim' },
     result: ''
   };},
 
   function() { return {
     name: 'simpleLoop',
     template: '<p foreach="index in ${[ 1, 2, 3 ]}">${index}</p>',
+    options: { allowEval: true },
     data: {},
     result: '<p>1</p><p>2</p><p>3</p>'
   };},
 
   function() { return {
     name: 'loopElement',
     template: '<loop foreach="i in ${array}">${i}</loop>',
     data: { array: [ 1, 2, 3 ] },
     result: '123'
   };},
 
+  // Bug 692028: DOMTemplate memory leak with asynchronous arrays
   // Bug 692031: DOMTemplate async loops do not drop the loop element
   function() { return {
     name: 'asyncLoopElement',
     template: '<loop foreach="i in ${array}">${i}</loop>',
     data: { array: delayReply([1, 2, 3]) },
     result: '<span></span>',
     later: '123'
   };},
@@ -145,16 +150,17 @@ var tests = [
       ok(options.data.element.innerHTML, 'pass 9', 'saveElement saved');
       delete options.data.element;
     }
   };},
 
   function() { return {
     name: 'useElement',
     template: '<p id="pass9">${adjust(__element)}</p>',
+    options: { allowEval: true },
     data: {
       adjust: function(element) {
         is('pass9', element.id, 'useElement adjust');
         return 'pass 9b'
       }
     },
     result: '<p id="pass9">pass 9b</p>'
   };},
@@ -162,44 +168,78 @@ var tests = [
   function() { return {
     name: 'asyncInline',
     template: '${delayed}',
     data: { delayed: delayReply('inline') },
     result: '<span></span>',
     later: 'inline'
   };},
 
+  // Bug 692028: DOMTemplate memory leak with asynchronous arrays
   function() { return {
     name: 'asyncArray',
     template: '<p foreach="i in ${delayed}">${i}</p>',
     data: { delayed: delayReply([1, 2, 3]) },
     result: '<span></span>',
     later: '<p>1</p><p>2</p><p>3</p>'
   };},
 
   function() { return {
     name: 'asyncMember',
     template: '<p foreach="i in ${delayed}">${i}</p>',
     data: { delayed: [delayReply(4), delayReply(5), delayReply(6)] },
     result: '<span></span><span></span><span></span>',
     later: '<p>4</p><p>5</p><p>6</p>'
   };},
 
+  // Bug 692028: DOMTemplate memory leak with asynchronous arrays
   function() { return {
     name: 'asyncBoth',
     template: '<p foreach="i in ${delayed}">${i}</p>',
     data: {
       delayed: delayReply([
         delayReply(4),
         delayReply(5),
         delayReply(6)
       ])
     },
     result: '<span></span>',
     later: '<p>4</p><p>5</p><p>6</p>'
+  };},
+
+  // Bug 701762: DOMTemplate fails when ${foo()} returns undefined
+  function() { return {
+    name: 'functionReturningUndefiend',
+    template: '<p>${foo()}</p>',
+    options: { allowEval: true },
+    data: {
+      foo: function() {}
+    },
+    result: '<p>undefined</p>'
+  };},
+
+  // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${}
+  function() { return {
+    name: 'propertySimple',
+    template: '<p>${a.b.c}</p>',
+    data: { a: { b: { c: 'hello' } } },
+    result: '<p>hello</p>'
+  };},
+
+  function() { return {
+    name: 'propertyPass',
+    template: '<p>${Math.max(1, 2)}</p>',
+    options: { allowEval: true },
+    result: '<p>2</p>'
+  };},
+
+  function() { return {
+    name: 'propertyFail',
+    template: '<p>${Math.max(1, 2)}</p>',
+    result: '<p>${Math.max(1, 2)}</p>'
   };}
 ];
 
 function delayReply(data) {
   var p = new Promise();
   executeSoon(function() {
     p.resolve(data);
   });
--- a/browser/devtools/styleinspector/CssHtmlTree.jsm
+++ b/browser/devtools/styleinspector/CssHtmlTree.jsm
@@ -209,17 +209,20 @@ CssHtmlTree.processTemplate = function C
 {
   if (!aPreserveDestination) {
     aDestination.innerHTML = "";
   }
 
   // All the templater does is to populate a given DOM tree with the given
   // values, so we need to clone the template first.
   let duplicated = aTemplate.cloneNode(true);
-  new Templater().processNode(duplicated, aData);
+
+  // See https://github.com/mozilla/domtemplate/blob/master/README.md
+  // for docs on the template() function
+  template(duplicated, aData, { allowEval: true });
   while (duplicated.firstChild) {
     aDestination.appendChild(duplicated.firstChild);
   }
 };
 
 XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
         .createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
 
--- a/browser/devtools/webconsole/GcliCommands.jsm
+++ b/browser/devtools/webconsole/GcliCommands.jsm
@@ -57,50 +57,16 @@ gcli.addCommand({
   ],
   returnType: "string",
   exec: function Command_echo(args, context) {
     return args.message;
   }
 });
 
 
-let canon = gcli._internal.require("gcli/canon");
-
-/**
- * 'help' command
- */
-gcli.addCommand({
-  name: "help",
-  returnType: "html",
-  description: gcli.lookup("helpDesc"),
-  exec: function Command_help(args, context) {
-    let output = [];
-
-    output.push("<strong>" + gcli.lookup("helpAvailable") + ":</strong><br/>");
-
-    let commandNames = canon.getCommandNames();
-    commandNames.sort();
-
-    output.push("<table>");
-    for (let i = 0; i < commandNames.length; i++) {
-      let command = canon.getCommand(commandNames[i]);
-      if (!command.hidden && command.description) {
-        output.push("<tr>");
-        output.push('<th class="gcli-help-right">' + command.name + "</th>");
-        output.push("<td>&#x2192; " + command.description + "</td>");
-        output.push("</tr>");
-      }
-    }
-    output.push("</table>");
-
-    return output.join("");
-  }
-});
-
-
 /**
  * 'console' command
  */
 gcli.addCommand({
   name: "console",
   description: gcli.lookup("consoleDesc"),
   manual: gcli.lookup("consoleManual")
 });
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -87,16 +87,22 @@ XPCOMUtils.defineLazyGetter(this, "CssRu
 });
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function () {
   var obj = {};
   Cu.import("resource://gre/modules/NetUtil.jsm", obj);
   return obj.NetUtil;
 });
 
+XPCOMUtils.defineLazyGetter(this, "template", function () {
+  var obj = {};
+  Cu.import("resource:///modules/devtools/Templater.jsm", obj);
+  return obj.template;
+});
+
 XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () {
   var obj = {};
   try {
     Cu.import("resource:///modules/PropertyPanel.jsm", obj);
   } catch (err) {
     Cu.reportError(err);
   }
   return obj.PropertyPanel;
@@ -6849,24 +6855,48 @@ GcliTerm.prototype = {
     this.writeOutput(aEvent.output.typed, { category: CATEGORY_INPUT });
 
     if (aEvent.output.output == null) {
       return;
     }
 
     let output = aEvent.output.output;
     if (aEvent.output.command.returnType == "html" && typeof output == "string") {
-      let frag = this.document.createRange().createContextualFragment(
+      output = this.document.createRange().createContextualFragment(
           '<div xmlns="' + HTML_NS + '" xmlns:xul="' + XUL_NS + '">' +
-          output + '</div>');
-
-      output = this.document.createElementNS(HTML_NS, "div");
-      output.appendChild(frag);
-    }
-    this.writeOutput(output);
+          output + '</div>').firstChild;
+    }
+
+    // See https://github.com/mozilla/domtemplate/blob/master/README.md
+    // for docs on the template() function
+    let element = this.document.createRange().createContextualFragment(
+      '<richlistitem xmlns="' + XUL_NS + '" clipboardText="${clipboardText}"' +
+      '    timestamp="${timestamp}" id="${id}" class="hud-msg-node">' +
+      '  <label class="webconsole-timestamp" value="${timestampString}"/>' +
+      '  <vbox class="webconsole-msg-icon-container" style="${iconContainerStyle}">' +
+      '    <image class="webconsole-msg-icon"/>' +
+      '    <spacer flex="1"/>' +
+      '  </vbox>' +
+      '  <hbox flex="1" class="gcliterm-msg-body">${output}</hbox>' +
+      '  <hbox align="start"><label value="1" class="webconsole-msg-repeat"/></hbox>' +
+      '</richlistitem>').firstChild;
+
+    let hud = HUDService.getHudReferenceById(this.hudId);
+    let timestamp = ConsoleUtils.timestamp();
+    template(element, {
+      iconContainerStyle: "margin-left=" + (hud.groupDepth * GROUP_INDENT) + "px",
+      output: output,
+      timestamp: timestamp,
+      timestampString: ConsoleUtils.timestampString(timestamp),
+      clipboardText: output.innerText,
+      id: "console-msg-" + HUDService.sequenceId()
+    });
+
+    ConsoleUtils.setMessageType(element, CATEGORY_OUTPUT, SEVERITY_LOG);
+    ConsoleUtils.outputMessageNode(element, this.hudId);
   },
 
   /**
    * Setup the eval sandbox, should be called whenever we are attached.
    */
   createSandbox: function Gcli_createSandbox()
   {
     let win = this.context.get().QueryInterface(Ci.nsIDOMWindow);
--- a/browser/devtools/webconsole/gcli.jsm
+++ b/browser/devtools/webconsole/gcli.jsm
@@ -681,32 +681,33 @@ var mozl10n = {};
     }
     catch (ex) {
       throw new Error("Failure in lookupFormat('" + name + "')");
     }
   };
 
 })(mozl10n);
 
-define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/types/node', 'gcli/cli', 'gcli/ui/display'], function(require, exports, module) {
+define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/types/node', 'gcli/cli', 'gcli/commands/help', 'gcli/ui/console'], function(require, exports, module) {
 
   // The API for use by command authors
   exports.addCommand = require('gcli/canon').addCommand;
   exports.removeCommand = require('gcli/canon').removeCommand;
   exports.lookup = mozl10n.lookup;
   exports.lookupFormat = mozl10n.lookupFormat;
 
   // Internal startup process. Not exported
   require('gcli/types/basic').startup();
   require('gcli/types/javascript').startup();
   require('gcli/types/node').startup();
   require('gcli/cli').startup();
+  require('gcli/commands/help').startup();
 
   var Requisition = require('gcli/cli').Requisition;
-  var Display = require('gcli/ui/display').Display;
+  var Console = require('gcli/ui/console').Console;
 
   var cli = require('gcli/cli');
   var jstype = require('gcli/types/javascript');
   var nodetype = require('gcli/types/node');
 
   /**
    * API for use by HUDService only.
    * This code is internal and subject to change without notice.
@@ -734,25 +735,25 @@ define('gcli/index', ['require', 'export
       jstype.setGlobalObject(opts.jsEnvironment.globalObject);
       nodetype.setDocument(opts.contentDocument);
       cli.setEvalFunction(opts.jsEnvironment.evalFunction);
 
       if (opts.requisition == null) {
         opts.requisition = new Requisition(opts.environment, opts.chromeDocument);
       }
 
-      opts.display = new Display(opts);
+      opts.console = new Console(opts);
     },
 
     /**
      * Undo the effects of createView() to prevent memory leaks
      */
     removeView: function(opts) {
-      opts.display.destroy();
-      delete opts.display;
+      opts.console.destroy();
+      delete opts.console;
 
       opts.requisition.destroy();
       delete opts.requisition;
 
       cli.unsetEvalFunction();
       nodetype.unsetDocument();
       jstype.unsetGlobalObject();
     },
@@ -1024,17 +1025,18 @@ canon.removeCommand = function removeCom
   canon.canonChange();
 };
 
 /**
  * Retrieve a command by name
  * @param name The name of the command to retrieve
  */
 canon.getCommand = function getCommand(name) {
-  return commands[name];
+  // '|| undefined' is to silence 'reference to undefined property' warnings
+  return commands[name] || undefined;
 };
 
 /**
  * Get an array of all the registered commands.
  */
 canon.getCommands = function getCommands() {
   // return Object.values(commands);
   return Object.keys(commands).map(function(name) {
@@ -1185,19 +1187,27 @@ exports.createEvent = function(name) {
   return event;
 };
 
 
 //------------------------------------------------------------------------------
 
 var dom = {};
 
+/**
+ * XHTML namespace
+ */
 dom.NS_XHTML = 'http://www.w3.org/1999/xhtml';
 
 /**
+ * XUL namespace
+ */
+dom.NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+
+/**
  * Create an HTML or XHTML element depending on whether the document is HTML
  * or XML based. Where HTML/XHTML elements are distinguished by whether they
  * are created using doc.createElementNS('http://www.w3.org/1999/xhtml', tag)
  * or doc.createElement(tag)
  * If you want to create a XUL element then you don't have a problem knowing
  * what namespace you want.
  * @param doc The document in which to create the element
  * @param tag The name of the tag to create
@@ -1241,34 +1251,40 @@ dom.importCss = function(cssText, doc) {
 };
 
 /**
  * There are problems with innerHTML on XML documents, so we need to do a dance
  * using document.createRange().createContextualFragment() when in XML mode
  */
 dom.setInnerHtml = function(elem, html) {
   if (dom.isXmlDocument(elem.ownerDocument)) {
-    dom.clearElement(elem);
-    html = '<div xmlns="' + dom.NS_XHTML + '">' + html + '</div>';
-    var range = elem.ownerDocument.createRange();
-    var child = range.createContextualFragment(html).childNodes[0];
-    while (child.hasChildNodes()) {
-      elem.appendChild(child.firstChild);
+    try {
+      dom.clearElement(elem);
+      html = '<div xmlns="' + dom.NS_XHTML + '">' + html + '</div>';
+      var range = elem.ownerDocument.createRange();
+      var child = range.createContextualFragment(html).firstChild;
+      while (child.hasChildNodes()) {
+        elem.appendChild(child.firstChild);
+      }
+    }
+    catch (ex) {
+      console.error('Bad XHTML', ex);
+      console.trace();
+      throw ex;
     }
   }
   else {
     elem.innerHTML = html;
   }
 };
 
 /**
- * How to detect if we're in an XUL document (and therefore should create
- * elements in an XHTML namespace)
- * In a Mozilla XUL document, document.xmlVersion = null, however in Chrome
- * document.contentType = undefined.
+ * How to detect if we're in an XML document.
+ * In a Mozilla we check that document.xmlVersion = null, however in Chrome
+ * we use document.contentType = undefined.
  * @param doc The document element to work from (defaulted to the global
  * 'document' if missing
  */
 dom.isXmlDocument = function(doc) {
   doc = doc || document;
   // Best test for Firefox
   if (doc.contentType && doc.contentType != 'text/html') {
     return true;
@@ -1474,16 +1490,23 @@ exports.lookup = function(key) {
     return stringBundle.GetStringFromName(key);
   }
   catch (ex) {
     console.error('Failed to lookup ', key, ex);
     return key;
   }
 };
 
+/** @see propertyLookup in lib/gcli/l10n.js */
+exports.propertyLookup = Proxy.create({
+  get: function(rcvr, name) {
+    return exports.lookup(name);
+  }
+});
+
 /** @see lookupFormat in lib/gcli/l10n.js */
 exports.lookupFormat = function(key, swaps) {
   try {
     return stringBundle.formatStringFromName(key, swaps, swaps.length);
   }
   catch (ex) {
     console.error('Failed to format ', key, ex);
     return key;
@@ -3457,16 +3480,24 @@ exports.setDocument = function(document)
 
 /**
  * Undo the effects of setDocument()
  */
 exports.unsetDocument = function() {
   doc = undefined;
 };
 
+/**
+ * Getter for the document that contains the nodes we're matching
+ * Most for changing things back to how they were for unit testing
+ */
+exports.getDocument = function() {
+  return doc;
+};
+
 
 /**
  * A CSS expression that refers to a single node
  */
 function NodeType(typeSpec) {
   if (typeSpec != null) {
     throw new Error('NodeType can not be customized');
   }
@@ -4037,17 +4068,25 @@ UnassignedAssignment.prototype.setUnassi
  * ExecutionContext.
  * @param doc A DOM Document passed to commands using ExecutionContext in
  * order to allow creation of DOM nodes. If missing Requisition will use the
  * global 'document'.
  * @constructor
  */
 function Requisition(environment, doc) {
   this.environment = environment;
-  this.document = doc || document;
+  this.document = doc;
+  if (this.document == null) {
+    try {
+      this.document = document;
+    }
+    catch (ex) {
+      // Ignore
+    }
+  }
 
   // The command that we are about to execute.
   // @see setCommandConversion()
   this.commandAssignment = new CommandAssignment();
 
   // The object that stores of Assignment objects that we are filling out.
   // The Assignment objects are stored under their param.name for named
   // lookup. Note: We make use of the property of Javascript objects that
@@ -4503,17 +4542,18 @@ Requisition.prototype.exec = function(in
 
   if (!command) {
     return false;
   }
 
   var outputObject = {
     command: command,
     args: args,
-    typed: this.toCanonicalString(),
+    typed: this.toString(),
+    canonical: this.toCanonicalString(),
     completed: false,
     start: new Date()
   };
 
   this.commandOutputManager.sendCommandOutput(outputObject);
 
   var onComplete = (function(output, error) {
     if (visible) {
@@ -4522,17 +4562,17 @@ Requisition.prototype.exec = function(in
       outputObject.error = error;
       outputObject.output = output;
       outputObject.completed = true;
       this.commandOutputManager.sendCommandOutput(outputObject);
     }
   }).bind(this);
 
   try {
-    var context = new ExecutionContext(this.environment, this.document);
+    var context = new ExecutionContext(this);
     var reply = command.exec(args, context);
 
     if (reply != null && reply.isPromise) {
       reply.then(
         function(data) { onComplete(data, false); },
         function(error) { onComplete(error, true); });
 
       // Add progress to our promise and add a handler for it here
@@ -5007,19 +5047,20 @@ Requisition.prototype._assign = function
 };
 
 exports.Requisition = Requisition;
 
 
 /**
  * Functions and data related to the execution of a command
  */
-function ExecutionContext(environment, document) {
-  this.environment = environment;
-  this.document = document;
+function ExecutionContext(requisition) {
+  this.requisition = requisition;
+  this.environment = requisition.environment;
+  this.document = requisition.document;
 }
 
 ExecutionContext.prototype.createPromise = function() {
   return new Promise();
 };
 
 
 });
@@ -5036,28 +5077,296 @@ define('gcli/promise', ['require', 'expo
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
-define('gcli/ui/display', ['require', 'exports', 'module' , 'gcli/ui/inputter', 'gcli/ui/arg_fetch', 'gcli/ui/menu', 'gcli/ui/focus'], function(require, exports, module) {
+define('gcli/commands/help', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/util', 'gcli/l10n', 'gcli/ui/domtemplate', 'text!gcli/commands/help.css', 'text!gcli/commands/help_intro.html', 'text!gcli/commands/help_list.html', 'text!gcli/commands/help_man.html'], function(require, exports, module) {
+var help = exports;
+
+
+var canon = require('gcli/canon');
+var util = require('gcli/util');
+var l10n = require('gcli/l10n');
+var domtemplate = require('gcli/ui/domtemplate');
+
+var helpCss = require('text!gcli/commands/help.css');
+var helpStyle = undefined;
+var helpIntroHtml = require('text!gcli/commands/help_intro.html');
+var helpIntroTemplate = undefined;
+var helpListHtml = require('text!gcli/commands/help_list.html');
+var helpListTemplate = undefined;
+var helpManHtml = require('text!gcli/commands/help_man.html');
+var helpManTemplate = undefined;
+
+/**
+ * 'help' command
+ * We delay definition of helpCommandSpec until help.startup() to ensure that
+ * the l10n strings have been loaded
+ */
+var helpCommandSpec;
+
+/**
+ * Registration and de-registration.
+ */
+help.startup = function() {
+
+  helpCommandSpec = {
+    name: 'help',
+    description: l10n.lookup('helpDesc'),
+    manual: l10n.lookup('helpManual'),
+    params: [
+      {
+        name: 'search',
+        type: 'string',
+        description: l10n.lookup('helpSearchDesc'),
+        manual: l10n.lookup('helpSearchManual'),
+        defaultValue: null
+      }
+    ],
+    returnType: 'html',
+
+    exec: function(args, context) {
+      help.onFirstUseStartup(context.document);
+
+      var match = canon.getCommand(args.search);
+      if (match) {
+        var clone = helpManTemplate.cloneNode(true);
+        domtemplate.template(clone, getManTemplateData(match, context),
+                { allowEval: true, stack: 'help_man.html' });
+        return clone;
+      }
+
+      var parent = util.dom.createElement(context.document, 'div');
+      if (!args.search) {
+        parent.appendChild(helpIntroTemplate.cloneNode(true));
+      }
+      parent.appendChild(helpListTemplate.cloneNode(true));
+      domtemplate.template(parent, getListTemplateData(args, context),
+              { allowEval: true, stack: 'help_intro.html | help_list.html' });
+      return parent;
+    }
+  };
+
+  canon.addCommand(helpCommandSpec);
+};
+
+help.shutdown = function() {
+  canon.removeCommand(helpCommandSpec);
+
+  helpListTemplate = undefined;
+  helpStyle.parentElement.removeChild(helpStyle);
+  helpStyle = undefined;
+};
+
+/**
+ * Called when the command is executed
+ */
+help.onFirstUseStartup = function(document) {
+  if (!helpIntroTemplate) {
+    helpIntroTemplate = util.dom.createElement(document, 'div');
+    util.dom.setInnerHtml(helpIntroTemplate, helpIntroHtml);
+  }
+  if (!helpListTemplate) {
+    helpListTemplate = util.dom.createElement(document, 'div');
+    util.dom.setInnerHtml(helpListTemplate, helpListHtml);
+  }
+  if (!helpManTemplate) {
+    helpManTemplate = util.dom.createElement(document, 'div');
+    util.dom.setInnerHtml(helpManTemplate, helpManHtml);
+  }
+  if (!helpStyle && helpCss != null) {
+    helpStyle = util.dom.importCss(helpCss, document);
+  }
+};
+
+/**
+ * Find an element within the passed element with the class gcli-help-command
+ * and update the requisition to contain this text.
+ */
+function updateCommand(element, context) {
+  context.requisition.update({
+    typed: element.querySelector('.gcli-help-command').textContent
+  });
+}
+
+/**
+ * Find an element within the passed element with the class gcli-help-command
+ * and execute this text.
+ */
+function executeCommand(element, context) {
+  context.requisition.exec({
+    visible: true,
+    typed: element.querySelector('.gcli-help-command').textContent
+  });
+}
+
+/**
+ * Create a block of data suitable to be passed to the help_list.html template
+ */
+function getListTemplateData(args, context) {
+  return {
+    l10n: l10n.propertyLookup,
+    lang: context.document.defaultView.navigator.language,
+
+    onclick: function(ev) {
+      updateCommand(ev.currentTarget, context);
+    },
+
+    ondblclick: function(ev) {
+      executeCommand(ev.currentTarget, context);
+    },
+
+    getHeading: function() {
+      return args.search == null ?
+              'Available Commands:' :
+              'Commands starting with \'' + args.search + '\':';
+    },
+
+    getMatchingCommands: function() {
+      var matching = canon.getCommands().filter(function(command) {
+        if (args.search && command.name.indexOf(args.search) !== 0) {
+          // Filtered out because they don't match the search
+          return false;
+        }
+        if (!args.search && command.name.indexOf(' ') != -1) {
+          // We don't show sub commands with plain 'help'
+          return false;
+        }
+        return true;
+      });
+      matching.sort();
+      return matching;
+    }
+  };
+}
+
+/**
+ * Create a block of data suitable to be passed to the help_man.html template
+ */
+function getManTemplateData(command, context) {
+  return {
+    l10n: l10n.propertyLookup,
+    lang: context.document.defaultView.navigator.language,
+
+    command: command,
+
+    onclick: function(ev) {
+      updateCommand(ev.currentTarget, context);
+    },
+
+    getTypeDescription: function(param) {
+      var input = '';
+      if (param.defaultValue === undefined) {
+        input = 'required';
+      }
+      else if (param.defaultValue === null) {
+        input = 'optional';
+      }
+      else {
+        input = param.defaultValue;
+      }
+      return '(' + param.type.name + ', ' + input + ')';
+    }
+  };
+}
+
+});
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+define('gcli/ui/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+  var obj = {};
+  Components.utils.import('resource:///modules/devtools/Templater.jsm', obj);
+  exports.template = obj.template;
+
+});
+define("text!gcli/commands/help.css", [], void 0);
+define("text!gcli/commands/help_intro.html", [], "\n" +
+  "<h2>${l10n.introHeader}</h2>\n" +
+  "\n" +
+  "<p>\n" +
+  "  <a target=\"_blank\" href=\"https://developer.mozilla.org/AppLinks/WebConsoleHelp?locale=${lang}\">\n" +
+  "    ${l10n.introBody}\n" +
+  "  </a>\n" +
+  "</p>\n" +
+  "");
+
+define("text!gcli/commands/help_list.html", [], "\n" +
+  "<h3>${getHeading()}</h3>\n" +
+  "\n" +
+  "<table>\n" +
+  "  <tr foreach=\"command in ${getMatchingCommands()}\"\n" +
+  "      onclick=\"${onclick}\" ondblclick=\"${ondblclick}\">\n" +
+  "    <th class=\"gcli-help-name\">${command.name}</th>\n" +
+  "    <td class=\"gcli-help-arrow\">&#x2192;</td>\n" +
+  "    <td>\n" +
+  "      ${command.description}\n" +
+  "      <span class=\"gcli-out-shortcut gcli-help-command\">help ${command.name}</span>\n" +
+  "    </td>\n" +
+  "  </tr>\n" +
+  "</table>\n" +
+  "");
+
+define("text!gcli/commands/help_man.html", [], "\n" +
+  "<h3>${command.name}</h3>\n" +
+  "\n" +
+  "<h4 class=\"gcli-help-header\">\n" +
+  "  ${l10n.helpManSynopsis}:\n" +
+  "  <span class=\"gcli-help-synopsis\" onclick=\"${onclick}\">\n" +
+  "    <span class=\"gcli-help-command\">${command.name}</span>\n" +
+  "    <span foreach=\"param in ${command.params}\">\n" +
+  "      ${param.defaultValue !== undefined ? '[' + param.name + ']' : param.name}\n" +
+  "    </span>\n" +
+  "  </span>\n" +
+  "</h4>\n" +
+  "\n" +
+  "<h4 class=\"gcli-help-header\">${l10n.helpManDescription}:</h4>\n" +
+  "\n" +
+  "<p class=\"gcli-help-description\">\n" +
+  "  ${command.manual || command.description}\n" +
+  "</p>\n" +
+  "\n" +
+  "<h4 class=\"gcli-help-header\">${l10n.helpManParameters}:</h4>\n" +
+  "\n" +
+  "<ul class=\"gcli-help-parameter\">\n" +
+  "  <li if=\"${command.params.length === 0}\">${l10n.helpManNone}</li>\n" +
+  "  <li foreach=\"param in ${command.params}\">\n" +
+  "    <tt>${param.name}</tt> ${getTypeDescription(param)}\n" +
+  "    <br/>\n" +
+  "    ${param.manual || param.description}\n" +
+  "  </li>\n" +
+  "</ul>\n" +
+  "");
+
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+define('gcli/ui/console', ['require', 'exports', 'module' , 'gcli/ui/inputter', 'gcli/ui/arg_fetch', 'gcli/ui/menu', 'gcli/ui/focus'], function(require, exports, module) {
 
 var Inputter = require('gcli/ui/inputter').Inputter;
 var ArgFetcher = require('gcli/ui/arg_fetch').ArgFetcher;
 var CommandMenu = require('gcli/ui/menu').CommandMenu;
 var FocusManager = require('gcli/ui/focus').FocusManager;
 
 /**
- * Display is responsible for generating the UI for GCLI, this implementation
+ * Console is responsible for generating the UI for GCLI, this implementation
  * is a special case for use inside Firefox
  */
-function Display(options) {
+function Console(options) {
   this.hintElement = options.hintElement;
   this.gcliTerm = options.gcliTerm;
   this.consoleWrap = options.consoleWrap;
   this.requisition = options.requisition;
 
   // Create a FocusManager for the various parts to register with
   this.focusManager = new FocusManager({ document: options.chromeDocument });
   this.focusManager.onFocus.add(this.gcliTerm.show, this.gcliTerm);
@@ -5092,17 +5401,17 @@ function Display(options) {
   this.resizer = this.resizer.bind(this);
   this.chromeWindow.addEventListener('resize', this.resizer, false);
   this.requisition.commandChange.add(this.resizer, this);
 }
 
 /**
  * Avoid memory leaks
  */
-Display.prototype.destroy = function() {
+Console.prototype.destroy = function() {
   this.chromeWindow.removeEventListener('resize', this.resizer, false);
   delete this.resizer;
   delete this.chromeWindow;
   delete this.consoleWrap;
 
   this.hintElement.removeChild(this.menu.element);
   this.menu.destroy();
   this.hintElement.removeChild(this.argFetcher.element);
@@ -5117,56 +5426,57 @@ Display.prototype.destroy = function() {
 
   delete this.gcliTerm;
   delete this.hintElement;
 };
 
 /**
  * Called on chrome window resize, or on divider slide
  */
-Display.prototype.resizer = function() {
+Console.prototype.resizer = function() {
+  // Bug 705109: There are several numbers hard-coded in this function.
+  // This is simpler than calculating them, but error-prone when the UI setup,
+  // the styling or display settings change.
+
   var parentRect = this.consoleWrap.getBoundingClientRect();
+  // Magic number: 64 is the size of the toolbar above the output area
   var parentHeight = parentRect.bottom - parentRect.top - 64;
 
+  // Magic number: 100 is the size at which we decide the hints are too small
+  // to be useful, so we hide them
   if (parentHeight < 100) {
     this.hintElement.classList.add('gcliterm-hint-nospace');
   }
   else {
     this.hintElement.classList.remove('gcliterm-hint-nospace');
 
     var isMenuVisible = this.menu.element.style.display !== 'none';
     if (isMenuVisible) {
       this.menu.setMaxHeight(parentHeight);
 
-      // Magic numbers. We have 2 options - lots of complex dom math to derive
-      // the height of a menu item (19 pixels) and the vertical padding
-      // (22 pixels), or we could just hard-code. The former is *slightly* more
-      // resilient to refactoring (but still breaks with dom structure changes),
-      // the latter is simpler, faster and easier.
+      // Magic numbers: 19 = height of a menu item, 22 = total vertical padding
+      // of container
       var idealMenuHeight = (19 * this.menu.items.length) + 22;
-
       if (idealMenuHeight > parentHeight) {
-        this.hintElement.style.overflowY = 'scroll';
-        this.hintElement.style.borderBottomColor = 'threedshadow';
+        this.hintElement.classList.add('gcliterm-hint-scroll');
       }
       else {
-        this.hintElement.style.overflowY = null;
-        this.hintElement.style.borderBottomColor = 'white';
+        this.hintElement.classList.remove('gcliterm-hint-scroll');
       }
     }
     else {
       this.argFetcher.setMaxHeight(parentHeight);
 
       this.hintElement.style.overflowY = null;
       this.hintElement.style.borderBottomColor = 'white';
     }
   }
 };
 
-exports.Display = Display;
+exports.Console = Console;
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
@@ -5577,18 +5887,19 @@ cliView.Inputter = Inputter;
  * Completer is an 'input-like' element that sits  an input element annotating
  * it with visual goodness.
  * @param {object} options An object that contains various options which
  * customizes how the completer functions.
  * Properties on the options object:
  * - document (required) DOM document to be used in creating elements
  * - requisition (required) A GCLI Requisition object whose state is monitored
  * - completeElement (optional) An element to use
- * - completionPrompt (optional) The prompt to show before a completion.
- *   Defaults to '&#x00bb;' (double greater-than, a.k.a right guillemet).
+ * - completionPrompt (optional) The prompt - defaults to '\u00bb'
+ *   (double greater-than, a.k.a right guillemet). The prompt is used directly
+ *   in a TextNode, so HTML entities are not allowed.
  */
 function Completer(options) {
   this.document = options.document || document;
   this.requisition = options.requisition;
   this.elementCreated = false;
 
   this.element = options.completeElement || 'gcli-row-complete';
   if (typeof this.element === 'string') {
@@ -5601,17 +5912,17 @@ function Completer(options) {
       this.element.className = 'gcli-in-complete gcli-in-valid';
       this.element.setAttribute('tabindex', '-1');
       this.element.setAttribute('aria-live', 'polite');
     }
   }
 
   this.completionPrompt = typeof options.completionPrompt === 'string'
       ? options.completionPrompt
-      : '&#x00bb;';
+      : '\u00bb';
 
   if (options.inputBackgroundElement) {
     this.backgroundElement = options.inputBackgroundElement;
   }
   else {
     this.backgroundElement = this.element;
   }
 }
@@ -5709,86 +6020,123 @@ function isStrictCompletion(inputValue, 
 
 /**
  * Bring the completion element up to date with what the requisition says
  */
 Completer.prototype.update = function(input) {
   var current = this.requisition.getAssignmentAt(input.cursor.start);
   var predictions = current.getPredictions();
 
-  var completion = '<span class="gcli-prompt">' + this.completionPrompt + '</span> ';
+  dom.clearElement(this.element);
+
+  // All this DOM manipulation is equivalent to the HTML below.
+  // It's not a template because it's very simple except appendMarkupStatus()
+  // which is complex due to a need to merge spans.
+  // Bug 707131 questions if we couldn't simplify this to use a template.
+  //
+  // <span class="gcli-prompt">${completionPrompt}</span>
+  // ${appendMarkupStatus()}
+  // ${prefix}
+  // <span class="gcli-in-ontab">${contents}</span>
+  // <span class="gcli-in-closebrace" if="${unclosedJs}">}<span>
+
+  var document = this.element.ownerDocument;
+  var prompt = document.createElement('span');
+  prompt.classList.add('gcli-prompt');
+  prompt.appendChild(document.createTextNode(this.completionPrompt + ' '));
+  this.element.appendChild(prompt);
+
   if (input.typed.length > 0) {
     var scores = this.requisition.getInputStatusMarkup(input.cursor.start);
-    completion += this.markupStatusScore(scores, input);
+    this.appendMarkupStatus(this.element, scores, input);
   }
 
   if (input.typed.length > 0 && predictions.length > 0) {
     var tab = predictions[0].name;
     var existing = current.getArg().text;
-    if (isStrictCompletion(existing, tab) && input.cursor.start === input.typed.length) {
-      // Display the suffix of the prediction as the completion.
+
+    var contents;
+    var prefix = null;
+
+    if (isStrictCompletion(existing, tab) &&
+            input.cursor.start === input.typed.length) {
+      // Display the suffix of the prediction as the completion
       var numLeadingSpaces = existing.match(/^(\s*)/)[0].length;
-      var suffix = tab.slice(existing.length - numLeadingSpaces);
-      completion += '<span class="gcli-in-ontab">' + suffix + '</span>';
+      contents = tab.slice(existing.length - numLeadingSpaces);
     } else {
       // Display the '-> prediction' at the end of the completer element
-      completion += ' &#xa0;<span class="gcli-in-ontab">&#x21E5; ' +
-          tab + '</span>';
-    }
-  }
-
-  // A hack to add a grey '}' to the end of the command line when we've opened
+      prefix = ' \u00a0';         // aka &nbsp;
+      contents = '\u21E5 ' + tab; // aka &rarr; the right arrow
+    }
+
+    if (prefix != null) {
+      this.element.appendChild(document.createTextNode(prefix));
+    }
+
+    var suffix = document.createElement('span');
+    suffix.classList.add('gcli-in-ontab');
+    suffix.appendChild(document.createTextNode(contents));
+    this.element.appendChild(suffix);
+  }
+
+  // Add a grey '}' to the end of the command line when we've opened
   // with a { but haven't closed it
   var command = this.requisition.commandAssignment.getValue();
-  if (command && command.name === '{') {
-    if (this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1) {
-      completion += '<span class="gcli-in-closebrace">}</span>';
-    }
-  }
-
-  dom.setInnerHtml(this.element, completion);
+  var unclosedJs = command && command.name === '{' &&
+          this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1;
+  if (unclosedJs) {
+    var close = document.createElement('span');
+    close.classList.add('gcli-in-closebrace');
+    close.appendChild(document.createTextNode('}'));
+    this.element.appendChild(close);
+  }
 };
 
 /**
  * Mark-up an array of Status values with spans
  */
-Completer.prototype.markupStatusScore = function(scores, input) {
-  var completion = '';
+Completer.prototype.appendMarkupStatus = function(element, scores, input) {
   if (scores.length === 0) {
-    return completion;
-  }
-
+    return;
+  }
+
+  var document = element.ownerDocument;
   var i = 0;
   var lastStatus = -1;
+  var span;
+  var contents = '';
+
   while (true) {
     if (lastStatus !== scores[i]) {
       var state = scores[i];
       if (!state) {
         console.error('No state at i=' + i + '. scores.len=' + scores.length);
         state = Status.VALID;
       }
-      completion += '<span class="gcli-in-' + state.toString().toLowerCase() + '">';
+      span = document.createElement('span');
+      span.classList.add('gcli-in-' + state.toString().toLowerCase());
       lastStatus = scores[i];
     }
     var char = input.typed[i];
     if (char === ' ') {
-      char = '&#xa0;';
-    }
-    completion += char;
+      char = '\u00a0';
+    }
+    contents += char;
     i++;
     if (i === input.typed.length) {
-      completion += '</span>';
+      span.appendChild(document.createTextNode(contents));
+      this.element.appendChild(span);
       break;
     }
     if (lastStatus !== scores[i]) {
-      completion += '</span>';
-    }
-  }
-
-  return completion;
+      span.appendChild(document.createTextNode(contents));
+      this.element.appendChild(span);
+      contents = '';
+    }
+  }
 };
 
 cliView.Completer = Completer;
 
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
@@ -5862,17 +6210,17 @@ exports.History = History;
 define('gcli/ui/arg_fetch', ['require', 'exports', 'module' , 'gcli/util', 'gcli/types', 'gcli/ui/field', 'gcli/ui/domtemplate', 'text!gcli/ui/arg_fetch.css', 'text!gcli/ui/arg_fetch.html'], function(require, exports, module) {
 var argFetch = exports;
 
 
 var dom = require('gcli/util').dom;
 var Status = require('gcli/types').Status;
 
 var getField = require('gcli/ui/field').getField;
-var Templater = require('gcli/ui/domtemplate').Templater;
+var domtemplate = require('gcli/ui/domtemplate');
 
 var editorCss = require('text!gcli/ui/arg_fetch.css');
 var argFetchHtml = require('text!gcli/ui/arg_fetch.html');
 
 
 /**
  * A widget to display an inline dialog which allows the user to fill out
  * the arguments to a command.
@@ -5891,17 +6239,16 @@ function ArgFetcher(options) {
     throw new Error('No document');
   }
 
   this.element =  dom.createElement(this.document, 'div');
   this.element.className = options.argFetcherClass || 'gcli-argfetch';
   // We cache the fields we create so we can destroy them later
   this.fields = [];
 
-  this.tmpl = new Templater();
   // Populated by template
   this.okElement = null;
 
   // Pull the HTML into the DOM, but don't add it to the document
   if (editorCss != null) {
     this.style = dom.importCss(editorCss, this.document);
   }
 
@@ -5948,17 +6295,18 @@ ArgFetcher.prototype.onCommandChange = f
       // Just the text has changed
       return;
     }
 
     this.fields.forEach(function(field) { field.destroy(); });
     this.fields = [];
 
     var reqEle = this.reqTempl.cloneNode(true);
-    this.tmpl.processNode(reqEle, this);
+    domtemplate.template(reqEle, this,
+            { allowEval: true, stack: 'arg_fetch.html' });
     dom.clearElement(this.element);
     this.element.appendChild(reqEle);
 
     var status = this.requisition.getStatus();
     this.okElement.disabled = (status === Status.VALID);
 
     this.element.style.display = 'block';
   }
@@ -6003,17 +6351,17 @@ ArgFetcher.prototype.getInputFor = funct
 
     // Bug 681894: we add the field as a property of the assignment so that
     // #linkMessageElement() can call 'field.setMessageElement(element)'
     assignment.field = newField;
 
     return newField.element;
   }
   catch (ex) {
-    // This is called from within Templater which can make tracing errors hard
+    // This is called from within template() which can make tracing errors hard
     // so we log here if anything goes wrong
     console.error(ex);
     return '';
   }
 };
 
 /**
  * Called by the template to setup an mutable message field
@@ -6247,17 +6595,17 @@ exports.getField = getField;
  */
 function StringField(type, options) {
   this.document = options.document;
   this.type = type;
   this.arg = new Argument();
 
   this.element = dom.createElement(this.document, 'input');
   this.element.type = 'text';
-  this.element.className = 'gcli-field';
+  this.element.classList.add('gcli-field');
 
   this.onInputChange = this.onInputChange.bind(this);
   this.element.addEventListener('keyup', this.onInputChange, false);
 
   this.fieldChanged = createEvent('StringField.fieldChanged');
 }
 
 StringField.prototype = Object.create(Field.prototype);
@@ -6407,17 +6755,17 @@ addField(BooleanField);
  * </ul>
  */
 function SelectionField(type, options) {
   this.document = options.document;
   this.type = type;
   this.items = [];
 
   this.element = dom.createElement(this.document, 'select');
-  this.element.className = 'gcli-field';
+  this.element.classList.add('gcli-field');
   this._addOption({
     name: l10n.lookupFormat('fieldSelectionSelect', [ options.name ])
   });
   var lookup = this.type.getLookup();
   lookup.forEach(this._addOption, this);
 
   this.onInputChange = this.onInputChange.bind(this);
   this.element.addEventListener('change', this.onInputChange, false);
@@ -6482,18 +6830,18 @@ function JavascriptField(type, options) 
   this.onInputChange = this.onInputChange.bind(this);
   this.arg = new Argument('', '{ ', ' }');
 
   this.element = dom.createElement(this.document, 'div');
 
   this.input = dom.createElement(this.document, 'input');
   this.input.type = 'text';
   this.input.addEventListener('keyup', this.onInputChange, false);
-  this.input.style.marginBottom = '0';
-  this.input.className = 'gcli-field';
+  this.input.classList.add('gcli-field');
+  this.input.classList.add('gcli-field-javascript');
   this.element.appendChild(this.input);
 
   this.menu = new Menu({ document: this.document, field: true });
   this.element.appendChild(this.menu.element);
 
   this.setConversion(this.type.parse(new Argument('')));
 
   this.fieldChanged = createEvent('JavascriptField.fieldChanged');
@@ -6675,28 +7023,28 @@ function ArrayField(type, options) {
   this.options = options;
   this.requ = options.requisition;
 
   this._onAdd = this._onAdd.bind(this);
   this.members = [];
 
   // <div class=gcliArrayParent save="${element}">
   this.element = dom.createElement(this.document, 'div');
-  this.element.className = 'gcliArrayParent';
+  this.element.classList.add('gcli-array-parent');
 
   // <button class=gcliArrayMbrAdd onclick="${_onAdd}" save="${addButton}">Add
   this.addButton = dom.createElement(this.document, 'button');
-  this.addButton.className = 'gcliArrayMbrAdd';
+  this.addButton.classList.add('gcli-array-member-add');
   this.addButton.addEventListener('click', this._onAdd, false);
   this.addButton.innerHTML = l10n.lookup('fieldArrayAdd');
   this.element.appendChild(this.addButton);
 
   // <div class=gcliArrayMbrs save="${mbrElement}">
   this.container = dom.createElement(this.document, 'div');
-  this.container.className = 'gcliArrayMbrs';
+  this.container.classList.add('gcli-array-members');
   this.element.appendChild(this.container);
 
   this.onInputChange = this.onInputChange.bind(this);
 
   this.fieldChanged = createEvent('ArrayField.fieldChanged');
 }
 
 ArrayField.prototype = Object.create(Field.prototype);
@@ -6729,17 +7077,17 @@ ArrayField.prototype.getConversion = fun
     arrayArg.addArgument(conversion.arg);
   }
   return new ArrayConversion(conversions, arrayArg);
 };
 
 ArrayField.prototype._onAdd = function(ev, subConversion) {
   // <div class=gcliArrayMbr save="${element}">
   var element = dom.createElement(this.document, 'div');
-  element.className = 'gcliArrayMbr';
+  element.classList.add('gcli-array-member');
   this.container.appendChild(element);
 
   // ${field.element}
   var field = getField(this.type.subtype, this.options);
   field.fieldChanged.add(function() {
     var conversion = this.getConversion();
     this.fieldChanged({ conversion: conversion });
     this.setMessage(conversion.message);
@@ -6747,17 +7095,17 @@ ArrayField.prototype._onAdd = function(e
 
   if (subConversion) {
     field.setConversion(subConversion);
   }
   element.appendChild(field.element);
 
   // <div class=gcliArrayMbrDel onclick="${_onDel}">
   var delButton = dom.createElement(this.document, 'button');
-  delButton.className = 'gcliArrayMbrDel';
+  delButton.classList.add('gcli-array-member-del');
   delButton.addEventListener('click', this._onDel, false);
   delButton.innerHTML = l10n.lookup('fieldArrayDel');
   element.appendChild(delButton);
 
   var member = {
     element: element,
     field: field,
     parent: this
@@ -6789,17 +7137,17 @@ define('gcli/ui/menu', ['require', 'expo
 
 
 var dom = require('gcli/util').dom;
 
 var Conversion = require('gcli/types').Conversion;
 var Argument = require('gcli/argument').Argument;
 var canon = require('gcli/canon');
 
-var Templater = require('gcli/ui/domtemplate').Templater;
+var domtemplate = require('gcli/ui/domtemplate');
 
 var menuCss = require('text!gcli/ui/menu.css');
 var menuHtml = require('text!gcli/ui/menu.html');
 
 
 /**
  * Menu is a display of the commands that are possible given the state of a
  * requisition.
@@ -6872,17 +7220,17 @@ Menu.prototype.show = function(items, er
   this.items = items;
 
   if (this.error == null && this.items.length === 0) {
     this.element.style.display = 'none';
     return;
   }
 
   var options = this.optTempl.cloneNode(true);
-  new Templater().processNode(options, this);
+  domtemplate.template(options, this, { allowEval: true, stack: 'menu.html' });
 
   dom.clearElement(this.element);
   this.element.appendChild(options);
 
   this.element.style.display = 'block';
 };
 
 /**
@@ -6980,28 +7328,16 @@ CommandMenu.prototype.onCommandChange = 
     this.hide();
   }
 };
 
 exports.CommandMenu = CommandMenu;
 
 
 });
-/*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
- */
-
-define('gcli/ui/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) {
-
-  Components.utils.import("resource:///modules/devtools/Templater.jsm");
-  exports.Templater = Templater;
-
-});
 define("text!gcli/ui/menu.css", [], void 0);
 define("text!gcli/ui/menu.html", [], "\n" +
   "<table class=\"gcli-menu-template\" aria-live=\"polite\">\n" +
   "  <tr class=\"gcli-menu-option\" foreach=\"item in ${items}\"\n" +
   "      onclick=\"${onItemClick}\" title=\"${item.manual || ''}\">\n" +
   "    <td class=\"gcli-menu-name\">${item.name}</td>\n" +
   "    <td class=\"gcli-menu-desc\">${item.description}</td>\n" +
   "  </tr>\n" +
--- a/browser/devtools/webconsole/test/browser/browser_gcli_integrate.js
+++ b/browser/devtools/webconsole/test/browser/browser_gcli_integrate.js
@@ -78,22 +78,22 @@ function testCallCommands() {
 
   // Test unsuccessful auto-completion
   gcliterm.inputNode.value = "ec";
   gcliterm.inputNode.focus();
   EventUtils.synthesizeKey("d", {});
   is(gcliterm.completeNode.textContent, " ecd", "Completion for \"ecd\"");
 
   // Test a normal command's life cycle
-  gcliterm.opts.display.inputter.setInput("echo hello world");
+  gcliterm.opts.console.inputter.setInput("echo hello world");
   gcliterm.opts.requisition.exec();
 
-  let nodes = hud.outputNode.querySelectorAll("description");
+  let nodes = hud.outputNode.querySelectorAll(".gcliterm-msg-body");
 
-  is(nodes.length, 2, "Right number of output nodes");
+  is(nodes.length, 1, "Right number of output nodes");
   ok(/hello world/.test(nodes[0].textContent), "the command's output is correct.");
 
   gcliterm.clearOutput();
 }
 
 function testRemoveCommands() {
   let gcli = require("gcli/index");
   gcli.removeCommand(tselarr);
--- a/browser/devtools/webconsole/test/browser/browser_gcli_web.js
+++ b/browser/devtools/webconsole/test/browser/browser_gcli_web.js
@@ -49,34 +49,38 @@ var define = obj.gcli._internal.define;
 var console = obj.gcli._internal.console;
 var Node = Components.interfaces.nsIDOMNode;
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
-define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testTokenize', 'gclitest/testSplit', 'gclitest/testCli', 'gclitest/testHistory', 'gclitest/testRequire', 'gclitest/testJs'], function(require, exports, module) {
+define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testTokenize', 'gclitest/testSplit', 'gclitest/testCli', 'gclitest/testExec', 'gclitest/testKeyboard', 'gclitest/testHistory', 'gclitest/testRequire', 'gclitest/testJs'], function(require, exports, module) {
 
   // We need to make sure GCLI is initialized before we begin testing it
   require('gcli/index');
 
   var examiner = require('test/examiner');
 
   // It's tempting to want to unify these strings and make addSuite() do the
   // call to require(), however that breaks the build system which looks for
   // the strings passed to require
   examiner.addSuite('gclitest/testTokenize', require('gclitest/testTokenize'));
   examiner.addSuite('gclitest/testSplit', require('gclitest/testSplit'));
   examiner.addSuite('gclitest/testCli', require('gclitest/testCli'));
+  examiner.addSuite('gclitest/testExec', require('gclitest/testExec'));
+  examiner.addSuite('gclitest/testKeyboard', require('gclitest/testKeyboard'));
   examiner.addSuite('gclitest/testHistory', require('gclitest/testHistory'));
   examiner.addSuite('gclitest/testRequire', require('gclitest/testRequire'));
   examiner.addSuite('gclitest/testJs', require('gclitest/testJs'));
 
   examiner.run();
+  console.log('Completed test suite');
+  // examiner.log();
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
@@ -163,16 +167,29 @@ examiner.toRemote = function() {
   return {
     suites: Object.keys(examiner.suites).map(function(suiteName) {
       return examiner.suites[suiteName].toRemote();
     }.bind(this))
   };
 };
 
 /**
+ * Output a test summary to console.log
+ */
+examiner.log = function() {
+  var remote = this.toRemote();
+  remote.suites.forEach(function(suite) {
+    console.log(suite.name);
+    suite.tests.forEach(function(test) {
+      console.log('- ' + test.name, test.status.name, test.message || '');
+    });
+  });
+};
+
+/**
  * Used by assert to record a failure against the current test
  */
 examiner.recordError = function(message) {
   if (!currentTest) {
     console.error('No currentTest for ' + message);
     return;
   }
 
@@ -294,18 +311,18 @@ Test.prototype.run = function() {
 
   try {
     this.func.apply(this.suite);
   }
   catch (ex) {
     this.status = stati.fail;
     this.messages.push('' + ex);
     console.error(ex);
-    if (console.trace) {
-      console.trace();
+    if (ex.stack) {
+      console.error(ex.stack);
     }
   }
 
   if (this.status === stati.executing) {
     this.status = stati.pass;
   }
 
   currentTest = null;
@@ -698,21 +715,22 @@ exports.testJavascript = function() {
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
-define('gclitest/commands', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types'], function(require, exports, module) {
+define('gclitest/commands', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/util', 'gcli/types/basic', 'gcli/types'], function(require, exports, module) {
 var commands = exports;
 
 
 var canon = require('gcli/canon');
+var util = require('gcli/util');
 
 var SelectionType = require('gcli/types/basic').SelectionType;
 var DeferredType = require('gcli/types/basic').DeferredType;
 var types = require('gcli/types');
 
 /**
  * Registration and de-registration.
  */
@@ -720,47 +738,57 @@ commands.setup = function() {
   commands.option1.type = types.getType('number');
   commands.option2.type = types.getType('boolean');
 
   types.registerType(commands.optionType);
   types.registerType(commands.optionValue);
 
   canon.addCommand(commands.tsv);
   canon.addCommand(commands.tsr);
+  canon.addCommand(commands.tse);
+  canon.addCommand(commands.tsj);
+  canon.addCommand(commands.tsb);
+  canon.addCommand(commands.tss);
   canon.addCommand(commands.tsu);
   canon.addCommand(commands.tsn);
   canon.addCommand(commands.tsnDif);
   canon.addCommand(commands.tsnExt);
   canon.addCommand(commands.tsnExte);
   canon.addCommand(commands.tsnExten);
   canon.addCommand(commands.tsnExtend);
   canon.addCommand(commands.tselarr);
   canon.addCommand(commands.tsm);
+  canon.addCommand(commands.tsg);
 };
 
 commands.shutdown = function() {
   canon.removeCommand(commands.tsv);
   canon.removeCommand(commands.tsr);
+  canon.removeCommand(commands.tse);
+  canon.removeCommand(commands.tsj);
+  canon.removeCommand(commands.tsb);
+  canon.removeCommand(commands.tss);
   canon.removeCommand(commands.tsu);
   canon.removeCommand(commands.tsn);
   canon.removeCommand(commands.tsnDif);
   canon.removeCommand(commands.tsnExt);
   canon.removeCommand(commands.tsnExte);
   canon.removeCommand(commands.tsnExten);
   canon.removeCommand(commands.tsnExtend);
   canon.removeCommand(commands.tselarr);
   canon.removeCommand(commands.tsm);
+  canon.removeCommand(commands.tsg);
 
   types.deregisterType(commands.optionType);
   types.deregisterType(commands.optionValue);
 };
 
 
-commands.option1 = { };
-commands.option2 = { };
+commands.option1 = { type: types.getType('string') };
+commands.option2 = { type: types.getType('number') };
 
 commands.optionType = new SelectionType({
   name: 'optionType',
   lookup: [
     { name: 'option1', value: commands.option1 },
     { name: 'option2', value: commands.option2 }
   ],
   noMatch: function() {
@@ -784,90 +812,151 @@ commands.optionValue = new DeferredType(
       return commands.optionType.lastOption.type;
     }
     else {
       return types.getType('blank');
     }
   }
 });
 
+commands.commandExec = util.createEvent('commands.commandExec');
+
+function createExec(name) {
+  return function(args, context) {
+    var data = {
+      command: commands[name],
+      args: args,
+      context: context
+    };
+    commands.commandExec(data);
+    return data;
+  };
+}
+
 commands.tsv = {
   name: 'tsv',
   params: [
     { name: 'optionType', type: 'optionType' },
     { name: 'optionValue', type: 'optionValue' }
   ],
-  exec: function(args, context) { }
+  exec: createExec('tsv')
 };
 
 commands.tsr = {
   name: 'tsr',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(args, context) { }
+  exec: createExec('tsr')
+};
+
+commands.tse = {
+  name: 'tse',
+  params: [ { name: 'node', type: 'node' } ],
+  exec: createExec('tse')
+};
+
+commands.tsj = {
+  name: 'tsj',
+  params: [ { name: 'javascript', type: 'javascript' } ],
+  exec: createExec('tsj')
+};
+
+commands.tsb = {
+  name: 'tsb',
+  params: [ { name: 'toggle', type: 'boolean' } ],
+  exec: createExec('tsb')
+};
+
+commands.tss = {
+  name: 'tss',
+  exec: createExec('tss')
 };
 
 commands.tsu = {
   name: 'tsu',
-  params: [ { name: 'num', type: 'number' } ],
-  exec: function(args, context) { }
+  params: [ { name: 'num', type: { name: 'number', max: 10, min: -5, step: 3 } } ],
+  exec: createExec('tsu')
 };
 
 commands.tsn = {
   name: 'tsn'
 };
 
 commands.tsnDif = {
   name: 'tsn dif',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('tsnDif')
 };
 
 commands.tsnExt = {
   name: 'tsn ext',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('tsnExt')
 };
 
 commands.tsnExte = {
   name: 'tsn exte',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('')
 };
 
 commands.tsnExten = {
   name: 'tsn exten',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('tsnExte')
 };
 
 commands.tsnExtend = {
   name: 'tsn extend',
   params: [ { name: 'text', type: 'string' } ],
-  exec: function(text) { }
+  exec: createExec('tsnExtend')
 };
 
 commands.tselarr = {
   name: 'tselarr',
   params: [
     { name: 'num', type: { name: 'selection', data: [ '1', '2', '3' ] } },
     { name: 'arr', type: { name: 'array', subtype: 'string' } },
   ],
-  exec: function(args, context) {}
+  exec: createExec('tselarr')
 };
 
 commands.tsm = {
   name: 'tsm',
   hidden: true,
   description: 'a 3-param test selection|string|number',
   params: [
     { name: 'abc', type: { name: 'selection', data: [ 'a', 'b', 'c' ] } },
     { name: 'txt', type: 'string' },
     { name: 'num', type: { name: 'number', max: 42, min: 0 } },
   ],
-  exec: function(args, context) {}
+  exec: createExec('tsm')
+};
+
+commands.tsg = {
+  name: 'tsg',
+  hidden: true,
+  description: 'a param group test',
+  params: [
+    { name: 'solo', type: { name: 'selection', data: [ 'aaa', 'bbb', 'ccc' ] } },
+    {
+      group: 'First',
+      params: [
+        { name: 'txt1', type: 'string', defaultValue: null },
+        { name: 'boolean1', type: 'boolean' }
+      ]
+    },
+    {
+      group: 'Second',
+      params: [
+        { name: 'txt2', type: 'string', defaultValue: 'd' },
+        { name: 'num2', type: { name: 'number', defaultValue: 42 } }
+      ]
+    }
+  ],
+  exec: createExec('tsg')
 };
 
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
@@ -1213,16 +1302,288 @@ exports.testNestedCommand = function() {
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
+define('gclitest/testExec', ['require', 'exports', 'module' , 'gcli/cli', 'gcli/types', 'gcli/canon', 'gclitest/commands', 'gcli/types/node', 'test/assert'], function(require, exports, module) {
+
+
+var Requisition = require('gcli/cli').Requisition;
+var Status = require('gcli/types').Status;
+var canon = require('gcli/canon');
+var commands = require('gclitest/commands');
+var nodetype = require('gcli/types/node');
+
+var test = require('test/assert');
+
+var actualExec;
+var actualOutput;
+
+exports.setup = function() {
+  commands.setup();
+  commands.commandExec.add(onCommandExec);
+  canon.commandOutputManager.addListener(onCommandOutput);
+};
+
+exports.shutdown = function() {
+  commands.shutdown();
+  commands.commandExec.remove(onCommandExec);
+  canon.commandOutputManager.removeListener(onCommandOutput);
+};
+
+function onCommandExec(ev) {
+  actualExec = ev;
+}
+
+function onCommandOutput(ev) {
+  actualOutput = ev.output;
+}
+
+function exec(command, expectedArgs) {
+  var environment = {};
+
+  var requisition = new Requisition(environment);
+  var reply = requisition.exec({ typed: command });
+
+  test.is(command.indexOf(actualExec.command.name), 0, 'Command name: ' + command);
+
+  if (reply !== true) {
+    test.ok(false, 'reply = false for command: ' + command);
+  }
+
+  if (expectedArgs == null) {
+    test.ok(false, 'expectedArgs == null for ' + command);
+    return;
+  }
+  if (actualExec.args == null) {
+    test.ok(false, 'actualExec.args == null for ' + command);
+    return;
+  }
+
+  test.is(Object.keys(expectedArgs).length, Object.keys(actualExec.args).length,
+          'Arg count: ' + command);
+  Object.keys(expectedArgs).forEach(function(arg) {
+    var expectedArg = expectedArgs[arg];
+    var actualArg = actualExec.args[arg];
+
+    if (Array.isArray(expectedArg)) {
+      if (!Array.isArray(actualArg)) {
+        test.ok(false, 'actual is not an array. ' + command + '/' + arg);
+        return;
+      }
+
+      test.is(expectedArg.length, actualArg.length,
+              'Array length: ' + command + '/' + arg);
+      for (var i = 0; i < expectedArg.length; i++) {
+        test.is(expectedArg[i], actualArg[i],
+                'Member: "' + command + '/' + arg + '/' + i);
+      }
+    }
+    else {
+      test.is(expectedArg, actualArg, 'Command: "' + command + '" arg: ' + arg);
+    }
+  });
+
+  test.is(environment, actualExec.context.environment, 'Environment');
+
+  test.is(false, actualOutput.error, 'output error is false');
+  test.is(command, actualOutput.typed, 'command is typed');
+  test.ok(typeof actualOutput.canonical === 'string', 'canonical exists');
+
+  test.is(actualExec.args, actualOutput.args, 'actualExec.args is actualOutput.args');
+}
+
+
+exports.testExec = function() {
+  exec('tss', {});
+
+  // Bug 707008 - GCLI defered types don't work properly
+  // exec('tsv option1 10', { optionType: commands.option1, optionValue: '10' });
+  // exec('tsv option2 10', { optionType: commands.option1, optionValue: 10 });
+
+  exec('tsr fred', { text: 'fred' });
+  exec('tsr fred bloggs', { text: 'fred bloggs' });
+  exec('tsr "fred bloggs"', { text: 'fred bloggs' });
+
+  exec('tsb', { toggle: false });
+  exec('tsb --toggle', { toggle: true });
+
+  exec('tsu 10', { num: 10 });
+  exec('tsu --num 10', { num: 10 });
+
+  // Bug 704829 - Enable GCLI Javascript parameters
+  // The answer to this should be 2
+  exec('tsj { 1 + 1 }', { javascript: '1 + 1' });
+
+  var origDoc = nodetype.getDocument();
+  nodetype.setDocument(mockDoc);
+  exec('tse :root', { node: mockBody });
+  nodetype.setDocument(origDoc);
+
+  exec('tsn dif fred', { text: 'fred' });
+  exec('tsn exten fred', { text: 'fred' });
+  exec('tsn extend fred', { text: 'fred' });
+
+  exec('tselarr 1', { num: '1', arr: [ ] });
+  exec('tselarr 1 a', { num: '1', arr: [ 'a' ] });
+  exec('tselarr 1 a b', { num: '1', arr: [ 'a', 'b' ] });
+
+  exec('tsm a 10 10', { abc: 'a', txt: '10', num: 10 });
+
+  // Bug 707009 - GCLI doesn't always fill in default parameters properly
+  // exec('tsg a', { solo: 'a', txt1: null, boolean1: false, txt2: 'd', num2: 42 });
+};
+
+var mockBody = {
+  style: {}
+};
+
+var mockDoc = {
+  querySelectorAll: function(css) {
+    if (css === ':root') {
+      return {
+        length: 1,
+        item: function(i) {
+          return mockBody;
+        }
+      };
+    }
+    throw new Error('mockDoc.querySelectorAll(\'' + css + '\') error');
+  }
+};
+
+
+});
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+define('gclitest/testKeyboard', ['require', 'exports', 'module' , 'gcli/cli', 'gcli/types', 'gcli/canon', 'gclitest/commands', 'gcli/types/node', 'test/assert'], function(require, exports, module) {
+
+
+var Requisition = require('gcli/cli').Requisition;
+var Status = require('gcli/types').Status;
+var canon = require('gcli/canon');
+var commands = require('gclitest/commands');
+var nodetype = require('gcli/types/node');
+
+var test = require('test/assert');
+
+
+exports.setup = function() {
+  commands.setup();
+};
+
+exports.shutdown = function() {
+  commands.shutdown();
+};
+
+var COMPLETES_TO = 'complete';
+var KEY_UPS_TO = 'keyup';
+var KEY_DOWNS_TO = 'keydown';
+
+function check(initial, action, after) {
+  var requisition = new Requisition();
+  requisition.update({
+    typed: initial,
+    cursor: { start: initial.length, end: initial.length }
+  });
+  var assignment = requisition.getAssignmentAt(initial.length);
+  switch (action) {
+    case COMPLETES_TO:
+      assignment.complete();
+      break;
+
+    case KEY_UPS_TO:
+      assignment.increment();
+      break;
+
+    case KEY_DOWNS_TO:
+      assignment.decrement();
+      break;
+  }
+
+  test.is(after, requisition.toString(), initial + ' + ' + action + ' -> ' + after);
+}
+
+exports.testComplete = function() {
+  check('tsela', COMPLETES_TO, 'tselarr ');
+  check('tsn di', COMPLETES_TO, 'tsn dif ');
+  check('tsg a', COMPLETES_TO, 'tsg aaa ');
+
+  check('{ wind', COMPLETES_TO, '{ window');
+  check('{ window.docum', COMPLETES_TO, '{ window.document');
+  check('{ window.document.titl', COMPLETES_TO, '{ window.document.title ');
+};
+
+exports.testIncrDecr = function() {
+  check('tsu -70', KEY_UPS_TO, 'tsu -5');
+  check('tsu -7', KEY_UPS_TO, 'tsu -5');
+  check('tsu -6', KEY_UPS_TO, 'tsu -5');
+  check('tsu -5', KEY_UPS_TO, 'tsu -3');
+  check('tsu -4', KEY_UPS_TO, 'tsu -3');
+  check('tsu -3', KEY_UPS_TO, 'tsu 0');
+  check('tsu -2', KEY_UPS_TO, 'tsu 0');
+  check('tsu -1', KEY_UPS_TO, 'tsu 0');
+  check('tsu 0', KEY_UPS_TO, 'tsu 3');
+  check('tsu 1', KEY_UPS_TO, 'tsu 3');
+  check('tsu 2', KEY_UPS_TO, 'tsu 3');
+  check('tsu 3', KEY_UPS_TO, 'tsu 6');
+  check('tsu 4', KEY_UPS_TO, 'tsu 6');
+  check('tsu 5', KEY_UPS_TO, 'tsu 6');
+  check('tsu 6', KEY_UPS_TO, 'tsu 9');
+  check('tsu 7', KEY_UPS_TO, 'tsu 9');
+  check('tsu 8', KEY_UPS_TO, 'tsu 9');
+  check('tsu 9', KEY_UPS_TO, 'tsu 10');
+  check('tsu 10', KEY_UPS_TO, 'tsu 10');
+  check('tsu 100', KEY_UPS_TO, 'tsu -5');
+
+  check('tsu -70', KEY_DOWNS_TO, 'tsu 10');
+  check('tsu -7', KEY_DOWNS_TO, 'tsu 10');
+  check('tsu -6', KEY_DOWNS_TO, 'tsu 10');
+  check('tsu -5', KEY_DOWNS_TO, 'tsu -5');
+  check('tsu -4', KEY_DOWNS_TO, 'tsu -5');
+  check('tsu -3', KEY_DOWNS_TO, 'tsu -5');
+  check('tsu -2', KEY_DOWNS_TO, 'tsu -3');
+  check('tsu -1', KEY_DOWNS_TO, 'tsu -3');
+  check('tsu 0', KEY_DOWNS_TO, 'tsu -3');
+  check('tsu 1', KEY_DOWNS_TO, 'tsu 0');
+  check('tsu 2', KEY_DOWNS_TO, 'tsu 0');
+  check('tsu 3', KEY_DOWNS_TO, 'tsu 0');
+  check('tsu 4', KEY_DOWNS_TO, 'tsu 3');
+  check('tsu 5', KEY_DOWNS_TO, 'tsu 3');
+  check('tsu 6', KEY_DOWNS_TO, 'tsu 3');
+  check('tsu 7', KEY_DOWNS_TO, 'tsu 6');
+  check('tsu 8', KEY_DOWNS_TO, 'tsu 6');
+  check('tsu 9', KEY_DOWNS_TO, 'tsu 6');
+  check('tsu 10', KEY_DOWNS_TO, 'tsu 9');
+  check('tsu 100', KEY_DOWNS_TO, 'tsu 10');
+
+  // Bug 707007 - GCLI increment and decrement operations cycle through
+  // selection options in the wrong order
+  check('tselarr 1', KEY_DOWNS_TO, 'tselarr 2');
+  check('tselarr 2', KEY_DOWNS_TO, 'tselarr 3');
+  check('tselarr 3', KEY_DOWNS_TO, 'tselarr 1');
+
+  check('tselarr 3', KEY_UPS_TO, 'tselarr 2');
+};
+
+});
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
 define('gclitest/testHistory', ['require', 'exports', 'module' , 'test/assert', 'gcli/history'], function(require, exports, module) {
 
 var test = require('test/assert');
 var History = require('gcli/history').History;
 
 exports.setup = function() {
 };
 
@@ -1539,28 +1900,32 @@ exports.testBasic = function() {
 function undefine() {
   delete define.modules['gclitest/suite'];
   delete define.modules['test/examiner'];
   delete define.modules['gclitest/testTokenize'];
   delete define.modules['test/assert'];
   delete define.modules['gclitest/testSplit'];
   delete define.modules['gclitest/commands'];
   delete define.modules['gclitest/testCli'];
+  delete define.modules['gclitest/testExec'];
+  delete define.modules['gclitest/testKeyboard'];
   delete define.modules['gclitest/testHistory'];
   delete define.modules['gclitest/testRequire'];
   delete define.modules['gclitest/requirable'];
   delete define.modules['gclitest/testJs'];
 
   delete define.globalDomain.modules['gclitest/suite'];
   delete define.globalDomain.modules['test/examiner'];
   delete define.globalDomain.modules['gclitest/testTokenize'];
   delete define.globalDomain.modules['test/assert'];
   delete define.globalDomain.modules['gclitest/testSplit'];
   delete define.globalDomain.modules['gclitest/commands'];
   delete define.globalDomain.modules['gclitest/testCli'];
+  delete define.globalDomain.modules['gclitest/testExec'];
+  delete define.globalDomain.modules['gclitest/testKeyboard'];
   delete define.globalDomain.modules['gclitest/testHistory'];
   delete define.globalDomain.modules['gclitest/testRequire'];
   delete define.globalDomain.modules['gclitest/requirable'];
   delete define.globalDomain.modules['gclitest/testJs'];
 }
 
 registerCleanupFunction(function() {
   Services.prefs.clearUserPref("devtools.gcli.enable");
--- a/browser/locales/en-US/chrome/browser/devtools/gcli.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties
@@ -62,22 +62,22 @@ jstypeParseError=Error
 # LOCALIZATION NOTE (typesNumberNan): When the command line is passed a
 # number, however the input string is not a valid number, this error message
 # is displayed.
 typesNumberNan=Can't convert "%S" to a number.
 
 # LOCALIZATION NOTE (typesNumberMax): When the command line is passed a
 # number, but the number is bigger than the largest allowed number, this error
 # message is displayed.
-typesNumberMax=%1$S is greater that maximum allowed: %2$S.
+typesNumberMax=%1$S is greater than maximum allowed: %2$S.
 
 # LOCALIZATION NOTE (typesNumberMin): When the command line is passed a
 # number, but the number is lower than the smallest allowed number, this error
 # message is displayed.
-typesNumberMin=%1$S is smaller that minimum allowed: %2$S.
+typesNumberMin=%1$S is smaller than minimum allowed: %2$S.
 
 # LOCALIZATION NOTE (typesSelectionNomatch): When the command line is passed
 # an option with a limited number of correct values, but the passed value is
 # not one of them, this error message is displayed.
 typesSelectionNomatch=Can't use '%S'.
 
 # LOCALIZATION NOTE (nodeParseSyntax): When the command line is expecting a
 # CSS query string, however the passed string is not valid, this error message
@@ -89,8 +89,55 @@ nodeParseSyntax=Syntax error in CSS quer
 # error message is displayed.
 nodeParseMultiple=Too many matches (%S)
 
 # LOCALIZATION NOTE (nodeParseNone): When the command line is expecting a CSS
 # string that matches a single node, but no nodes match, this error message is
 # displayed.
 nodeParseNone=No matches
 
+# LOCALIZATION NOTE (helpDesc): A very short description of the 'help'
+# command. This string is designed to be shown in a menu alongside the command
+# name, which is why it should be as short as possible. See helpManual for a
+# fuller description of what it does.
+helpDesc=Get help on the available commands
+
+# LOCALIZATION NOTE (helpManual): A fuller description of the 'help' command.
+# Displayed when the user asks for help on what it does.
+helpManual=Provide help either on a specific command (if a search string is provided and an exact match is found) or on the available commands (if a search string is not provided, or if no exact match is found).
+
+# LOCALIZATION NOTE (helpSearchDesc): A very short description of the 'search'
+# parameter to the 'help' command. See helpSearchManual for a fuller
+# description of what it does. This string is designed to be shown in a dialog
+# with restricted space, which is why it should be as short as possible.
+helpSearchDesc=Search string
+
+# LOCALIZATION NOTE (helpSearchManual): A fuller description of the 'search'
+# parameter to the 'help' command. Displayed when the user asks for help on
+# what it does.
+helpSearchManual=A search string to use in narrowing down the list of commands that are displayed to the user. Any part of the string can match, regular expressions are not supported.
+
+# LOCALIZATION NOTE (helpManSynopsis): A heading shown at the top of a help
+# page for a command in the console It labels a summary of the parameters to
+# the command
+helpManSynopsis=Synopsis
+
+# LOCALIZATION NOTE (helpManDescription): A heading shown in a help page for a
+# command in the console. This heading precedes the top level description.
+helpManDescription=Description
+
+# LOCALIZATION NOTE (helpManParameters): A heading shown above the parameters
+# in a help page for a command in the console.
+helpManParameters=Parameters
+
+# LOCALIZATION NOTE (helpManNone): Some text shown under the parameters
+# heading in a help page for a command which has no parameters.
+helpManNone=None
+
+# LOCALIZATION NOTE (introHeader): The heading displayed at the top of the
+# output for the help command
+introHeader=Welcome to Firefox Developer Tools
+
+# LOCALIZATION NOTE (introBody): The text displayed at the top of the output
+# for the help command, just before the list of commands. This text is wrapped
+# inside a link to a localized MDN article
+introBody=For more information see MDN.
+
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -39,21 +39,19 @@ consoleclearDesc=Clear the console
 # LOCALIZATION NOTE (inspectDesc) A very short description of the 'inspect'
 # command. See inspectManual for a fuller description of what it does. This
 # string is designed to be shown in a menu alongside the command name, which
 # is why it should be as short as possible.
 inspectDesc=Inspect a node
 
 # LOCALIZATION NOTE (inspectManual) A fuller description of the 'inspect'
 # command, displayed when the user asks for help on what it does.
-inspectManual=Investigate the dimensions and properties of an element using \
-a CSS selector to open the DOM highlighter
+inspectManual=Investigate the dimensions and properties of an element using a CSS selector to open the DOM highlighter
 
 # LOCALIZATION NOTE (inspectNodeDesc) A very short string to describe the
 # 'node' parameter to the 'inspect' command, which is displayed in a dialog
 # when the user is using this command.
 inspectNodeDesc=CSS selector
 
 # LOCALIZATION NOTE (inspectNodeManual) A fuller description of the 'node'
 # parameter to the 'inspect' command, displayed when the user asks for help
 # on what it does.
-inspectNodeManual=A CSS selector for use with Document.querySelector which \
-identifies a single element
+inspectNodeManual=A CSS selector for use with Document.querySelector which identifies a single element
--- a/browser/themes/gnomestripe/devtools/gcli.css
+++ b/browser/themes/gnomestripe/devtools/gcli.css
@@ -41,17 +41,16 @@
 
 .gcliterm-input-node,
 .gcliterm-complete-node {
   border: none;
   -moz-appearance: none;
   height: 100%;
   vertical-align: middle;
   background-color: transparent;
-  font: 12px Consolas, "Lucida Console", monospace;
 }
 
 .gcliterm-input-node {
   padding-top: 2px;
   padding-bottom: 0;
   -moz-padding-start: 16px;
   -moz-padding-end: 0;
 }
@@ -59,34 +58,16 @@
 .gcliterm-complete-node {
   color: #FFF;
   padding-top: 4px;
   padding-bottom: 2px;
   -moz-padding-start: 21px;
   -moz-padding-end: 4px;
 }
 
-.gcli-in-valid {
-  border-bottom: none;
-}
-
-.gcli-in-incomplete {
-  color: #DDD;
-  border-bottom: 1px dotted #999;
-}
-
-.gcli-in-error {
-  color: #DDD;
-  border-bottom: 1px dotted #F00;
-}
-
-.gcli-in-ontab {
-  color: #999;
-}
-
 .gcliterm-stack-node {
   background: url("chrome://global/skin/icons/commandline.png") 4px center no-repeat;
   width: 100%;
 }
 
 .gcliterm-argfetcher {
   display: -moz-box;
   -moz-box-flex: 1;
@@ -104,29 +85,57 @@
 
 .gcliterm-hint-parent {
   width: 300px;
   padding: 10px 10px 0;
   border-top: 1px solid threedshadow;
   border-bottom: 1px solid threedshadow;
 }
 
-.gcli-help-right {
-  text-align: right;
-}
-
 .gcliterm-menu {
   display: -moz-box;
   -moz-box-flex: 1;
+  border-bottom-color: white;
+}
+
+.gcliterm-hint-scroll {
+  overflow-y: scroll;
+  border-bottom-color: threedshadow;
 }
 
 .gcliterm-hint-nospace {
   display: none;
 }
 
+.gcliterm-msg-body {
+  margin-top: 0;
+  margin-bottom: 3px;
+  -moz-margin-start: 3px;
+  -moz-margin-end: 6px;
+}
+
+/* Extract from display.css, we only want these 2 rules */
+
+.gcli-out-shortcut {
+  border: 1px solid #999;
+  border-radius: 3px;
+  padding: 0 4px;
+  margin: 0 4px;
+  font-size: 70%;
+  color: #666;
+  cursor: pointer;
+  vertical-align: bottom;
+}
+
+.gcli-out-shortcut:before {
+  color: #66F;
+  content: '\bb';
+  padding: 0 2px;
+}
+
 /*
  * The language of a console is not en_US or any other common language
  * (i.e we don't attempt to translate 'console.log(x)')
  * So we fix .gcliterm-input-node/.gcliterm-complete-node elements to be ltr.
  * As a result we also want the hints to pop up on the left (above the prompt)
  */
 .gcliterm-input-node,
 .gcliterm-complete-node,
@@ -146,16 +155,25 @@
  */
 .gcliterm-hint-parent:-moz-locale-dir(rtl),
 .hud-output-node:-moz-locale-dir(rtl) {
   direction: rtl;
 }
 
 /* From: $GCLI/mozilla/gcli/ui/gcliterm-gnomestripe.css */
 
+.gcliterm-input-node,
+.gcliterm-complete-node {
+  font: 12px "DejaVu Sans Mono", monospace;
+}
+
+.gcli-out-shortcut {
+  font-family: "DejaVu Sans Mono", monospace;
+}
+
 /* From: $GCLI/lib/gcli/ui/arg_fetch.css */
 
 .gcli-argfetch {
   width: 100%;
   box-sizing: border-box;
   -moz-box-sizing: border-box;
 }
 
@@ -176,32 +194,36 @@
 .gcli-af-paramname {
   text-align: right;
   font-size: 90%;
 }
 
 .gcli-af-required {
   font-size: 90%;
   color: #f66;
-  padding-left: 5px;
+  -moz-padding-start: 5px;
 }
 
 .gcli-af-error {
   font-size: 80%;
   color: #900;
 }
 
 .gcli-af-submit {
   text-align: right;
 }
 
 .gcli-field {
   width: 100%;
 }
 
+.gcli-field-javascript {
+  margin-bottom: 0;
+}
+
 /* From: $GCLI/lib/gcli/ui/menu.css */
 
 .gcli-menu {
   width: 100%;
   overflow: hidden;
 }
 
 .gcli-menu-field {
@@ -279,8 +301,50 @@
 .gcli-in-closebrace {
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
+
+/* From: $GCLI/lib/gcli/commands/help.css */
+
+.gcli-help-name {
+  text-align: end;
+}
+
+.gcli-help-arrow {
+  font-size: 70%;
+  color: #AAA;
+}
+
+.gcli-help-synopsis {
+  font-family: monospace;
+  font-weight: normal;
+  padding: 0 3px;
+  margin: 0 10px;
+  border: 1px solid #999;
+  border-radius: 3px;
+  color: #666;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.gcli-help-synopsis:before {
+  color: #66F;
+  content: '\bb';
+}
+
+.gcli-help-description {
+  margin: 0 20px;
+  padding: 0;
+}
+
+.gcli-help-parameter {
+  margin: 0 30px;
+  padding: 0;
+}
+
+.gcli-help-header {
+  margin: 10px 0 6px;
+}
--- a/browser/themes/pinstripe/devtools/gcli.css
+++ b/browser/themes/pinstripe/devtools/gcli.css
@@ -41,17 +41,16 @@
 
 .gcliterm-input-node,
 .gcliterm-complete-node {
   border: none;
   -moz-appearance: none;
   height: 100%;
   vertical-align: middle;
   background-color: transparent;
-  font: 12px Consolas, "Lucida Console", monospace;
 }
 
 .gcliterm-input-node {
   padding-top: 2px;
   padding-bottom: 0;
   -moz-padding-start: 16px;
   -moz-padding-end: 0;
 }
@@ -59,34 +58,16 @@
 .gcliterm-complete-node {
   color: #FFF;
   padding-top: 4px;
   padding-bottom: 2px;
   -moz-padding-start: 21px;
   -moz-padding-end: 4px;
 }
 
-.gcli-in-valid {
-  border-bottom: none;
-}
-
-.gcli-in-incomplete {
-  color: #DDD;
-  border-bottom: 1px dotted #999;
-}
-
-.gcli-in-error {
-  color: #DDD;
-  border-bottom: 1px dotted #F00;
-}
-
-.gcli-in-ontab {
-  color: #999;
-}
-
 .gcliterm-stack-node {
   background: url("chrome://global/skin/icons/commandline.png") 4px center no-repeat;
   width: 100%;
 }
 
 .gcliterm-argfetcher {
   display: -moz-box;
   -moz-box-flex: 1;
@@ -104,29 +85,57 @@
 
 .gcliterm-hint-parent {
   width: 300px;
   padding: 10px 10px 0;
   border-top: 1px solid threedshadow;
   border-bottom: 1px solid threedshadow;
 }
 
-.gcli-help-right {
-  text-align: right;
-}
-
 .gcliterm-menu {
   display: -moz-box;
   -moz-box-flex: 1;
+  border-bottom-color: white;
+}
+
+.gcliterm-hint-scroll {
+  overflow-y: scroll;
+  border-bottom-color: threedshadow;
 }
 
 .gcliterm-hint-nospace {
   display: none;
 }
 
+.gcliterm-msg-body {
+  margin-top: 0;
+  margin-bottom: 3px;
+  -moz-margin-start: 3px;
+  -moz-margin-end: 6px;
+}
+
+/* Extract from display.css, we only want these 2 rules */
+
+.gcli-out-shortcut {
+  border: 1px solid #999;
+  border-radius: 3px;
+  padding: 0 4px;
+  margin: 0 4px;
+  font-size: 70%;
+  color: #666;
+  cursor: pointer;
+  vertical-align: bottom;
+}
+
+.gcli-out-shortcut:before {
+  color: #66F;
+  content: '\bb';
+  padding: 0 2px;
+}
+
 /*
  * The language of a console is not en_US or any other common language
  * (i.e we don't attempt to translate 'console.log(x)')
  * So we fix .gcliterm-input-node/.gcliterm-complete-node elements to be ltr.
  * As a result we also want the hints to pop up on the left (above the prompt)
  */
 .gcliterm-input-node,
 .gcliterm-complete-node,
@@ -146,20 +155,29 @@
  */
 .gcliterm-hint-parent:-moz-locale-dir(rtl),
 .hud-output-node:-moz-locale-dir(rtl) {
   direction: rtl;
 }
 
 /* From: $GCLI/mozilla/gcli/ui/gcliterm-pinstripe.css */
 
+.gcliterm-input-node,
+.gcliterm-complete-node {
+  font: 11px Menlo, Monaco, monospace;
+}
+
 .gcliterm-complete-node {
   padding-top: 6px !important;
 }
 
+.gcli-out-shortcut {
+  font-family: Menlo, Monaco, monospace;
+}
+
 /* From: $GCLI/lib/gcli/ui/arg_fetch.css */
 
 .gcli-argfetch {
   width: 100%;
   box-sizing: border-box;
   -moz-box-sizing: border-box;
 }
 
@@ -180,32 +198,36 @@
 .gcli-af-paramname {
   text-align: right;
   font-size: 90%;
 }
 
 .gcli-af-required {
   font-size: 90%;
   color: #f66;
-  padding-left: 5px;
+  -moz-padding-start: 5px;
 }
 
 .gcli-af-error {
   font-size: 80%;
   color: #900;
 }
 
 .gcli-af-submit {
   text-align: right;
 }
 
 .gcli-field {
   width: 100%;
 }
 
+.gcli-field-javascript {
+  margin-bottom: 0;
+}
+
 /* From: $GCLI/lib/gcli/ui/menu.css */
 
 .gcli-menu {
   width: 100%;
   overflow: hidden;
 }
 
 .gcli-menu-field {
@@ -283,8 +305,50 @@
 .gcli-in-closebrace {
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
+
+/* From: $GCLI/lib/gcli/commands/help.css */
+
+.gcli-help-name {
+  text-align: end;
+}
+
+.gcli-help-arrow {
+  font-size: 70%;
+  color: #AAA;
+}
+
+.gcli-help-synopsis {
+  font-family: monospace;
+  font-weight: normal;
+  padding: 0 3px;
+  margin: 0 10px;
+  border: 1px solid #999;
+  border-radius: 3px;
+  color: #666;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.gcli-help-synopsis:before {
+  color: #66F;
+  content: '\bb';
+}
+
+.gcli-help-description {
+  margin: 0 20px;
+  padding: 0;
+}
+
+.gcli-help-parameter {
+  margin: 0 30px;
+  padding: 0;
+}
+
+.gcli-help-header {
+  margin: 10px 0 6px;
+}
--- a/browser/themes/winstripe/devtools/gcli.css
+++ b/browser/themes/winstripe/devtools/gcli.css
@@ -41,17 +41,16 @@
 
 .gcliterm-input-node,
 .gcliterm-complete-node {
   border: none;
   -moz-appearance: none;
   height: 100%;
   vertical-align: middle;
   background-color: transparent;
-  font: 12px Consolas, "Lucida Console", monospace;
 }
 
 .gcliterm-input-node {
   padding-top: 2px;
   padding-bottom: 0;
   -moz-padding-start: 16px;
   -moz-padding-end: 0;
 }
@@ -59,34 +58,16 @@
 .gcliterm-complete-node {
   color: #FFF;
   padding-top: 4px;
   padding-bottom: 2px;
   -moz-padding-start: 21px;
   -moz-padding-end: 4px;
 }
 
-.gcli-in-valid {
-  border-bottom: none;
-}
-
-.gcli-in-incomplete {
-  color: #DDD;
-  border-bottom: 1px dotted #999;
-}
-
-.gcli-in-error {
-  color: #DDD;
-  border-bottom: 1px dotted #F00;
-}
-
-.gcli-in-ontab {
-  color: #999;
-}
-
 .gcliterm-stack-node {
   background: url("chrome://global/skin/icons/commandline.png") 4px center no-repeat;
   width: 100%;
 }
 
 .gcliterm-argfetcher {
   display: -moz-box;
   -moz-box-flex: 1;
@@ -104,29 +85,57 @@
 
 .gcliterm-hint-parent {
   width: 300px;
   padding: 10px 10px 0;
   border-top: 1px solid threedshadow;
   border-bottom: 1px solid threedshadow;
 }
 
-.gcli-help-right {
-  text-align: right;
-}
-
 .gcliterm-menu {
   display: -moz-box;
   -moz-box-flex: 1;
+  border-bottom-color: white;
+}
+
+.gcliterm-hint-scroll {
+  overflow-y: scroll;
+  border-bottom-color: threedshadow;
 }
 
 .gcliterm-hint-nospace {
   display: none;
 }
 
+.gcliterm-msg-body {
+  margin-top: 0;
+  margin-bottom: 3px;
+  -moz-margin-start: 3px;
+  -moz-margin-end: 6px;
+}
+
+/* Extract from display.css, we only want these 2 rules */
+
+.gcli-out-shortcut {
+  border: 1px solid #999;
+  border-radius: 3px;
+  padding: 0 4px;
+  margin: 0 4px;
+  font-size: 70%;
+  color: #666;
+  cursor: pointer;
+  vertical-align: bottom;
+}
+
+.gcli-out-shortcut:before {
+  color: #66F;
+  content: '\bb';
+  padding: 0 2px;
+}
+
 /*
  * The language of a console is not en_US or any other common language
  * (i.e we don't attempt to translate 'console.log(x)')
  * So we fix .gcliterm-input-node/.gcliterm-complete-node elements to be ltr.
  * As a result we also want the hints to pop up on the left (above the prompt)
  */
 .gcliterm-input-node,
 .gcliterm-complete-node,
@@ -146,16 +155,25 @@
  */
 .gcliterm-hint-parent:-moz-locale-dir(rtl),
 .hud-output-node:-moz-locale-dir(rtl) {
   direction: rtl;
 }
 
 /* From: $GCLI/mozilla/gcli/ui/gcliterm-winstripe.css */
 
+.gcliterm-input-node,
+.gcliterm-complete-node {
+  font: 12px Consolas, "Lucida Console", monospace;
+}
+
+.gcli-out-shortcut {
+  font-family: Consolas, Inconsolata, "Courier New", monospace;
+}
+
 /* From: $GCLI/lib/gcli/ui/arg_fetch.css */
 
 .gcli-argfetch {
   width: 100%;
   box-sizing: border-box;
   -moz-box-sizing: border-box;
 }
 
@@ -176,32 +194,36 @@
 .gcli-af-paramname {
   text-align: right;
   font-size: 90%;
 }
 
 .gcli-af-required {
   font-size: 90%;
   color: #f66;
-  padding-left: 5px;
+  -moz-padding-start: 5px;
 }
 
 .gcli-af-error {
   font-size: 80%;
   color: #900;
 }
 
 .gcli-af-submit {
   text-align: right;
 }
 
 .gcli-field {
   width: 100%;
 }
 
+.gcli-field-javascript {
+  margin-bottom: 0;
+}
+
 /* From: $GCLI/lib/gcli/ui/menu.css */
 
 .gcli-menu {
   width: 100%;
   overflow: hidden;
 }
 
 .gcli-menu-field {
@@ -279,8 +301,50 @@
 .gcli-in-closebrace {
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
+
+/* From: $GCLI/lib/gcli/commands/help.css */
+
+.gcli-help-name {
+  text-align: end;
+}
+
+.gcli-help-arrow {
+  font-size: 70%;
+  color: #AAA;
+}
+
+.gcli-help-synopsis {
+  font-family: monospace;
+  font-weight: normal;
+  padding: 0 3px;
+  margin: 0 10px;
+  border: 1px solid #999;
+  border-radius: 3px;
+  color: #666;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.gcli-help-synopsis:before {
+  color: #66F;
+  content: '\bb';
+}
+
+.gcli-help-description {
+  margin: 0 20px;
+  padding: 0;
+}
+
+.gcli-help-parameter {
+  margin: 0 30px;
+  padding: 0;
+}
+
+.gcli-help-header {
+  margin: 10px 0 6px;
+}
--- a/toolkit/components/prompts/src/nsPrompter.js
+++ b/toolkit/components/prompts/src/nsPrompter.js
@@ -350,32 +350,27 @@ let PromptUtils = {
         // values, lest the prompt forget to set them.
         for (let propName in obj)
             obj[propName] = propBag.getProperty(propName);
     },
 
     getTabModalPrompt : function (domWin) {
         var promptBox = null;
 
-        // Given a content DOM window, returns the chrome window it's in.
-        function getChromeWindow(aWindow) {
-            var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                                   .getInterface(Ci.nsIWebNavigation)
-                                   .QueryInterface(Ci.nsIDocShell)
-                                   .chromeEventHandler.ownerDocument.defaultView;
-            return chromeWin;
-        }
-
         try {
             // Get the topmost window, in case we're in a frame.
             var promptWin = domWin.top;
 
             // Get the chrome window for the content window we're using.
             // (Unwrap because we need a non-IDL property below.)
-            var chromeWin = getChromeWindow(promptWin).wrappedJSObject;
+            var chromeWin = promptWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                                     .getInterface(Ci.nsIWebNavigation)
+                                     .QueryInterface(Ci.nsIDocShell)
+                                     .chromeEventHandler.ownerDocument
+                                     .defaultView.wrappedJSObject;
 
             if (chromeWin.getTabModalPromptBox)
                 promptBox = chromeWin.getTabModalPromptBox(promptWin);
         } catch (e) {
             // If any errors happen, just assume no tabmodal prompter.
         }
 
         return promptBox;