Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Sat, 25 Aug 2012 22:29:00 -0400
changeset 105503 8a566c3bff64acba851640a8919610e35452202b
parent 105502 90c92e55affdf1698371956adf6e24fa182d91b9 (current diff)
parent 105496 b3cce81fef1ad4558bda4dd899db70c4c301e6cc (diff)
child 105504 f551e52160e062737ea77c0b1eece31000c81ddd
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
milestone17.0a1
Merge m-c to inbound.
browser/devtools/commandline/GcliCommands.jsm
browser/devtools/commandline/GcliCookieCommands.jsm
browser/devtools/commandline/GcliTiltCommands.jsm
browser/devtools/commandline/gcli.css
browser/devtools/commandline/gclioutput.xhtml
browser/devtools/commandline/gclitooltip.xhtml
browser/devtools/commandline/test/browser_gcli_addon.js
browser/devtools/commandline/test/browser_gcli_break.html
browser/devtools/commandline/test/browser_gcli_break.js
browser/devtools/commandline/test/browser_gcli_calllog.js
browser/devtools/commandline/test/browser_gcli_commands.js
browser/devtools/commandline/test/browser_gcli_cookie.js
browser/devtools/commandline/test/browser_gcli_dbg.js
browser/devtools/commandline/test/browser_gcli_edit.js
browser/devtools/commandline/test/browser_gcli_inspect.html
browser/devtools/commandline/test/browser_gcli_inspect.js
browser/devtools/commandline/test/browser_gcli_integrate.js
browser/devtools/commandline/test/browser_gcli_jsb.js
browser/devtools/commandline/test/browser_gcli_pagemod_export.js
browser/devtools/commandline/test/browser_gcli_pref.js
browser/devtools/commandline/test/browser_gcli_responsivemode.js
browser/devtools/commandline/test/browser_gcli_restart.js
browser/devtools/commandline/test/browser_gcli_settings.js
browser/devtools/commandline/test/resources.html
browser/devtools/commandline/test/resources_dbg.html
browser/devtools/commandline/test/resources_inpage.js
browser/devtools/commandline/test/resources_inpage1.css
browser/devtools/commandline/test/resources_inpage2.css
browser/devtools/commandline/test/resources_jsb_script.js
browser/themes/gnomestripe/devtools/gcli.css
browser/themes/pinstripe/devtools/gcli.css
browser/themes/winstripe/devtools/gcli.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1025,17 +1025,19 @@ pref("devtools.debugger.remote-autoconne
 pref("devtools.debugger.remote-connection-retries", 3);
 pref("devtools.debugger.remote-timeout", 3000);
 
 // The default Debugger UI settings
 pref("devtools.debugger.ui.height", 250);
 pref("devtools.debugger.ui.remote-win.width", 900);
 pref("devtools.debugger.ui.remote-win.height", 400);
 pref("devtools.debugger.ui.stackframes-width", 200);
+pref("devtools.debugger.ui.stackframes-pane-visible", true);
 pref("devtools.debugger.ui.variables-width", 300);
+pref("devtools.debugger.ui.variables-pane-visible", true);
 
 // Enable the style inspector
 pref("devtools.styleinspector.enabled", true);
 
 // Enable the Tilt inspector
 pref("devtools.tilt.enabled", true);
 pref("devtools.tilt.intro_transition", true);
 pref("devtools.tilt.outro_transition", true);
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -67,19 +67,21 @@ let SocialUI = {
   _providerReady: function SocialUI_providerReady() {
     // If we couldn't find a provider, nothing to do here.
     if (!Social.provider)
       return;
 
     this.updateToggleCommand();
 
     let toggleCommand = this.toggleCommand;
-    let label = gNavigatorBundle.getFormattedString("social.enable.label",
-                                                    [Social.provider.name]);
-    let accesskey = gNavigatorBundle.getString("social.enable.accesskey");
+    let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
+    let label = gNavigatorBundle.getFormattedString("social.toggle.label",
+                                                    [Social.provider.name,
+                                                     brandShortName]);
+    let accesskey = gNavigatorBundle.getString("social.toggle.accesskey");
     toggleCommand.setAttribute("label", label);
     toggleCommand.setAttribute("accesskey", accesskey);
 
     SocialToolbar.init();
     SocialShareButton.init();
     SocialSidebar.init();
   },
 
@@ -133,17 +135,17 @@ let SocialUI = {
     Social.lastEventReceived = now;
 
     // Enable the social functionality, and indicate that it was activated
     Social.active = true;
 
     // Show a warning, allow undoing the activation
     let description = document.getElementById("social-activation-message");
     let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
-    let message = gNavigatorBundle.getFormattedString("social.activated.message",
+    let message = gNavigatorBundle.getFormattedString("social.activated.description",
                                                       [Social.provider.name, brandShortName]);
     description.value = message;
 
     SocialUI.notificationPanel.hidden = false;
 
     setTimeout(function () {
       SocialUI.notificationPanel.openPopup(SocialToolbar.button, "bottomcenter topright");
     }.bind(this), 0);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdAddon.jsm
@@ -0,0 +1,290 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
+
+/**
+ * 'addon' command.
+ */
+gcli.addCommand({
+  name: "addon",
+  description: gcli.lookup("addonDesc")
+});
+
+/**
+ * 'addon list' command.
+ */
+gcli.addCommand({
+  name: "addon list",
+  description: gcli.lookup("addonListDesc"),
+  params: [{
+    name: 'type',
+    type: {
+      name: 'selection',
+      data: ["dictionary", "extension", "locale", "plugin", "theme", "all"]
+    },
+    defaultValue: 'all',
+    description: gcli.lookup("addonListTypeDesc"),
+  }],
+  exec: function(aArgs, context) {
+    function representEnabledAddon(aAddon) {
+      return "<li><![CDATA[" + aAddon.name + "\u2002" + aAddon.version +
+      getAddonStatus(aAddon) + "]]></li>";
+    }
+
+    function representDisabledAddon(aAddon) {
+      return "<li class=\"gcli-addon-disabled\">" +
+        "<![CDATA[" + aAddon.name + "\u2002" + aAddon.version + aAddon.version +
+        "]]></li>";
+    }
+
+    function getAddonStatus(aAddon) {
+      let operations = [];
+
+      if (aAddon.pendingOperations & AddonManager.PENDING_ENABLE) {
+        operations.push("PENDING_ENABLE");
+      }
+
+      if (aAddon.pendingOperations & AddonManager.PENDING_DISABLE) {
+        operations.push("PENDING_DISABLE");
+      }
+
+      if (aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
+        operations.push("PENDING_UNINSTALL");
+      }
+
+      if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL) {
+        operations.push("PENDING_INSTALL");
+      }
+
+      if (aAddon.pendingOperations & AddonManager.PENDING_UPGRADE) {
+        operations.push("PENDING_UPGRADE");
+      }
+
+      if (operations.length) {
+        return " (" + operations.join(", ") + ")";
+      }
+      return "";
+    }
+
+    /**
+     * Compares two addons by their name. Used in sorting.
+     */
+    function compareAddonNames(aNameA, aNameB) {
+      return String.localeCompare(aNameA.name, aNameB.name);
+    }
+
+    /**
+     * Resolves the promise which is the scope (this) of this function, filling
+     * it with an HTML representation of the passed add-ons.
+     */
+    function list(aType, aAddons) {
+      if (!aAddons.length) {
+        this.resolve(gcli.lookup("addonNoneOfType"));
+      }
+
+      // Separate the enabled add-ons from the disabled ones.
+      let enabledAddons = [];
+      let disabledAddons = [];
+
+      aAddons.forEach(function(aAddon) {
+        if (aAddon.isActive) {
+          enabledAddons.push(aAddon);
+        } else {
+          disabledAddons.push(aAddon);
+        }
+      });
+
+      let header;
+      switch(aType) {
+        case "dictionary":
+          header = gcli.lookup("addonListDictionaryHeading");
+          break;
+        case "extension":
+          header = gcli.lookup("addonListExtensionHeading");
+          break;
+        case "locale":
+          header = gcli.lookup("addonListLocaleHeading");
+          break;
+        case "plugin":
+          header = gcli.lookup("addonListPluginHeading");
+          break;
+        case "theme":
+          header = gcli.lookup("addonListThemeHeading");
+        case "all":
+          header = gcli.lookup("addonListAllHeading");
+          break;
+        default:
+          header = gcli.lookup("addonListUnknownHeading");
+      }
+
+      // Map and sort the add-ons, and create an HTML list.
+      this.resolve(header +
+        "<ol>" +
+        enabledAddons.sort(compareAddonNames).map(representEnabledAddon).join("") +
+        disabledAddons.sort(compareAddonNames).map(representDisabledAddon).join("") +
+        "</ol>");
+    }
+
+    // Create the promise that will be resolved when the add-on listing has
+    // been finished.
+    let promise = context.createPromise();
+    let types = aArgs.type == "all" ? null : [aArgs.type];
+    AddonManager.getAddonsByTypes(types, list.bind(promise, aArgs.type));
+    return promise;
+  }
+});
+
+// We need a list of addon names for the enable and disable commands. Because
+// getting the name list is async we do not add the commands until we have the
+// list.
+AddonManager.getAllAddons(function addonAsync(aAddons) {
+  // We listen for installs to keep our addon list up to date. There is no need
+  // to listen for uninstalls because uninstalled addons are simply disabled
+  // until restart (to enable undo functionality).
+  AddonManager.addAddonListener({
+    onInstalled: function(aAddon) {
+      addonNameCache.push({
+        name: representAddon(aAddon).replace(/\s/g, "_"),
+        value: aAddon.name
+      });
+    },
+    onUninstalled: function(aAddon) {
+      let name = representAddon(aAddon).replace(/\s/g, "_");
+
+      for (let i = 0; i < addonNameCache.length; i++) {
+        if(addonNameCache[i].name == name) {
+          addonNameCache.splice(i, 1);
+          break;
+        }
+      }
+    },
+  });
+
+  /**
+   * Returns a string that represents the passed add-on.
+   */
+  function representAddon(aAddon) {
+    let name = aAddon.name + " " + aAddon.version;
+    return name.trim();
+  }
+
+  let addonNameCache = [];
+
+  // The name parameter, used in "addon enable" and "addon disable."
+  let nameParameter = {
+    name: "name",
+    type: {
+      name: "selection",
+      lookup: addonNameCache
+    },
+    description: gcli.lookup("addonNameDesc")
+  };
+
+  for (let addon of aAddons) {
+    addonNameCache.push({
+      name: representAddon(addon).replace(/\s/g, "_"),
+      value: addon.name
+    });
+  }
+
+  /**
+   * 'addon enable' command.
+   */
+  gcli.addCommand({
+    name: "addon enable",
+    description: gcli.lookup("addonEnableDesc"),
+    params: [nameParameter],
+    exec: function(aArgs, context) {
+      /**
+       * Enables the addon in the passed list which has a name that matches
+       * according to the passed name comparer, and resolves the promise which
+       * is the scope (this) of this function to display the result of this
+       * enable attempt.
+       */
+      function enable(aName, addons) {
+        // Find the add-on.
+        let addon = null;
+        addons.some(function(candidate) {
+          if (candidate.name == aName) {
+            addon = candidate;
+            return true;
+          } else {
+            return false;
+          }
+        });
+
+        let name = representAddon(addon);
+
+        if (!addon.userDisabled) {
+          this.resolve("<![CDATA[" +
+            gcli.lookupFormat("addonAlreadyEnabled", [name]) + "]]>");
+        } else {
+          addon.userDisabled = false;
+          // nl-nl: {$1} is ingeschakeld.
+          this.resolve("<![CDATA[" +
+            gcli.lookupFormat("addonEnabled", [name]) + "]]>");
+        }
+      }
+
+      let promise = context.createPromise();
+      // List the installed add-ons, enable one when done listing.
+      AddonManager.getAllAddons(enable.bind(promise, aArgs.name));
+      return promise;
+    }
+  });
+
+  /**
+   * 'addon disable' command.
+   */
+  gcli.addCommand({
+    name: "addon disable",
+    description: gcli.lookup("addonDisableDesc"),
+    params: [nameParameter],
+    exec: function(aArgs, context) {
+      /**
+       * Like enable, but ... you know ... the exact opposite.
+       */
+      function disable(aName, addons) {
+        // Find the add-on.
+        let addon = null;
+        addons.some(function(candidate) {
+          if (candidate.name == aName) {
+            addon = candidate;
+            return true;
+          } else {
+            return false;
+          }
+        });
+
+        let name = representAddon(addon);
+
+        if (addon.userDisabled) {
+          this.resolve("<![CDATA[" +
+            gcli.lookupFormat("addonAlreadyDisabled", [name]) + "]]>");
+        } else {
+          addon.userDisabled = true;
+          // nl-nl: {$1} is uitgeschakeld.
+          this.resolve("<![CDATA[" +
+            gcli.lookupFormat("addonDisabled", [name]) + "]]>");
+        }
+      }
+
+      let promise = context.createPromise();
+      // List the installed add-ons, disable one when done listing.
+      AddonManager.getAllAddons(disable.bind(promise, aArgs.name));
+      return promise;
+    }
+  });
+  Services.obs.notifyObservers(null, "gcli_addon_commands_ready", null);
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdBreak.jsm
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
+                                  "resource:///modules/HUDService.jsm");
+
+/**
+ * 'break' command
+ */
+gcli.addCommand({
+  name: "break",
+  description: gcli.lookup("breakDesc"),
+  manual: gcli.lookup("breakManual")
+});
+
+
+/**
+ * 'break list' command
+ */
+gcli.addCommand({
+  name: "break list",
+  description: gcli.lookup("breaklistDesc"),
+  returnType: "html",
+  exec: function(args, context) {
+    let win = HUDService.currentContext();
+    let dbg = win.DebuggerUI.getDebugger();
+    if (!dbg) {
+      return gcli.lookup("breakaddDebuggerStopped");
+    }
+    let breakpoints = dbg.breakpoints;
+
+    if (Object.keys(breakpoints).length === 0) {
+      return gcli.lookup("breaklistNone");
+    }
+
+    let reply = gcli.lookup("breaklistIntro");
+    reply += "<ol>";
+    for each (let breakpoint in breakpoints) {
+      let text = gcli.lookupFormat("breaklistLineEntry",
+                                   [breakpoint.location.url,
+                                    breakpoint.location.line]);
+      reply += "<li>" + text + "</li>";
+    };
+    reply += "</ol>";
+    return reply;
+  }
+});
+
+
+/**
+ * 'break add' command
+ */
+gcli.addCommand({
+  name: "break add",
+  description: gcli.lookup("breakaddDesc"),
+  manual: gcli.lookup("breakaddManual")
+});
+
+/**
+ * 'break add line' command
+ */
+gcli.addCommand({
+  name: "break add line",
+  description: gcli.lookup("breakaddlineDesc"),
+  params: [
+    {
+      name: "file",
+      type: {
+        name: "selection",
+        data: function() {
+          let win = HUDService.currentContext();
+          let dbg = win.DebuggerUI.getDebugger();
+          let files = [];
+          if (dbg) {
+            let scriptsView = dbg.contentWindow.DebuggerView.Scripts;
+            for each (let script in scriptsView.scriptLocations) {
+              files.push(script);
+            }
+          }
+          return files;
+        }
+      },
+      description: gcli.lookup("breakaddlineFileDesc")
+    },
+    {
+      name: "line",
+      type: { name: "number", min: 1, step: 10 },
+      description: gcli.lookup("breakaddlineLineDesc")
+    }
+  ],
+  returnType: "html",
+  exec: function(args, context) {
+    args.type = "line";
+    let win = HUDService.currentContext();
+    let dbg = win.DebuggerUI.getDebugger();
+    if (!dbg) {
+      return gcli.lookup("breakaddDebuggerStopped");
+    }
+    var promise = context.createPromise();
+    let position = { url: args.file, line: args.line };
+    dbg.addBreakpoint(position, function(aBreakpoint, aError) {
+      if (aError) {
+        promise.resolve(gcli.lookupFormat("breakaddFailed", [aError]));
+        return;
+      }
+      promise.resolve(gcli.lookup("breakaddAdded"));
+    });
+    return promise;
+  }
+});
+
+
+/**
+ * 'break del' command
+ */
+gcli.addCommand({
+  name: "break del",
+  description: gcli.lookup("breakdelDesc"),
+  params: [
+    {
+      name: "breakid",
+      type: {
+        name: "number",
+        min: 0,
+        max: function() {
+          let win = HUDService.currentContext();
+          let dbg = win.DebuggerUI.getDebugger();
+          if (!dbg) {
+            return gcli.lookup("breakaddDebuggerStopped");
+          }
+          return Object.keys(dbg.breakpoints).length - 1;
+        },
+      },
+      description: gcli.lookup("breakdelBreakidDesc")
+    }
+  ],
+  returnType: "html",
+  exec: function(args, context) {
+    let win = HUDService.currentContext();
+    let dbg = win.DebuggerUI.getDebugger();
+    if (!dbg) {
+      return gcli.lookup("breakaddDebuggerStopped");
+    }
+
+    let breakpoints = dbg.breakpoints;
+    let id = Object.keys(dbg.breakpoints)[args.breakid];
+    if (!id || !(id in breakpoints)) {
+      return gcli.lookup("breakNotFound");
+    }
+
+    let promise = context.createPromise();
+    try {
+      dbg.removeBreakpoint(breakpoints[id], function() {
+        promise.resolve(gcli.lookup("breakdelRemoved"));
+      });
+    } catch (ex) {
+      // If the debugger has been closed already, don't scare the user.
+      promise.resolve(gcli.lookup("breakdelRemoved"));
+    }
+    return promise;
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdCalllog.jsm
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
+                                  "resource:///modules/HUDService.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "Debugger", function() {
+  let JsDebugger = {};
+  Components.utils.import("resource://gre/modules/jsdebugger.jsm", JsDebugger);
+
+  let global = Components.utils.getGlobalForObject({});
+  JsDebugger.addDebuggerToGlobal(global);
+
+  return global.Debugger;
+});
+
+let debuggers = [];
+
+/**
+ * 'calllog' command
+ */
+gcli.addCommand({
+  name: "calllog",
+  description: gcli.lookup("calllogDesc")
+})
+
+/**
+ * 'calllog start' command
+ */
+gcli.addCommand({
+  name: "calllog start",
+  description: gcli.lookup("calllogStartDesc"),
+
+  exec: function(args, context) {
+    let contentWindow = context.environment.contentDocument.defaultView;
+
+    let dbg = new Debugger(contentWindow);
+    dbg.onEnterFrame = function(frame) {
+      // BUG 773652 -  Make the output from the GCLI calllog command nicer
+      contentWindow.console.log("Method call: " + this.callDescription(frame));
+    }.bind(this);
+
+    debuggers.push(dbg);
+
+    let tab = context.environment.chromeDocument.defaultView.gBrowser.selectedTab;
+    HUDService.activateHUDForContext(tab);
+
+    return gcli.lookup("calllogStartReply");
+  },
+
+  callDescription: function(frame) {
+    let name = "<anonymous>";
+    if (frame.callee.name) {
+      name = frame.callee.name;
+    }
+    else {
+      let desc = frame.callee.getOwnPropertyDescriptor("displayName");
+      if (desc && desc.value && typeof desc.value == "string") {
+        name = desc.value;
+      }
+    }
+
+    let args = frame.arguments.map(this.valueToString).join(", ");
+    return name + "(" + args + ")";
+  },
+
+  valueToString: function(value) {
+    if (typeof value !== "object" || value === null) {
+      return uneval(value);
+    }
+    return "[object " + value.class + "]";
+  }
+});
+
+/**
+ * 'calllog stop' command
+ */
+gcli.addCommand({
+  name: "calllog stop",
+  description: gcli.lookup("calllogStopDesc"),
+
+  exec: function(args, context) {
+    let numDebuggers = debuggers.length;
+    if (numDebuggers == 0) {
+      return gcli.lookup("calllogStopNoLogging");
+    }
+
+    for (let dbg of debuggers) {
+      dbg.onEnterFrame = undefined;
+    }
+    debuggers = [];
+
+    return gcli.lookupFormat("calllogStopReply", [ numDebuggers ]);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdCalllogChrome.jsm
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+let EXPORTED_SYMBOLS = [ ];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
+                                  "resource:///modules/HUDService.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "Debugger", function() {
+  let JsDebugger = {};
+  Cu.import("resource://gre/modules/jsdebugger.jsm", JsDebugger);
+
+  let global = Components.utils.getGlobalForObject({});
+  JsDebugger.addDebuggerToGlobal(global);
+
+  return global.Debugger;
+});
+
+let debuggers = [];
+let sandboxes = [];
+
+/**
+ * 'calllog chromestart' command
+ */
+gcli.addCommand({
+  name: "calllog chromestart",
+  description: gcli.lookup("calllogChromeStartDesc"),
+  get hidden() gcli.hiddenByChromePref(),
+  params: [
+    {
+      name: "sourceType",
+      type: {
+        name: "selection",
+        data: ["content-variable", "chrome-variable", "jsm", "javascript"]
+      }
+    },
+    {
+      name: "source",
+      type: "string",
+      description: gcli.lookup("calllogChromeSourceTypeDesc"),
+      manual: gcli.lookup("calllogChromeSourceTypeManual"),
+    }
+  ],
+  exec: function(args, context) {
+    let globalObj;
+    let contentWindow = context.environment.contentDocument.defaultView;
+
+    if (args.sourceType == "jsm") {
+      try {
+        globalObj = Cu.import(args.source);
+      }
+      catch (e) {
+        return gcli.lookup("callLogChromeInvalidJSM");
+      }
+    } else if (args.sourceType == "content-variable") {
+      if (args.source in contentWindow) {
+        globalObj = Cu.getGlobalForObject(contentWindow[args.source]);
+      } else {
+        throw new Error(gcli.lookup("callLogChromeVarNotFoundContent"));
+      }
+    } else if (args.sourceType == "chrome-variable") {
+      let chromeWin = context.environment.chromeDocument.defaultView;
+      if (args.source in chromeWin) {
+        globalObj = Cu.getGlobalForObject(chromeWin[args.source]);
+      } else {
+        return gcli.lookup("callLogChromeVarNotFoundChrome");
+      }
+    } else {
+      let chromeWin = context.environment.chromeDocument.defaultView;
+      let sandbox = new Cu.Sandbox(chromeWin,
+                                   {
+                                     sandboxPrototype: chromeWin,
+                                     wantXrays: false,
+                                     sandboxName: "gcli-cmd-calllog-chrome"
+                                   });
+      let returnVal;
+      try {
+        returnVal = Cu.evalInSandbox(args.source, sandbox, "ECMAv5");
+        sandboxes.push(sandbox);
+      } catch(e) {
+        // We need to save the message before cleaning up else e contains a dead
+        // object.
+        let msg = gcli.lookup("callLogChromeEvalException") + ": " + e;
+        Cu.nukeSandbox(sandbox);
+        return msg;
+      }
+
+      if (typeof returnVal == "undefined") {
+        return gcli.lookup("callLogChromeEvalNeedsObject");
+      }
+
+      globalObj = Cu.getGlobalForObject(returnVal);
+    }
+
+    let dbg = new Debugger(globalObj);
+    debuggers.push(dbg);
+
+    dbg.onEnterFrame = function(frame) {
+      // BUG 773652 -  Make the output from the GCLI calllog command nicer
+      contentWindow.console.log(gcli.lookup("callLogChromeMethodCall") +
+                                ": " + this.callDescription(frame));
+    }.bind(this);
+
+    let tab = context.environment.chromeDocument.defaultView.gBrowser.selectedTab;
+    HUDService.activateHUDForContext(tab);
+
+    return gcli.lookup("calllogChromeStartReply");
+  },
+
+  valueToString: function(value) {
+    if (typeof value !== "object" || value === null)
+      return uneval(value);
+    return "[object " + value.class + "]";
+  },
+
+  callDescription: function(frame) {
+    let name = frame.callee.name || gcli.lookup("callLogChromeAnonFunction");
+    let args = frame.arguments.map(this.valueToString).join(", ");
+    return name + "(" + args + ")";
+  }
+});
+
+/**
+ * 'calllog chromestop' command
+ */
+gcli.addCommand({
+  name: "calllog chromestop",
+  description: gcli.lookup("calllogChromeStopDesc"),
+  get hidden() gcli.hiddenByChromePref(),
+  exec: function(args, context) {
+    let numDebuggers = debuggers.length;
+    if (numDebuggers == 0) {
+      return gcli.lookup("calllogChromeStopNoLogging");
+    }
+
+    for (let dbg of debuggers) {
+      dbg.onEnterFrame = undefined;
+      dbg.enabled = false;
+    }
+    for (let sandbox of sandboxes) {
+      Cu.nukeSandbox(sandbox);
+    }
+    debuggers = [];
+    sandboxes = [];
+
+    return gcli.lookupFormat("calllogChromeStopReply", [ numDebuggers ]);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdCmd.jsm
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let EXPORTED_SYMBOLS = [ "CmdCommands" ];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let prefSvc = "@mozilla.org/preferences-service;1";
+XPCOMUtils.defineLazyGetter(this, "prefBranch", function() {
+  let prefService = Cc[prefSvc].getService(Ci.nsIPrefService);
+  return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+                                  "resource:///modules/devtools/Console.jsm");
+
+const PREF_DIR = "devtools.commands.dir";
+
+/**
+ * A place to store the names of the commands that we have added as a result of
+ * calling refreshAutoCommands(). Used by refreshAutoCommands to remove the
+ * added commands.
+ */
+let commands = [];
+
+/**
+ * Exported API
+ */
+let CmdCommands = {
+  /**
+   * Called to look in a directory pointed at by the devtools.commands.dir pref
+   * for *.mozcmd files which are then loaded.
+   * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
+   * we eval the script from the .mozcmd file. This should be a chrome window.
+   */
+  refreshAutoCommands: function GC_refreshAutoCommands(aSandboxPrincipal) {
+    // First get rid of the last set of commands
+    commands.forEach(function(name) {
+      gcli.removeCommand(name);
+    });
+
+    let dirName = prefBranch.getComplexValue(PREF_DIR,
+                                             Ci.nsISupportsString).data.trim();
+    if (dirName == "") {
+      return;
+    }
+
+    let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+    dir.initWithPath(dirName);
+    if (!dir.exists() || !dir.isDirectory()) {
+      throw new Error('\'' + dirName + '\' is not a directory.');
+    }
+
+    let en = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+
+    while (true) {
+      let file = en.nextFile;
+      if (!file) {
+        break;
+      }
+      if (file.leafName.match(/.*\.mozcmd$/) && file.isFile() && file.isReadable()) {
+        loadCommandFile(file, aSandboxPrincipal);
+      }
+    }
+  },
+};
+
+/**
+ * Load the commands from a single file
+ * @param nsIFile aFile The file containing the commands that we should read
+ * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
+ * we eval the script from the .mozcmd file. This should be a chrome window.
+ */
+function loadCommandFile(aFile, aSandboxPrincipal) {
+  NetUtil.asyncFetch(aFile, function refresh_fetch(aStream, aStatus) {
+    if (!Components.isSuccessCode(aStatus)) {
+      console.error("NetUtil.asyncFetch(" + aFile.path + ",..) failed. Status=" + aStatus);
+      return;
+    }
+
+    let source = NetUtil.readInputStreamToString(aStream, aStream.available());
+    aStream.close();
+
+    let sandbox = new Cu.Sandbox(aSandboxPrincipal, {
+      sandboxPrototype: aSandboxPrincipal,
+      wantXrays: false,
+      sandboxName: aFile.path
+    });
+    let data = Cu.evalInSandbox(source, sandbox, "1.8", aFile.leafName, 1);
+
+    if (!Array.isArray(data)) {
+      console.error("Command file '" + aFile.leafName + "' does not have top level array.");
+      return;
+    }
+
+    data.forEach(function(commandSpec) {
+      gcli.addCommand(commandSpec);
+      commands.push(commandSpec.name);
+    });
+  }.bind(this));
+}
+
+/**
+ * 'cmd' command
+ */
+gcli.addCommand({
+  name: "cmd",
+  get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
+  description: gcli.lookup("cmdDesc")
+});
+
+/**
+ * 'cmd refresh' command
+ */
+gcli.addCommand({
+  name: "cmd refresh",
+  description: gcli.lookup("cmdRefreshDesc"),
+  get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); },
+  exec: function Command_cmdRefresh(args, context) {
+    let chromeWindow = context.environment.chromeDocument.defaultView;
+    GcliCommands.refreshAutoCommands(chromeWindow);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdConsole.jsm
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
+                                  "resource:///modules/HUDService.jsm");
+
+/**
+ * 'console' command
+ */
+gcli.addCommand({
+  name: "console",
+  description: gcli.lookup("consoleDesc"),
+  manual: gcli.lookup("consoleManual")
+});
+
+/**
+ * 'console clear' command
+ */
+gcli.addCommand({
+  name: "console clear",
+  description: gcli.lookup("consoleclearDesc"),
+  exec: function Command_consoleClear(args, context) {
+    let window = context.environment.contentDocument.defaultView;
+    let hud = HUDService.getHudByWindow(window);
+    // hud will be null if the web console has not been opened for this window
+    if (hud) {
+      hud.jsterm.clearOutput();
+    }
+  }
+});
+
+/**
+ * 'console close' command
+ */
+gcli.addCommand({
+  name: "console close",
+  description: gcli.lookup("consolecloseDesc"),
+  exec: function Command_consoleClose(args, context) {
+    let tab = context.environment.chromeDocument.defaultView.gBrowser.selectedTab
+    HUDService.deactivateHUDForContext(tab);
+  }
+});
+
+/**
+ * 'console open' command
+ */
+gcli.addCommand({
+  name: "console open",
+  description: gcli.lookup("consoleopenDesc"),
+  exec: function Command_consoleOpen(args, context) {
+    let tab = context.environment.chromeDocument.defaultView.gBrowser.selectedTab
+    HUDService.activateHUDForContext(tab);
+  }
+});
rename from browser/devtools/commandline/GcliCookieCommands.jsm
rename to browser/devtools/commandline/CmdCookie.jsm
--- a/browser/devtools/commandline/GcliCookieCommands.jsm
+++ b/browser/devtools/commandline/CmdCookie.jsm
@@ -1,17 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 let EXPORTED_SYMBOLS = [ ];
 
-Components.utils.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+                                  "resource:///modules/devtools/Console.jsm");
 
 // We should really be using nsICookieManager so we can read more than just the
 // key/value of cookies. The difficulty is filtering the cookies that are
 // relevant to the current page. See
 // https://github.com/firebug/firebug/blob/master/extension/content/firebug/cookies/cookieObserver.js#L123
 // For details on how this is done with Firebug
 
 /**
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdDbg.jsm
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * 'dbg' command
+ */
+gcli.addCommand({
+  name: "dbg",
+  description: gcli.lookup("dbgDesc"),
+  manual: gcli.lookup("dbgManual")
+});
+
+
+/**
+ * 'dbg interrupt' command
+ */
+gcli.addCommand({
+  name: "dbg interrupt",
+  description: gcli.lookup("dbgInterrupt"),
+  params: [],
+  exec: function(args, context) {
+    let win = context.environment.chromeDocument.defaultView;
+    let dbg = win.DebuggerUI.getDebugger();
+
+    if (dbg) {
+      let controller = dbg.contentWindow.DebuggerController;
+      let thread = controller.activeThread;
+      if (!thread.paused) {
+        thread.interrupt();
+      }
+    }
+  }
+});
+
+/**
+ * 'dbg continue' command
+ */
+gcli.addCommand({
+  name: "dbg continue",
+  description: gcli.lookup("dbgContinue"),
+  params: [],
+  exec: function(args, context) {
+    let win = context.environment.chromeDocument.defaultView;
+    let dbg = win.DebuggerUI.getDebugger();
+
+    if (dbg) {
+      let controller = dbg.contentWindow.DebuggerController;
+      let thread = controller.activeThread;
+      if (thread.paused) {
+        thread.resume();
+      }
+    }
+  }
+});
+
+
+/**
+ * 'dbg step' command
+ */
+gcli.addCommand({
+  name: "dbg step",
+  description: gcli.lookup("dbgStepDesc"),
+  manual: gcli.lookup("dbgStepManual")
+});
+
+
+/**
+ * 'dbg step over' command
+ */
+gcli.addCommand({
+  name: "dbg step over",
+  description: gcli.lookup("dbgStepOverDesc"),
+  params: [],
+  exec: function(args, context) {
+    let win = context.environment.chromeDocument.defaultView;
+    let dbg = win.DebuggerUI.getDebugger();
+
+    if (dbg) {
+      let controller = dbg.contentWindow.DebuggerController;
+      let thread = controller.activeThread;
+      if (thread.paused) {
+        thread.stepOver();
+      }
+    }
+  }
+});
+
+/**
+ * 'dbg step in' command
+ */
+gcli.addCommand({
+  name: 'dbg step in',
+  description: gcli.lookup("dbgStepInDesc"),
+  params: [],
+  exec: function(args, context) {
+    let win = context.environment.chromeDocument.defaultView;
+    let dbg = win.DebuggerUI.getDebugger();
+
+    if (dbg) {
+      let controller = dbg.contentWindow.DebuggerController;
+      let thread = controller.activeThread;
+      if (thread.paused) {
+        thread.stepIn();
+      }
+    }
+  }
+});
+
+/**
+ * 'dbg step over' command
+ */
+gcli.addCommand({
+  name: 'dbg step out',
+  description: gcli.lookup("dbgStepOutDesc"),
+  params: [],
+  exec: function(args, context) {
+    let win = context.environment.chromeDocument.defaultView;
+    let dbg = win.DebuggerUI.getDebugger();
+
+    if (dbg) {
+      let controller = dbg.contentWindow.DebuggerController;
+      let thread = controller.activeThread;
+      if (thread.paused) {
+        thread.stepOut();
+      }
+    }
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdEcho.jsm
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+
+/**
+ * 'echo' command
+ */
+gcli.addCommand({
+  name: "echo",
+  description: gcli.lookup("echoDesc"),
+  params: [
+    {
+      name: "message",
+      type: "string",
+      description: gcli.lookup("echoMessageDesc")
+    }
+  ],
+  returnType: "string",
+  hidden: true,
+  exec: function Command_echo(args, context) {
+    return args.message;
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdExport.jsm
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+
+/**
+ * 'export' command
+ */
+gcli.addCommand({
+  name: "export",
+  description: gcli.lookup("exportDesc"),
+});
+
+/**
+ * The 'export html' command. This command allows the user to export the page to
+ * HTML after they do DOM changes.
+ */
+gcli.addCommand({
+  name: "export html",
+  description: gcli.lookup("exportHtmlDesc"),
+  exec: function(args, context) {
+    let document = context.environment.contentDocument;
+    let window = document.defaultView;
+    let page = document.documentElement.outerHTML;
+    window.open('data:text/plain;charset=utf8,' + encodeURIComponent(page));
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdJsb.jsm
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+const XMLHttpRequest =
+  Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/gcli.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "js_beautify",
+                                  "resource:///modules/devtools/Jsbeautify.jsm");
+
+/**
+ * jsb command.
+ */
+gcli.addCommand({
+  name: 'jsb',
+  description: gcli.lookup('jsbDesc'),
+  returnValue:'string',
+  params: [
+    {
+      name: 'url',
+      type: 'string',
+      description: gcli.lookup('jsbUrlDesc'),
+      manual: 'The URL of the JS to prettify'
+    },
+    {
+      name: 'indentSize',
+      type: 'number',
+      description: gcli.lookup('jsbIndentSizeDesc'),
+      manual: gcli.lookup('jsbIndentSizeManual'),
+      defaultValue: 2
+    },
+    {
+      name: 'indentChar',
+      type: {
+        name: 'selection',
+        lookup: [
+          { name: "space", value: " " },
+          { name: "tab", value: "\t" }
+        ]
+      },
+      description: gcli.lookup('jsbIndentCharDesc'),
+      manual: gcli.lookup('jsbIndentCharManual'),
+      defaultValue: ' ',
+    },
+    {
+      name: 'preserveNewlines',
+      type: 'boolean',
+      description: gcli.lookup('jsbPreserveNewlinesDesc'),
+      manual: gcli.lookup('jsbPreserveNewlinesManual')
+    },
+    {
+      name: 'preserveMaxNewlines',
+      type: 'number',
+      description: gcli.lookup('jsbPreserveMaxNewlinesDesc'),
+      manual: gcli.lookup('jsbPreserveMaxNewlinesManual'),
+      defaultValue: -1
+    },
+    {
+      name: 'jslintHappy',
+      type: 'boolean',
+      description: gcli.lookup('jsbJslintHappyDesc'),
+      manual: gcli.lookup('jsbJslintHappyManual')
+    },
+    {
+      name: 'braceStyle',
+      type: {
+        name: 'selection',
+        data: ['collapse', 'expand', 'end-expand', 'expand-strict']
+      },
+      description: gcli.lookup('jsbBraceStyleDesc'),
+      manual: gcli.lookup('jsbBraceStyleManual'),
+      defaultValue: "collapse"
+    },
+    {
+      name: 'spaceBeforeConditional',
+      type: 'boolean',
+      description: gcli.lookup('jsbSpaceBeforeConditionalDesc'),
+      manual: gcli.lookup('jsbSpaceBeforeConditionalManual')
+    },
+    {
+      name: 'unescapeStrings',
+      type: 'boolean',
+      description: gcli.lookup('jsbUnescapeStringsDesc'),
+      manual: gcli.lookup('jsbUnescapeStringsManual')
+    }
+  ],
+  exec: function(args, context) {
+    let opts = {
+      indent_size: args.indentSize,
+      indent_char: args.indentChar,
+      preserve_newlines: args.preserveNewlines,
+      max_preserve_newlines: args.preserveMaxNewlines == -1 ?
+                             undefined : args.preserveMaxNewlines,
+      jslint_happy: args.jslintHappy,
+      brace_style: args.braceStyle,
+      space_before_conditional: args.spaceBeforeConditional,
+      unescape_strings: args.unescapeStrings
+    }
+
+    let xhr = new XMLHttpRequest();
+
+    try {
+      xhr.open("GET", args.url, true);
+    } catch(e) {
+      return gcli.lookup('jsbInvalidURL');
+    }
+
+    let promise = context.createPromise();
+
+    xhr.onreadystatechange = function(aEvt) {
+      if (xhr.readyState == 4) {
+        if (xhr.status == 200 || xhr.status == 0) {
+          let browserDoc = context.environment.chromeDocument;
+          let browserWindow = browserDoc.defaultView;
+          let browser = browserWindow.gBrowser;
+  
+          browser.selectedTab = browser.addTab("data:text/plain;base64," +
+            browserWindow.btoa(js_beautify(xhr.responseText, opts)));
+          promise.resolve();
+        }
+        else {
+          promise.resolve("Unable to load page to beautify: " + args.url + " " +
+                          xhr.status + " " + xhr.statusText);
+        }
+      };
+    }
+    xhr.send(null);
+    return promise;
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdPagemod.jsm
@@ -0,0 +1,264 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+
+/**
+ * 'pagemod' command
+ */
+gcli.addCommand({
+  name: "pagemod",
+  description: gcli.lookup("pagemodDesc"),
+});
+
+/**
+ * The 'pagemod replace' command. This command allows the user to search and
+ * replace within text nodes and attributes.
+ */
+gcli.addCommand({
+  name: "pagemod replace",
+  description: gcli.lookup("pagemodReplaceDesc"),
+  params: [
+    {
+      name: "search",
+      type: "string",
+      description: gcli.lookup("pagemodReplaceSearchDesc"),
+    },
+    {
+      name: "replace",
+      type: "string",
+      description: gcli.lookup("pagemodReplaceReplaceDesc"),
+    },
+    {
+      name: "ignoreCase",
+      type: "boolean",
+      description: gcli.lookup("pagemodReplaceIgnoreCaseDesc"),
+    },
+    {
+      name: "selector",
+      type: "string",
+      description: gcli.lookup("pagemodReplaceSelectorDesc"),
+      defaultValue: "*:not(script):not(style):not(embed):not(object):not(frame):not(iframe):not(frameset)",
+    },
+    {
+      name: "root",
+      type: "node",
+      description: gcli.lookup("pagemodReplaceRootDesc"),
+      defaultValue: null,
+    },
+    {
+      name: "attrOnly",
+      type: "boolean",
+      description: gcli.lookup("pagemodReplaceAttrOnlyDesc"),
+    },
+    {
+      name: "contentOnly",
+      type: "boolean",
+      description: gcli.lookup("pagemodReplaceContentOnlyDesc"),
+    },
+    {
+      name: "attributes",
+      type: "string",
+      description: gcli.lookup("pagemodReplaceAttributesDesc"),
+      defaultValue: null,
+    },
+  ],
+  exec: function(args, context) {
+    let document = context.environment.contentDocument;
+    let searchTextNodes = !args.attrOnly;
+    let searchAttributes = !args.contentOnly;
+    let regexOptions = args.ignoreCase ? 'ig' : 'g';
+    let search = new RegExp(escapeRegex(args.search), regexOptions);
+    let attributeRegex = null;
+    if (args.attributes) {
+      attributeRegex = new RegExp(args.attributes, regexOptions);
+    }
+
+    let root = args.root || document;
+    let elements = root.querySelectorAll(args.selector);
+    elements = Array.prototype.slice.call(elements);
+
+    let replacedTextNodes = 0;
+    let replacedAttributes = 0;
+
+    function replaceAttribute() {
+      replacedAttributes++;
+      return args.replace;
+    }
+    function replaceTextNode() {
+      replacedTextNodes++;
+      return args.replace;
+    }
+
+    for (let i = 0; i < elements.length; i++) {
+      let element = elements[i];
+      if (searchTextNodes) {
+        for (let y = 0; y < element.childNodes.length; y++) {
+          let node = element.childNodes[y];
+          if (node.nodeType == node.TEXT_NODE) {
+            node.textContent = node.textContent.replace(search, replaceTextNode);
+          }
+        }
+      }
+
+      if (searchAttributes) {
+        if (!element.attributes) {
+          continue;
+        }
+        for (let y = 0; y < element.attributes.length; y++) {
+          let attr = element.attributes[y];
+          if (!attributeRegex || attributeRegex.test(attr.name)) {
+            attr.value = attr.value.replace(search, replaceAttribute);
+          }
+        }
+      }
+    }
+
+    return gcli.lookupFormat("pagemodReplaceResult",
+                             [elements.length, replacedTextNodes,
+                              replacedAttributes]);
+  }
+});
+
+/**
+ * 'pagemod remove' command
+ */
+gcli.addCommand({
+  name: "pagemod remove",
+  description: gcli.lookup("pagemodRemoveDesc"),
+});
+
+
+/**
+ * The 'pagemod remove element' command.
+ */
+gcli.addCommand({
+  name: "pagemod remove element",
+  description: gcli.lookup("pagemodRemoveElementDesc"),
+  params: [
+    {
+      name: "search",
+      type: "string",
+      description: gcli.lookup("pagemodRemoveElementSearchDesc"),
+    },
+    {
+      name: "root",
+      type: "node",
+      description: gcli.lookup("pagemodRemoveElementRootDesc"),
+      defaultValue: null,
+    },
+    {
+      name: 'stripOnly',
+      type: 'boolean',
+      description: gcli.lookup("pagemodRemoveElementStripOnlyDesc"),
+    },
+    {
+      name: 'ifEmptyOnly',
+      type: 'boolean',
+      description: gcli.lookup("pagemodRemoveElementIfEmptyOnlyDesc"),
+    },
+  ],
+  exec: function(args, context) {
+    let document = context.environment.contentDocument;
+    let root = args.root || document;
+    let elements = Array.prototype.slice.call(root.querySelectorAll(args.search));
+
+    let removed = 0;
+    for (let i = 0; i < elements.length; i++) {
+      let element = elements[i];
+      let parentNode = element.parentNode;
+      if (!parentNode || !element.removeChild) {
+        continue;
+      }
+      if (args.stripOnly) {
+        while (element.hasChildNodes()) {
+          parentNode.insertBefore(element.childNodes[0], element);
+        }
+      }
+      if (!args.ifEmptyOnly || !element.hasChildNodes()) {
+        element.parentNode.removeChild(element);
+        removed++;
+      }
+    }
+
+    return gcli.lookupFormat("pagemodRemoveElementResultMatchedAndRemovedElements",
+                             [elements.length, removed]);
+  }
+});
+
+/**
+ * The 'pagemod remove attribute' command.
+ */
+gcli.addCommand({
+  name: "pagemod remove attribute",
+  description: gcli.lookup("pagemodRemoveAttributeDesc"),
+  params: [
+    {
+      name: "searchAttributes",
+      type: "string",
+      description: gcli.lookup("pagemodRemoveAttributeSearchAttributesDesc"),
+    },
+    {
+      name: "searchElements",
+      type: "string",
+      description: gcli.lookup("pagemodRemoveAttributeSearchElementsDesc"),
+    },
+    {
+      name: "root",
+      type: "node",
+      description: gcli.lookup("pagemodRemoveAttributeRootDesc"),
+      defaultValue: null,
+    },
+    {
+      name: "ignoreCase",
+      type: "boolean",
+      description: gcli.lookup("pagemodRemoveAttributeIgnoreCaseDesc"),
+    },
+  ],
+  exec: function(args, context) {
+    let document = context.environment.contentDocument;
+
+    let root = args.root || document;
+    let regexOptions = args.ignoreCase ? 'ig' : 'g';
+    let attributeRegex = new RegExp(args.searchAttributes, regexOptions);
+    let elements = root.querySelectorAll(args.searchElements);
+    elements = Array.prototype.slice.call(elements);
+
+    let removed = 0;
+    for (let i = 0; i < elements.length; i++) {
+      let element = elements[i];
+      if (!element.attributes) {
+        continue;
+      }
+
+      var attrs = Array.prototype.slice.call(element.attributes);
+      for (let y = 0; y < attrs.length; y++) {
+        let attr = attrs[y];
+        if (attributeRegex.test(attr.name)) {
+          element.removeAttribute(attr.name);
+          removed++;
+        }
+      }
+    }
+
+    return gcli.lookupFormat("pagemodRemoveAttributeResult",
+                             [elements.length, removed]);
+  }
+});
+
+/**
+ * Make a given string safe to use  in a regular expression.
+ *
+ * @param string aString
+ *        The string you want to use in a regex.
+ * @return string
+ *         The equivalent of |aString| but safe to use in a regex.
+ */
+function escapeRegex(aString) {
+  return aString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdRestart.jsm
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Restart command
+ *
+ * @param boolean nocache
+ *        Disables loading content from cache upon restart.
+ *
+ * Examples :
+ * >> restart
+ * - restarts browser immediately
+ * >> restart --nocache
+ * - restarts immediately and starts Firefox without using cache
+ */
+gcli.addCommand({
+  name: "restart",
+  description: gcli.lookup("restartFirefoxDesc"),
+  params: [
+    {
+      name: "nocache",
+      type: "boolean",
+      description: gcli.lookup("restartFirefoxNocacheDesc")
+    }
+  ],
+  returnType: "string",
+  exec: function Restart(args, context) {
+    let canceled = Cc["@mozilla.org/supports-PRBool;1"]
+                     .createInstance(Ci.nsISupportsPRBool);
+    Services.obs.notifyObservers(canceled, "quit-application-requested", "restart");
+    if (canceled.data) {
+      return gcli.lookup("restartFirefoxRequestCancelled");
+    }
+
+    // disable loading content from cache.
+    if (args.nocache) {
+      Services.appinfo.invalidateCachesOnRestart();
+    }
+
+    // restart
+    Cc['@mozilla.org/toolkit/app-startup;1']
+      .getService(Ci.nsIAppStartup)
+      .quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+    return gcli.lookup("restartFirefoxRestarting");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/CmdScreenshot.jsm
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+let EXPORTED_SYMBOLS = [ ];
+
+Cu.import("resource:///modules/devtools/gcli.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LayoutHelpers",
+                                  "resource:///modules/devtools/LayoutHelpers.jsm");
+
+/**
+ * 'screenshot' command
+ */
+gcli.addCommand({
+  name: "screenshot",
+  description: gcli.lookup("screenshotDesc"),
+  manual: gcli.lookup("screenshotManual"),
+  returnType: "string",
+  params: [
+    {
+      name: "filename",
+      type: "string",
+      description: gcli.lookup("screenshotFilenameDesc"),
+      manual: gcli.lookup("screenshotFilenameManual")
+    },
+    {
+      name: "delay",
+      type: { name: "number", min: 0 },
+      defaultValue: 0,
+      description: gcli.lookup("screenshotDelayDesc"),
+      manual: gcli.lookup("screenshotDelayManual")
+    },
+    {
+      name: "fullpage",
+      type: "boolean",
+      description: gcli.lookup("screenshotFullPageDesc"),
+      manual: gcli.lookup("screenshotFullPageManual")
+    },
+    {
+      name: "node",
+      type: "node",
+      defaultValue: null,
+      description: gcli.lookup("inspectNodeDesc"),
+      manual: gcli.lookup("inspectNodeManual")
+    }
+  ],
+  exec: function Command_screenshot(args, context) {
+    var document = context.environment.contentDocument;
+    if (args.delay > 0) {
+      var promise = context.createPromise();
+      document.defaultView.setTimeout(function Command_screenshotDelay() {
+        let reply = this.grabScreen(document, args.filename);
+        promise.resolve(reply);
+      }.bind(this), args.delay * 1000);
+      return promise;
+    }
+    else {
+      return this.grabScreen(document, args.filename, args.fullpage, args.node);
+    }
+  },
+  grabScreen:
+  function Command_screenshotGrabScreen(document, filename, fullpage, node) {
+    let window = document.defaultView;
+    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    let left = 0;
+    let top = 0;
+    let width;
+    let height;
+
+    if (!fullpage) {
+      if (!node) {
+        left = window.scrollX;
+        top = window.scrollY;
+        width = window.innerWidth;
+        height = window.innerHeight;
+      } else {
+        let rect = LayoutHelpers.getRect(node, window);
+        top = rect.top;
+        left = rect.left;
+        width = rect.width;
+        height = rect.height;
+      }
+    } else {
+      width = window.innerWidth + window.scrollMaxX;
+      height = window.innerHeight + window.scrollMaxY;
+    }
+    canvas.width = width;
+    canvas.height = height;
+
+    let ctx = canvas.getContext("2d");
+    ctx.drawWindow(window, left, top, width, height, "#fff");
+
+    let data = canvas.toDataURL("image/png", "");
+    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+
+    // Check there is a .png extension to filename
+    if (!filename.match(/.png$/i)) {
+      filename += ".png";
+    }
+
+    // If the filename is relative, tack it onto the download directory
+    if (!filename.match(/[\\\/]/)) {
+      let downloadMgr = Cc["@mozilla.org/download-manager;1"]
+        .getService(Ci.nsIDownloadManager);
+      let tempfile = downloadMgr.userDownloadsDirectory;
+      tempfile.append(filename);
+      filename = tempfile.path;
+    }
+
+    try {
+      file.initWithPath(filename);
+    } catch (ex) {
+      return "Error saving to " + filename;
+    }
+
+    let ioService = Cc["@mozilla.org/network/io-service;1"]
+      .getService(Ci.nsIIOService);
+
+    let Persist = Ci.nsIWebBrowserPersist;
+    let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+      .createInstance(Persist);
+    persist.persistFlags = Persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+                           Persist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+    let source = ioService.newURI(data, "UTF8", null);
+    persist.saveURI(source, null, null, null, null, file);
+
+    return "Saved to " + filename;
+  }
+});
rename from browser/devtools/commandline/GcliCommands.jsm
rename to browser/devtools/commandline/Commands.jsm
--- a/browser/devtools/commandline/GcliCommands.jsm
+++ b/browser/devtools/commandline/Commands.jsm
@@ -1,1549 +1,26 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
-let EXPORTED_SYMBOLS = [ "GcliCommands" ];
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-const XMLHttpRequest =
-  Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
-
-Cu.import("resource:///modules/devtools/gcli.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "HUDService",
-                                  "resource:///modules/HUDService.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-                                  "resource://gre/modules/NetUtil.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "LayoutHelpers",
-                                  "resource:///modules/devtools/LayoutHelpers.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "console",
-                                  "resource://gre/modules/devtools/Console.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
-                                  "resource://gre/modules/AddonManager.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "js_beautify",
-                                  "resource:///modules/devtools/Jsbeautify.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "Debugger", function() {
-  let JsDebugger = {};
-  Components.utils.import("resource://gre/modules/jsdebugger.jsm", JsDebugger);
-
-  let global = Components.utils.getGlobalForObject({});
-  JsDebugger.addDebuggerToGlobal(global);
-
-  return global.Debugger;
-});
-
-let prefSvc = "@mozilla.org/preferences-service;1";
-XPCOMUtils.defineLazyGetter(this, "prefBranch", function() {
-  let prefService = Cc[prefSvc].getService(Ci.nsIPrefService);
-  return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
-});
-
-Cu.import("resource:///modules/devtools/GcliTiltCommands.jsm", {});
-Cu.import("resource:///modules/devtools/GcliCookieCommands.jsm", {});
-
-/**
- * A place to store the names of the commands that we have added as a result of
- * calling refreshAutoCommands(). Used by refreshAutoCommands to remove the
- * added commands.
- */
-let commands = [];
-
-/**
- * Exported API
- */
-let GcliCommands = {
-  /**
-   * Called to look in a directory pointed at by the devtools.commands.dir pref
-   * for *.mozcmd files which are then loaded.
-   * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
-   * we eval the script from the .mozcmd file. This should be a chrome window.
-   */
-  refreshAutoCommands: function GC_refreshAutoCommands(aSandboxPrincipal) {
-    // First get rid of the last set of commands
-    commands.forEach(function(name) {
-      gcli.removeCommand(name);
-    });
-
-    let dirName = prefBranch.getComplexValue("devtools.commands.dir",
-                                             Ci.nsISupportsString).data;
-    if (dirName == "") {
-      return;
-    }
-
-    let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
-    dir.initWithPath(dirName);
-    if (!dir.exists() || !dir.isDirectory()) {
-      throw new Error('\'' + dirName + '\' is not a directory.');
-    }
-
-    let en = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
-
-    while (true) {
-      let file = en.nextFile;
-      if (!file) {
-        break;
-      }
-      if (file.leafName.match(/.*\.mozcmd$/) && file.isFile() && file.isReadable()) {
-        loadCommandFile(file, aSandboxPrincipal);
-      }
-    }
-  },
-};
-
-/**
- * Load the commands from a single file
- * @param nsIFile aFile The file containing the commands that we should read
- * @param nsIPrincipal aSandboxPrincipal Scope object for the Sandbox in which
- * we eval the script from the .mozcmd file. This should be a chrome window.
- */
-function loadCommandFile(aFile, aSandboxPrincipal) {
-  NetUtil.asyncFetch(aFile, function refresh_fetch(aStream, aStatus) {
-    if (!Components.isSuccessCode(aStatus)) {
-      console.error("NetUtil.asyncFetch(" + aFile.path + ",..) failed. Status=" + aStatus);
-      return;
-    }
-
-    let source = NetUtil.readInputStreamToString(aStream, aStream.available());
-    aStream.close();
-
-    let sandbox = new Cu.Sandbox(aSandboxPrincipal, {
-      sandboxPrototype: aSandboxPrincipal,
-      wantXrays: false,
-      sandboxName: aFile.path
-    });
-    let data = Cu.evalInSandbox(source, sandbox, "1.8", aFile.leafName, 1);
-
-    if (!Array.isArray(data)) {
-      console.error("Command file '" + aFile.leafName + "' does not have top level array.");
-      return;
-    }
-
-    data.forEach(function(commandSpec) {
-      gcli.addCommand(commandSpec);
-      commands.push(commandSpec.name);
-    });
-  }.bind(this));
-}
-
-/**
- * 'cmd' command
- */
-gcli.addCommand({
-  name: "cmd",
-  description: gcli.lookup("cmdDesc"),
-  hidden: true
-});
-
-/**
- * 'cmd refresh' command
- */
-gcli.addCommand({
-  name: "cmd refresh",
-  description: gcli.lookup("cmdRefreshDesc"),
-  hidden: true,
-  exec: function Command_cmdRefresh(args, context) {
-    GcliCommands.refreshAutoCommands(context.environment.chromeDocument.defaultView);
-  }
-});
-
-/**
- * 'echo' command
- */
-gcli.addCommand({
-  name: "echo",
-  description: gcli.lookup("echoDesc"),
-  params: [
-    {
-      name: "message",
-      type: "string",
-      description: gcli.lookup("echoMessageDesc")
-    }
-  ],
-  returnType: "string",
-  hidden: true,
-  exec: function Command_echo(args, context) {
-    return args.message;
-  }
-});
-
-
-/**
- * 'screenshot' command
- */
-gcli.addCommand({
-  name: "screenshot",
-  description: gcli.lookup("screenshotDesc"),
-  manual: gcli.lookup("screenshotManual"),
-  returnType: "string",
-  params: [
-    {
-      name: "filename",
-      type: "string",
-      description: gcli.lookup("screenshotFilenameDesc"),
-      manual: gcli.lookup("screenshotFilenameManual")
-    },
-    {
-      name: "delay",
-      type: { name: "number", min: 0 },
-      defaultValue: 0,
-      description: gcli.lookup("screenshotDelayDesc"),
-      manual: gcli.lookup("screenshotDelayManual")
-    },
-    {
-      name: "fullpage",
-      type: "boolean",
-      defaultValue: false,
-      description: gcli.lookup("screenshotFullPageDesc"),
-      manual: gcli.lookup("screenshotFullPageManual")
-    },
-    {
-      name: "node",
-      type: "node",
-      defaultValue: null,
-      description: gcli.lookup("inspectNodeDesc"),
-      manual: gcli.lookup("inspectNodeManual")
-    }
-  ],
-  exec: function Command_screenshot(args, context) {
-    var document = context.environment.contentDocument;
-    if (args.delay > 0) {
-      var promise = context.createPromise();
-      document.defaultView.setTimeout(function Command_screenshotDelay() {
-        let reply = this.grabScreen(document, args.filename);
-        promise.resolve(reply);
-      }.bind(this), args.delay * 1000);
-      return promise;
-    }
-    else {
-      return this.grabScreen(document, args.filename, args.fullpage, args.node);
-    }
-  },
-  grabScreen:
-  function Command_screenshotGrabScreen(document, filename, fullpage, node) {
-    let window = document.defaultView;
-    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-    let left = 0;
-    let top = 0;
-    let width;
-    let height;
-
-    if (!fullpage) {
-      if (!node) {
-        left = window.scrollX;
-        top = window.scrollY;
-        width = window.innerWidth;
-        height = window.innerHeight;
-      } else {
-        let rect = LayoutHelpers.getRect(node, window);
-        top = rect.top;
-        left = rect.left;
-        width = rect.width;
-        height = rect.height;
-      }
-    } else {
-      width = window.innerWidth + window.scrollMaxX;
-      height = window.innerHeight + window.scrollMaxY;
-    }
-    canvas.width = width;
-    canvas.height = height;
-
-    let ctx = canvas.getContext("2d");
-    ctx.drawWindow(window, left, top, width, height, "#fff");
-
-    let data = canvas.toDataURL("image/png", "");
-    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
-
-    // Check there is a .png extension to filename
-    if (!filename.match(/.png$/i)) {
-      filename += ".png";
-    }
-
-    // If the filename is relative, tack it onto the download directory
-    if (!filename.match(/[\\\/]/)) {
-      let downloadMgr = Cc["@mozilla.org/download-manager;1"]
-        .getService(Ci.nsIDownloadManager);
-      let tempfile = downloadMgr.userDownloadsDirectory;
-      tempfile.append(filename);
-      filename = tempfile.path;
-    }
-
-    try {
-      file.initWithPath(filename);
-    } catch (ex) {
-      return "Error saving to " + filename;
-    }
-
-    let ioService = Cc["@mozilla.org/network/io-service;1"]
-      .getService(Ci.nsIIOService);
-
-    let Persist = Ci.nsIWebBrowserPersist;
-    let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
-      .createInstance(Persist);
-    persist.persistFlags = Persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
-                           Persist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
-
-    let source = ioService.newURI(data, "UTF8", null);
-    persist.saveURI(source, null, null, null, null, file);
-
-    return "Saved to " + filename;
-  }
-});
-
-
-let callLogDebuggers = [];
-
-/**
- * 'calllog' command
- */
-gcli.addCommand({
-  name: "calllog",
-  description: gcli.lookup("calllogDesc")
-})
-
-/**
- * 'calllog start' command
- */
-gcli.addCommand({
-  name: "calllog start",
-  description: gcli.lookup("calllogStartDesc"),
-
-  exec: function(args, context) {
-    let contentWindow = context.environment.contentDocument.defaultView;
-
-    let dbg = new Debugger(contentWindow);
-    dbg.onEnterFrame = function(frame) {
-      // BUG 773652 -  Make the output from the GCLI calllog command nicer
-      contentWindow.console.log("Method call: " + this.callDescription(frame));
-    }.bind(this);
-
-    callLogDebuggers.push(dbg);
-
-    let tab = context.environment.chromeDocument.defaultView.gBrowser.selectedTab;
-    HUDService.activateHUDForContext(tab);
-
-    return gcli.lookup("calllogStartReply");
-  },
-
-  callDescription: function(frame) {
-    let name = "<anonymous>";
-    if (frame.callee.name) {
-      name = frame.callee.name;
-    }
-    else {
-      let desc = frame.callee.getOwnPropertyDescriptor("displayName");
-      if (desc && desc.value && typeof desc.value == "string") {
-        name = desc.value;
-      }
-    }
-
-    let args = frame.arguments.map(this.valueToString).join(", ");
-    return name + "(" + args + ")";
-  },
-
-  valueToString: function(value) {
-    if (typeof value !== "object" || value === null) {
-      return uneval(value);
-    }
-    return "[object " + value.class + "]";
-  }
-});
+let EXPORTED_SYMBOLS = [ ];
 
-/**
- * 'calllog stop' command
- */
-gcli.addCommand({
-  name: "calllog stop",
-  description: gcli.lookup("calllogStopDesc"),
-
-  exec: function(args, context) {
-    let numDebuggers = callLogDebuggers.length;
-    if (numDebuggers == 0) {
-      return gcli.lookup("calllogStopNoLogging");
-    }
-
-    for (let dbg of callLogDebuggers) {
-      dbg.onEnterFrame = undefined;
-    }
-    callLogDebuggers = [];
-
-    return gcli.lookupFormat("calllogStopReply", [ numDebuggers ]);
-  }
-});
-
-
-/**
- * 'console' command
- */
-gcli.addCommand({
-  name: "console",
-  description: gcli.lookup("consoleDesc"),
-  manual: gcli.lookup("consoleManual")
-});
-
-/**
- * 'console clear' command
- */
-gcli.addCommand({
-  name: "console clear",
-  description: gcli.lookup("consoleclearDesc"),
-  exec: function Command_consoleClear(args, context) {
-    let window = context.environment.contentDocument.defaultView;
-    let hud = HUDService.getHudByWindow(window);
-    // hud will be null if the web console has not been opened for this window
-    if (hud) {
-      hud.jsterm.clearOutput();
-    }
-  }
-});
-
-/**
- * 'console close' command
- */
-gcli.addCommand({
-  name: "console close",
-  description: gcli.lookup("consolecloseDesc"),
-  exec: function Command_consoleClose(args, context) {
-    let tab = context.environment.chromeDocument.defaultView.gBrowser.selectedTab
-    HUDService.deactivateHUDForContext(tab);
-  }
-});
-
-/**
- * 'console open' command
- */
-gcli.addCommand({
-  name: "console open",
-  description: gcli.lookup("consoleopenDesc"),
-  exec: function Command_consoleOpen(args, context) {
-    let tab = context.environment.chromeDocument.defaultView.gBrowser.selectedTab
-    HUDService.activateHUDForContext(tab);
-  }
-});
-
-/**
- * Restart command
- *
- * @param boolean nocache
- *        Disables loading content from cache upon restart.
- *
- * Examples :
- * >> restart
- * - restarts browser immediately
- * >> restart --nocache
- * - restarts immediately and starts Firefox without using cache
- */
-gcli.addCommand({
-  name: "restart",
-  description: gcli.lookup("restartFirefoxDesc"),
-  params: [
-    {
-      name: "nocache",
-      type: "boolean",
-      defaultValue: false,
-      description: gcli.lookup("restartFirefoxNocacheDesc")
-    }
-  ],
-  returnType: "string",
-  exec: function Restart(args, context) {
-    let canceled = Cc["@mozilla.org/supports-PRBool;1"]
-                     .createInstance(Ci.nsISupportsPRBool);
-    Services.obs.notifyObservers(canceled, "quit-application-requested", "restart");
-    if (canceled.data) {
-      return gcli.lookup("restartFirefoxRequestCancelled");
-    }
-
-    // disable loading content from cache.
-    if (args.nocache) {
-      Services.appinfo.invalidateCachesOnRestart();
-    }
-
-    // restart
-    Cc['@mozilla.org/toolkit/app-startup;1']
-      .getService(Ci.nsIAppStartup)
-      .quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
-    return gcli.lookup("restartFirefoxRestarting");
-  }
-});
-
-/**
- * 'inspect' command
- */
-gcli.addCommand({
-  name: "inspect",
-  description: gcli.lookup("inspectDesc"),
-  manual: gcli.lookup("inspectManual"),
-  params: [
-    {
-      name: "node",
-      type: "node",
-      description: gcli.lookup("inspectNodeDesc"),
-      manual: gcli.lookup("inspectNodeManual")
-    }
-  ],
-  exec: function Command_inspect(args, context) {
-    let document = context.environment.chromeDocument;
-    document.defaultView.InspectorUI.openInspectorUI(args.node);
-  }
-});
-
-/**
- * 'edit' command
- */
-gcli.addCommand({
-  name: "edit",
-  description: gcli.lookup("editDesc"),
-  manual: gcli.lookup("editManual2"),
-  params: [
-     {
-       name: 'resource',
-       type: {
-         name: 'resource',
-         include: 'text/css'
-       },
-       description: gcli.lookup("editResourceDesc")
-     },
-     {
-       name: "line",
-       defaultValue: 1,
-       type: {
-         name: "number",
-         min: 1,
-         step: 10
-       },
-       description: gcli.lookup("editLineToJumpToDesc")
-     }
-   ],
-   exec: function(args, context) {
-     let win = HUDService.currentContext();
-     win.StyleEditor.openChrome(args.resource.element, args.line);
-   }
-});
-
-/**
- * 'break' command
- */
-gcli.addCommand({
-  name: "break",
-  description: gcli.lookup("breakDesc"),
-  manual: gcli.lookup("breakManual")
-});
-
-
-/**
- * 'break list' command
- */
-gcli.addCommand({
-  name: "break list",
-  description: gcli.lookup("breaklistDesc"),
-  returnType: "html",
-  exec: function(args, context) {
-    let win = HUDService.currentContext();
-    let dbg = win.DebuggerUI.getDebugger();
-    if (!dbg) {
-      return gcli.lookup("breakaddDebuggerStopped");
-    }
-    let breakpoints = dbg.breakpoints;
-
-    if (Object.keys(breakpoints).length === 0) {
-      return gcli.lookup("breaklistNone");
-    }
-
-    let reply = gcli.lookup("breaklistIntro");
-    reply += "<ol>";
-    for each (let breakpoint in breakpoints) {
-      let text = gcli.lookupFormat("breaklistLineEntry",
-                                   [breakpoint.location.url,
-                                    breakpoint.location.line]);
-      reply += "<li>" + text + "</li>";
-    };
-    reply += "</ol>";
-    return reply;
-  }
-});
-
-
-/**
- * 'break add' command
- */
-gcli.addCommand({
-  name: "break add",
-  description: gcli.lookup("breakaddDesc"),
-  manual: gcli.lookup("breakaddManual")
-});
-
-/**
- * 'break add line' command
- */
-gcli.addCommand({
-  name: "break add line",
-  description: gcli.lookup("breakaddlineDesc"),
-  params: [
-    {
-      name: "file",
-      type: {
-        name: "selection",
-        data: function() {
-          let win = HUDService.currentContext();
-          let dbg = win.DebuggerUI.getDebugger();
-          let files = [];
-          if (dbg) {
-            let scriptsView = dbg.contentWindow.DebuggerView.Scripts;
-            for each (let script in scriptsView.scriptLocations) {
-              files.push(script);
-            }
-          }
-          return files;
-        }
-      },
-      description: gcli.lookup("breakaddlineFileDesc")
-    },
-    {
-      name: "line",
-      type: { name: "number", min: 1, step: 10 },
-      description: gcli.lookup("breakaddlineLineDesc")
-    }
-  ],
-  returnType: "html",
-  exec: function(args, context) {
-    args.type = "line";
-    let win = HUDService.currentContext();
-    let dbg = win.DebuggerUI.getDebugger();
-    if (!dbg) {
-      return gcli.lookup("breakaddDebuggerStopped");
-    }
-    var promise = context.createPromise();
-    let position = { url: args.file, line: args.line };
-    dbg.addBreakpoint(position, function(aBreakpoint, aError) {
-      if (aError) {
-        promise.resolve(gcli.lookupFormat("breakaddFailed", [aError]));
-        return;
-      }
-      promise.resolve(gcli.lookup("breakaddAdded"));
-    });
-    return promise;
-  }
-});
-
-
-/**
- * 'break del' command
- */
-gcli.addCommand({
-  name: "break del",
-  description: gcli.lookup("breakdelDesc"),
-  params: [
-    {
-      name: "breakid",
-      type: {
-        name: "number",
-        min: 0,
-        max: function() {
-          let win = HUDService.currentContext();
-          let dbg = win.DebuggerUI.getDebugger();
-          if (!dbg) {
-            return gcli.lookup("breakaddDebuggerStopped");
-          }
-          return Object.keys(dbg.breakpoints).length - 1;
-        },
-      },
-      description: gcli.lookup("breakdelBreakidDesc")
-    }
-  ],
-  returnType: "html",
-  exec: function(args, context) {
-    let win = HUDService.currentContext();
-    let dbg = win.DebuggerUI.getDebugger();
-    if (!dbg) {
-      return gcli.lookup("breakaddDebuggerStopped");
-    }
-
-    let breakpoints = dbg.breakpoints;
-    let id = Object.keys(dbg.breakpoints)[args.breakid];
-    if (!id || !(id in breakpoints)) {
-      return gcli.lookup("breakNotFound");
-    }
-
-    let promise = context.createPromise();
-    try {
-      dbg.removeBreakpoint(breakpoints[id], function() {
-        promise.resolve(gcli.lookup("breakdelRemoved"));
-      });
-    } catch (ex) {
-      // If the debugger has been closed already, don't scare the user.
-      promise.resolve(gcli.lookup("breakdelRemoved"));
-    }
-    return promise;
-  }
-});
-
-/**
- * 'export' command
- */
-gcli.addCommand({
-  name: "export",
-  description: gcli.lookup("exportDesc"),
-});
-
-/**
- * The 'export html' command. This command allows the user to export the page to
- * HTML after they do DOM changes.
- */
-gcli.addCommand({
-  name: "export html",
-  description: gcli.lookup("exportHtmlDesc"),
-  exec: function(args, context) {
-    let document = context.environment.contentDocument;
-    let window = document.defaultView;
-    let page = document.documentElement.outerHTML;
-    window.open('data:text/plain;charset=utf8,' + encodeURIComponent(page));
-  }
-});
-
-/**
- * 'pagemod' command
- */
-gcli.addCommand({
-  name: "pagemod",
-  description: gcli.lookup("pagemodDesc"),
-});
+const Cu = Components.utils;
 
-/**
- * The 'pagemod replace' command. This command allows the user to search and
- * replace within text nodes and attributes.
- */
-gcli.addCommand({
-  name: "pagemod replace",
-  description: gcli.lookup("pagemodReplaceDesc"),
-  params: [
-    {
-      name: "search",
-      type: "string",
-      description: gcli.lookup("pagemodReplaceSearchDesc"),
-    },
-    {
-      name: "replace",
-      type: "string",
-      description: gcli.lookup("pagemodReplaceReplaceDesc"),
-    },
-    {
-      name: "ignoreCase",
-      type: "boolean",
-      description: gcli.lookup("pagemodReplaceIgnoreCaseDesc"),
-    },
-    {
-      name: "selector",
-      type: "string",
-      description: gcli.lookup("pagemodReplaceSelectorDesc"),
-      defaultValue: "*:not(script):not(style):not(embed):not(object):not(frame):not(iframe):not(frameset)",
-    },
-    {
-      name: "root",
-      type: "node",
-      description: gcli.lookup("pagemodReplaceRootDesc"),
-      defaultValue: null,
-    },
-    {
-      name: "attrOnly",
-      type: "boolean",
-      description: gcli.lookup("pagemodReplaceAttrOnlyDesc"),
-    },
-    {
-      name: "contentOnly",
-      type: "boolean",
-      description: gcli.lookup("pagemodReplaceContentOnlyDesc"),
-    },
-    {
-      name: "attributes",
-      type: "string",
-      description: gcli.lookup("pagemodReplaceAttributesDesc"),
-      defaultValue: null,
-    },
-  ],
-  exec: function(args, context) {
-    let document = context.environment.contentDocument;
-    let searchTextNodes = !args.attrOnly;
-    let searchAttributes = !args.contentOnly;
-    let regexOptions = args.ignoreCase ? 'ig' : 'g';
-    let search = new RegExp(escapeRegex(args.search), regexOptions);
-    let attributeRegex = null;
-    if (args.attributes) {
-      attributeRegex = new RegExp(args.attributes, regexOptions);
-    }
-
-    let root = args.root || document;
-    let elements = root.querySelectorAll(args.selector);
-    elements = Array.prototype.slice.call(elements);
-
-    let replacedTextNodes = 0;
-    let replacedAttributes = 0;
-
-    function replaceAttribute() {
-      replacedAttributes++;
-      return args.replace;
-    }
-    function replaceTextNode() {
-      replacedTextNodes++;
-      return args.replace;
-    }
-
-    for (let i = 0; i < elements.length; i++) {
-      let element = elements[i];
-      if (searchTextNodes) {
-        for (let y = 0; y < element.childNodes.length; y++) {
-          let node = element.childNodes[y];
-          if (node.nodeType == node.TEXT_NODE) {
-            node.textContent = node.textContent.replace(search, replaceTextNode);
-          }
-        }
-      }
-
-      if (searchAttributes) {
-        if (!element.attributes) {
-          continue;
-        }
-        for (let y = 0; y < element.attributes.length; y++) {
-          let attr = element.attributes[y];
-          if (!attributeRegex || attributeRegex.test(attr.name)) {
-            attr.value = attr.value.replace(search, replaceAttribute);
-          }
-        }
-      }
-    }
-
-    return gcli.lookupFormat("pagemodReplaceResult",
-                             [elements.length, replacedTextNodes,
-                              replacedAttributes]);
-  }
-});
-
-/**
- * 'pagemod remove' command
- */
-gcli.addCommand({
-  name: "pagemod remove",
-  description: gcli.lookup("pagemodRemoveDesc"),
-});
-
-
-/**
- * The 'pagemod remove element' command.
- */
-gcli.addCommand({
-  name: "pagemod remove element",
-  description: gcli.lookup("pagemodRemoveElementDesc"),
-  params: [
-    {
-      name: "search",
-      type: "string",
-      description: gcli.lookup("pagemodRemoveElementSearchDesc"),
-    },
-    {
-      name: "root",
-      type: "node",
-      description: gcli.lookup("pagemodRemoveElementRootDesc"),
-      defaultValue: null,
-    },
-    {
-      name: 'stripOnly',
-      type: 'boolean',
-      description: gcli.lookup("pagemodRemoveElementStripOnlyDesc"),
-    },
-    {
-      name: 'ifEmptyOnly',
-      type: 'boolean',
-      description: gcli.lookup("pagemodRemoveElementIfEmptyOnlyDesc"),
-    },
-  ],
-  exec: function(args, context) {
-    let document = context.environment.contentDocument;
-    let root = args.root || document;
-    let elements = Array.prototype.slice.call(root.querySelectorAll(args.search));
-
-    let removed = 0;
-    for (let i = 0; i < elements.length; i++) {
-      let element = elements[i];
-      let parentNode = element.parentNode;
-      if (!parentNode || !element.removeChild) {
-        continue;
-      }
-      if (args.stripOnly) {
-        while (element.hasChildNodes()) {
-          parentNode.insertBefore(element.childNodes[0], element);
-        }
-      }
-      if (!args.ifEmptyOnly || !element.hasChildNodes()) {
-        element.parentNode.removeChild(element);
-        removed++;
-      }
-    }
-
-    return gcli.lookupFormat("pagemodRemoveElementResultMatchedAndRemovedElements",
-                             [elements.length, removed]);
-  }
-});
-
-/**
- * The 'pagemod remove attribute' command.
- */
-gcli.addCommand({
-  name: "pagemod remove attribute",
-  description: gcli.lookup("pagemodRemoveAttributeDesc"),
-  params: [
-    {
-      name: "searchAttributes",
-      type: "string",
-      description: gcli.lookup("pagemodRemoveAttributeSearchAttributesDesc"),
-    },
-    {
-      name: "searchElements",
-      type: "string",
-      description: gcli.lookup("pagemodRemoveAttributeSearchElementsDesc"),
-    },
-    {
-      name: "root",
-      type: "node",
-      description: gcli.lookup("pagemodRemoveAttributeRootDesc"),
-      defaultValue: null,
-    },
-    {
-      name: "ignoreCase",
-      type: "boolean",
-      description: gcli.lookup("pagemodRemoveAttributeIgnoreCaseDesc"),
-    },
-  ],
-  exec: function(args, context) {
-    let document = context.environment.contentDocument;
-
-    let root = args.root || document;
-    let regexOptions = args.ignoreCase ? 'ig' : 'g';
-    let attributeRegex = new RegExp(args.searchAttributes, regexOptions);
-    let elements = root.querySelectorAll(args.searchElements);
-    elements = Array.prototype.slice.call(elements);
-
-    let removed = 0;
-    for (let i = 0; i < elements.length; i++) {
-      let element = elements[i];
-      if (!element.attributes) {
-        continue;
-      }
-
-      var attrs = Array.prototype.slice.call(element.attributes);
-      for (let y = 0; y < attrs.length; y++) {
-        let attr = attrs[y];
-        if (attributeRegex.test(attr.name)) {
-          element.removeAttribute(attr.name);
-          removed++;
-        }
-      }
-    }
-
-    return gcli.lookupFormat("pagemodRemoveAttributeResult",
-                             [elements.length, removed]);
-  }
-});
-
-
-/**
- * Make a given string safe to use  in a regular expression.
- *
- * @param string aString
- *        The string you want to use in a regex.
- * @return string
- *         The equivalent of |aString| but safe to use in a regex.
- */
-function escapeRegex(aString) {
-  return aString.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
-}
-
-/**
- * 'addon' command.
- */
-gcli.addCommand({
-  name: "addon",
-  description: gcli.lookup("addonDesc")
-});
-
-/**
- * 'addon list' command.
- */
-gcli.addCommand({
-  name: "addon list",
-  description: gcli.lookup("addonListDesc"),
-  params: [{
-    name: 'type',
-    type: {
-      name: 'selection',
-      data: ["dictionary", "extension", "locale", "plugin", "theme", "all"]
-    },
-    defaultValue: 'all',
-    description: gcli.lookup("addonListTypeDesc"),
-  }],
-  exec: function(aArgs, context) {
-    function representEnabledAddon(aAddon) {
-      return "<li><![CDATA[" + aAddon.name + "\u2002" + aAddon.version +
-      getAddonStatus(aAddon) + "]]></li>";
-    }
-
-    function representDisabledAddon(aAddon) {
-      return "<li class=\"gcli-addon-disabled\">" +
-        "<![CDATA[" + aAddon.name + "\u2002" + aAddon.version + aAddon.version +
-        "]]></li>";
-    }
-
-    function getAddonStatus(aAddon) {
-      let operations = [];
-
-      if (aAddon.pendingOperations & AddonManager.PENDING_ENABLE) {
-        operations.push("PENDING_ENABLE");
-      }
-
-      if (aAddon.pendingOperations & AddonManager.PENDING_DISABLE) {
-        operations.push("PENDING_DISABLE");
-      }
-
-      if (aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
-        operations.push("PENDING_UNINSTALL");
-      }
-
-      if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL) {
-        operations.push("PENDING_INSTALL");
-      }
-
-      if (aAddon.pendingOperations & AddonManager.PENDING_UPGRADE) {
-        operations.push("PENDING_UPGRADE");
-      }
-
-      if (operations.length) {
-        return " (" + operations.join(", ") + ")";
-      }
-      return "";
-    }
-
-    /**
-     * Compares two addons by their name. Used in sorting.
-     */
-    function compareAddonNames(aNameA, aNameB) {
-      return String.localeCompare(aNameA.name, aNameB.name);
-    }
-
-    /**
-     * Resolves the promise which is the scope (this) of this function, filling
-     * it with an HTML representation of the passed add-ons.
-     */
-    function list(aType, aAddons) {
-      if (!aAddons.length) {
-        this.resolve(gcli.lookup("addonNoneOfType"));
-      }
-
-      // Separate the enabled add-ons from the disabled ones.
-      let enabledAddons = [];
-      let disabledAddons = [];
-
-      aAddons.forEach(function(aAddon) {
-        if (aAddon.isActive) {
-          enabledAddons.push(aAddon);
-        } else {
-          disabledAddons.push(aAddon);
-        }
-      });
-
-      let header;
-      switch(aType) {
-        case "dictionary":
-          header = gcli.lookup("addonListDictionaryHeading");
-          break;
-        case "extension":
-          header = gcli.lookup("addonListExtensionHeading");
-          break;
-        case "locale":
-          header = gcli.lookup("addonListLocaleHeading");
-          break;
-        case "plugin":
-          header = gcli.lookup("addonListPluginHeading");
-          break;
-        case "theme":
-          header = gcli.lookup("addonListThemeHeading");
-        case "all":
-          header = gcli.lookup("addonListAllHeading");
-          break;
-        default:
-          header = gcli.lookup("addonListUnknownHeading");
-      }
-
-      // Map and sort the add-ons, and create an HTML list.
-      this.resolve(header +
-        "<ol>" +
-        enabledAddons.sort(compareAddonNames).map(representEnabledAddon).join("") +
-        disabledAddons.sort(compareAddonNames).map(representDisabledAddon).join("") +
-        "</ol>");
-    }
-
-    // Create the promise that will be resolved when the add-on listing has
-    // been finished.
-    let promise = context.createPromise();
-    let types = aArgs.type == "all" ? null : [aArgs.type];
-    AddonManager.getAddonsByTypes(types, list.bind(promise, aArgs.type));
-    return promise;
-  }
-});
-
-
-/**
- * 'dbg' command
- */
-gcli.addCommand({
-  name: "dbg",
-  description: gcli.lookup("dbgDesc"),
-  manual: gcli.lookup("dbgManual")
-});
-
-
-/**
- * 'dbg interrupt' command
- */
-gcli.addCommand({
-  name: "dbg interrupt",
-  description: gcli.lookup("dbgInterrupt"),
-  params: [],
-  exec: function(args, context) {
-    let win = context.environment.chromeDocument.defaultView;
-    let dbg = win.DebuggerUI.getDebugger();
-
-    if (dbg) {
-      let controller = dbg.contentWindow.DebuggerController;
-      let thread = controller.activeThread;
-      if (!thread.paused) {
-        thread.interrupt();
-      }
-    }
-  }
-});
-
-/**
- * 'dbg continue' command
- */
-gcli.addCommand({
-  name: "dbg continue",
-  description: gcli.lookup("dbgContinue"),
-  params: [],
-  exec: function(args, context) {
-    let win = context.environment.chromeDocument.defaultView;
-    let dbg = win.DebuggerUI.getDebugger();
-
-    if (dbg) {
-      let controller = dbg.contentWindow.DebuggerController;
-      let thread = controller.activeThread;
-      if (thread.paused) {
-        thread.resume();
-      }
-    }
-  }
-});
-
-
-/**
- * 'dbg step' command
- */
-gcli.addCommand({
-  name: "dbg step",
-  description: gcli.lookup("dbgStepDesc"),
-  manual: gcli.lookup("dbgStepManual")
-});
-
-
-/**
- * 'dbg step over' command
- */
-gcli.addCommand({
-  name: "dbg step over",
-  description: gcli.lookup("dbgStepOverDesc"),
-  params: [],
-  exec: function(args, context) {
-    let win = context.environment.chromeDocument.defaultView;
-    let dbg = win.DebuggerUI.getDebugger();
-
-    if (dbg) {
-      let controller = dbg.contentWindow.DebuggerController;
-      let thread = controller.activeThread;
-      if (thread.paused) {
-        thread.stepOver();
-      }
-    }
-  }
-});
-
-/**
- * 'dbg step in' command
- */
-gcli.addCommand({
-  name: 'dbg step in',
-  description: gcli.lookup("dbgStepInDesc"),
-  params: [],
-  exec: function(args, context) {
-    let win = context.environment.chromeDocument.defaultView;
-    let dbg = win.DebuggerUI.getDebugger();
-
-    if (dbg) {
-      let controller = dbg.contentWindow.DebuggerController;
-      let thread = controller.activeThread;
-      if (thread.paused) {
-        thread.stepIn();
-      }
-    }
-  }
-});
-
-/**
- * 'dbg step over' command
- */
-gcli.addCommand({
-  name: 'dbg step out',
-  description: gcli.lookup("dbgStepOutDesc"),
-  params: [],
-  exec: function(args, context) {
-    let win = context.environment.chromeDocument.defaultView;
-    let dbg = win.DebuggerUI.getDebugger();
-
-    if (dbg) {
-      let controller = dbg.contentWindow.DebuggerController;
-      let thread = controller.activeThread;
-      if (thread.paused) {
-        thread.stepOut();
-      }
-    }
-  }
-});
-
-// We need a list of addon names for the enable and disable commands. Because
-// getting the name list is async we do not add the commands until we have the
-// list.
-AddonManager.getAllAddons(function addonAsync(aAddons) {
-  // We listen for installs to keep our addon list up to date. There is no need
-  // to listen for uninstalls because uninstalled addons are simply disabled
-  // until restart (to enable undo functionality).
-  AddonManager.addAddonListener({
-    onInstalled: function(aAddon) {
-      addonNameCache.push({
-        name: representAddon(aAddon).replace(/\s/g, "_"),
-        value: aAddon.name
-      });
-    },
-    onUninstalled: function(aAddon) {
-      let name = representAddon(aAddon).replace(/\s/g, "_");
-
-      for (let i = 0; i < addonNameCache.length; i++) {
-        if(addonNameCache[i].name == name) {
-          addonNameCache.splice(i, 1);
-          break;
-        }
-      }
-    },
-  });
-
-  /**
-   * Returns a string that represents the passed add-on.
-   */
-  function representAddon(aAddon) {
-    let name = aAddon.name + " " + aAddon.version;
-    return name.trim();
-  }
-
-  let addonNameCache = [];
-
-  // The name parameter, used in "addon enable" and "addon disable."
-  let nameParameter = {
-    name: "name",
-    type: {
-      name: "selection",
-      lookup: addonNameCache
-    },
-    description: gcli.lookup("addonNameDesc")
-  };
-
-  for (let addon of aAddons) {
-    addonNameCache.push({
-      name: representAddon(addon).replace(/\s/g, "_"),
-      value: addon.name
-    });
-  }
-
-  /**
-   * 'addon enable' command.
-   */
-  gcli.addCommand({
-    name: "addon enable",
-    description: gcli.lookup("addonEnableDesc"),
-    params: [nameParameter],
-    exec: function(aArgs, context) {
-      /**
-       * Enables the addon in the passed list which has a name that matches
-       * according to the passed name comparer, and resolves the promise which
-       * is the scope (this) of this function to display the result of this
-       * enable attempt.
-       */
-      function enable(aName, addons) {
-        // Find the add-on.
-        let addon = null;
-        addons.some(function(candidate) {
-          if (candidate.name == aName) {
-            addon = candidate;
-            return true;
-          } else {
-            return false;
-          }
-        });
-
-        let name = representAddon(addon);
-
-        if (!addon.userDisabled) {
-          this.resolve("<![CDATA[" +
-            gcli.lookupFormat("addonAlreadyEnabled", [name]) + "]]>");
-        } else {
-          addon.userDisabled = false;
-          // nl-nl: {$1} is ingeschakeld.
-          this.resolve("<![CDATA[" +
-            gcli.lookupFormat("addonEnabled", [name]) + "]]>");
-        }
-      }
-
-      let promise = context.createPromise();
-      // List the installed add-ons, enable one when done listing.
-      AddonManager.getAllAddons(enable.bind(promise, aArgs.name));
-      return promise;
-    }
-  });
-
-  /**
-   * 'addon disable' command.
-   */
-  gcli.addCommand({
-    name: "addon disable",
-    description: gcli.lookup("addonDisableDesc"),
-    params: [nameParameter],
-    exec: function(aArgs, context) {
-      /**
-       * Like enable, but ... you know ... the exact opposite.
-       */
-      function disable(aName, addons) {
-        // Find the add-on.
-        let addon = null;
-        addons.some(function(candidate) {
-          if (candidate.name == aName) {
-            addon = candidate;
-            return true;
-          } else {
-            return false;
-          }
-        });
-
-        let name = representAddon(addon);
-
-        if (addon.userDisabled) {
-          this.resolve("<![CDATA[" +
-            gcli.lookupFormat("addonAlreadyDisabled", [name]) + "]]>");
-        } else {
-          addon.userDisabled = true;
-          // nl-nl: {$1} is uitgeschakeld.
-          this.resolve("<![CDATA[" +
-            gcli.lookupFormat("addonDisabled", [name]) + "]]>");
-        }
-      }
-
-      let promise = context.createPromise();
-      // List the installed add-ons, disable one when done listing.
-      AddonManager.getAllAddons(disable.bind(promise, aArgs.name));
-      return promise;
-    }
-  });
-  Services.obs.notifyObservers(null, "gcli_addon_commands_ready", null);
-});
-
-/* Responsive Mode commands */
-(function gcli_cmd_resize_container() {
-  function gcli_cmd_resize(args, context) {
-    let browserDoc = context.environment.chromeDocument;
-    let browserWindow = browserDoc.defaultView;
-    let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager;
-    mgr.handleGcliCommand(browserWindow,
-                          browserWindow.gBrowser.selectedTab,
-                          this.name,
-                          args);
-  }
-
-  gcli.addCommand({
-    name: 'resize',
-    description: gcli.lookup('resizeModeDesc')
-  });
-
-  gcli.addCommand({
-    name: 'resize on',
-    description: gcli.lookup('resizeModeOnDesc'),
-    manual: gcli.lookup('resizeModeManual'),
-    exec: gcli_cmd_resize
-  });
-
-  gcli.addCommand({
-    name: 'resize off',
-    description: gcli.lookup('resizeModeOffDesc'),
-    manual: gcli.lookup('resizeModeManual'),
-    exec: gcli_cmd_resize
-  });
-
-  gcli.addCommand({
-    name: 'resize toggle',
-    description: gcli.lookup('resizeModeToggleDesc'),
-    manual: gcli.lookup('resizeModeManual'),
-    exec: gcli_cmd_resize
-  });
-
-  gcli.addCommand({
-    name: 'resize to',
-    description: gcli.lookup('resizeModeToDesc'),
-    params: [
-      {
-        name: 'width',
-        type: 'number',
-        description: gcli.lookup("resizePageArgWidthDesc"),
-      },
-      {
-        name: 'height',
-        type: 'number',
-        description: gcli.lookup("resizePageArgHeightDesc"),
-      },
-    ],
-    exec: gcli_cmd_resize
-  });
-})();
-
-/**
- * jsb command.
- */
-gcli.addCommand({
-  name: 'jsb',
-  description: gcli.lookup('jsbDesc'),
-  returnValue:'string',
-  hidden: true,
-  params: [
-    {
-      name: 'url',
-      type: 'string',
-      description: gcli.lookup('jsbUrlDesc'),
-      manual: 'The URL of the JS to prettify'
-    },
-    {
-      name: 'indentSize',
-      type: 'number',
-      description: gcli.lookup('jsbIndentSizeDesc'),
-      manual: gcli.lookup('jsbIndentSizeManual'),
-      defaultValue: 2
-    },
-    {
-      name: 'indentChar',
-      type: {
-        name: 'selection',
-        lookup: [{name: "space", value: " "}, {name: "tab", value: "\t"}]
-      },
-      description: gcli.lookup('jsbIndentCharDesc'),
-      manual: gcli.lookup('jsbIndentCharManual'),
-      defaultValue: ' ',
-    },
-    {
-      name: 'preserveNewlines',
-      type: 'boolean',
-      description: gcli.lookup('jsbPreserveNewlinesDesc'),
-      manual: gcli.lookup('jsbPreserveNewlinesManual'),
-      defaultValue: true
-    },
-    {
-      name: 'preserveMaxNewlines',
-      type: 'number',
-      description: gcli.lookup('jsbPreserveMaxNewlinesDesc'),
-      manual: gcli.lookup('jsbPreserveMaxNewlinesManual'),
-      defaultValue: -1
-    },
-    {
-      name: 'jslintHappy',
-      type: 'boolean',
-      description: gcli.lookup('jsbJslintHappyDesc'),
-      manual: gcli.lookup('jsbJslintHappyManual'),
-      defaultValue: false
-    },
-    {
-      name: 'braceStyle',
-      type: {
-        name: 'selection',
-        data: ['collapse', 'expand', 'end-expand', 'expand-strict']
-      },
-      description: gcli.lookup('jsbBraceStyleDesc'),
-      manual: gcli.lookup('jsbBraceStyleManual'),
-      defaultValue: "collapse"
-    },
-    {
-      name: 'spaceBeforeConditional',
-      type: 'boolean',
-      description: gcli.lookup('jsbSpaceBeforeConditionalDesc'),
-      manual: gcli.lookup('jsbSpaceBeforeConditionalManual'),
-      defaultValue: true
-    },
-    {
-      name: 'unescapeStrings',
-      type: 'boolean',
-      description: gcli.lookup('jsbUnescapeStringsDesc'),
-      manual: gcli.lookup('jsbUnescapeStringsManual'),
-      defaultValue: false
-    }
-  ],
-  exec: function(args, context) {
-  let opts = {
-    indent_size: args.indentSize,
-    indent_char: args.indentChar,
-    preserve_newlines: args.preserveNewlines,
-    max_preserve_newlines: args.preserveMaxNewlines == -1 ?
-                           undefined : args.preserveMaxNewlines,
-    jslint_happy: args.jslintHappy,
-    brace_style: args.braceStyle,
-    space_before_conditional: args.spaceBeforeConditional,
-    unescape_strings: args.unescapeStrings
-  }
-
-  let xhr = new XMLHttpRequest();
-
-  try {
-    xhr.open("GET", args.url, true);
-  } catch(e) {
-    return gcli.lookup('jsbInvalidURL');
-  }
-
-  let promise = context.createPromise();
-
-  xhr.onreadystatechange = function(aEvt) {
-    if (xhr.readyState == 4) {
-      if (xhr.status == 200 || xhr.status == 0) {
-        let browserDoc = context.environment.chromeDocument;
-        let browserWindow = browserDoc.defaultView;
-        let browser = browserWindow.gBrowser;
-
-        browser.selectedTab = browser.addTab("data:text/plain;base64," +
-          browserWindow.btoa(js_beautify(xhr.responseText, opts)));
-        promise.resolve();
-      }
-      else {
-        promise.resolve("Unable to load page to beautify: " + args.url + " " +
-                        xhr.status + " " + xhr.statusText);
-      }
-    };
-  }
-  xhr.send(null);
-  return promise;
-  }
-});
+Cu.import("resource:///modules/devtools/CmdAddon.jsm");
+Cu.import("resource:///modules/devtools/CmdBreak.jsm");
+Cu.import("resource:///modules/devtools/CmdCalllog.jsm");
+Cu.import("resource:///modules/devtools/CmdCalllogChrome.jsm");
+Cu.import("resource:///modules/devtools/CmdConsole.jsm");
+Cu.import("resource:///modules/devtools/CmdCookie.jsm");
+Cu.import("resource:///modules/devtools/CmdDbg.jsm");
+Cu.import("resource:///modules/devtools/CmdEcho.jsm");
+Cu.import("resource:///modules/devtools/CmdEdit.jsm");
+Cu.import("resource:///modules/devtools/CmdExport.jsm");
+Cu.import("resource:///modules/devtools/CmdInspect.jsm");
+Cu.import("resource:///modules/devtools/CmdJsb.jsm");
+Cu.import("resource:///modules/devtools/CmdPagemod.jsm");
+Cu.import("resource:///modules/devtools/CmdResize.jsm");
+Cu.import("resource:///modules/devtools/CmdRestart.jsm");
+Cu.import("resource:///modules/devtools/CmdScreenshot.jsm");
+Cu.import("resource:///modules/devtools/CmdTilt.jsm");
rename from browser/devtools/commandline/gcli.css
rename to browser/devtools/commandline/commandline.css
rename from browser/devtools/commandline/gclioutput.xhtml
rename to browser/devtools/commandline/commandlineoutput.xhtml
--- a/browser/devtools/commandline/gclioutput.xhtml
+++ b/browser/devtools/commandline/commandlineoutput.xhtml
@@ -4,15 +4,15 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/>
-  <link rel="stylesheet" href="chrome://browser/content/devtools/gcli.css" type="text/css"/>
-  <link rel="stylesheet" href="chrome://browser/skin/devtools/gcli.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/content/devtools/commandline.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/skin/devtools/commandline.css" type="text/css"/>
 </head>
 <body class="gcli-body">
 <div id="gcli-output-root"></div>
 </body>
 </html>
rename from browser/devtools/commandline/gclitooltip.xhtml
rename to browser/devtools/commandline/commandlinetooltip.xhtml
--- a/browser/devtools/commandline/gclitooltip.xhtml
+++ b/browser/devtools/commandline/commandlinetooltip.xhtml
@@ -4,16 +4,16 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/>
-  <link rel="stylesheet" href="chrome://browser/content/devtools/gcli.css" type="text/css"/>
-  <link rel="stylesheet" href="chrome://browser/skin/devtools/gcli.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/content/devtools/commandline.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/skin/devtools/commandline.css" type="text/css"/>
 </head>
 <body class="gcli-body">
 <div id="gcli-tooltip-root"></div>
 <div id="gcli-tooltip-connector"></div>
 </body>
 </html>
--- a/browser/devtools/commandline/gcli.jsm
+++ b/browser/devtools/commandline/gcli.jsm
@@ -101,16 +101,22 @@ define('gcli/index', ['require', 'export
   require('gcli/ui/focus').startup();
   require('gcli/ui/fields/basic').startup();
   require('gcli/ui/fields/javascript').startup();
   require('gcli/ui/fields/selection').startup();
 
   require('gcli/commands/help').startup();
   require('gcli/commands/pref').startup();
 
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+  var prefSvc = "@mozilla.org/preferences-service;1";
+  var prefService = Cc[prefSvc].getService(Ci.nsIPrefService);
+  var prefBranch = prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
+
   // 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;
 
   /**
    * This code is internal and subject to change without notice.
@@ -126,16 +132,20 @@ define('gcli/index', ['require', 'export
    * - hintElement: GCLITerm.hintNode
    * - inputBackgroundElement: GCLITerm.inputStack
    */
   exports.createDisplay = function(opts) {
     var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay;
     return new FFDisplay(opts);
   };
 
+  exports.hiddenByChromePref = function() {
+    return !prefBranch.prefHasUserValue("devtools.chrome.enabled");
+  };
+
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
@@ -800,47 +810,18 @@ Conversion.prototype.toString = function
 Conversion.prototype.getPredictions = function() {
   if (typeof this.predictions === 'function') {
     return this.predictions();
   }
   return this.predictions || [];
 };
 
 /**
- * Accessor for a prediction by index.
- * This is useful above <tt>getPredictions()[index]</tt> because it normalizes
- * index to be within the bounds of the predictions, which means that the UI
- * can maintain an index of which prediction to choose without caring how many
- * predictions there are.
- * @param index The index of the prediction to choose
- */
-Conversion.prototype.getPredictionAt = function(index) {
-  if (index == null) {
-    return undefined;
-  }
-
-  var predictions = this.getPredictions();
-  if (predictions.length === 0) {
-    return undefined;
-  }
-
-  index = index % predictions.length;
-  if (index < 0) {
-    index = predictions.length + index;
-  }
-  return predictions[index];
-};
-
-/**
- * Accessor for a prediction by index.
- * This is useful above <tt>getPredictions()[index]</tt> because it normalizes
- * index to be within the bounds of the predictions, which means that the UI
- * can maintain an index of which prediction to choose without caring how many
- * predictions there are.
- * @param index The index of the prediction to choose
+ * Return an index constrained by the available predictions. Basically
+ * (index % predicitons.length)
  */
 Conversion.prototype.constrainPredictionIndex = function(index) {
   if (index == null) {
     return undefined;
   }
 
   var predictions = this.getPredictions();
   if (predictions.length === 0) {
@@ -1122,17 +1103,16 @@ exports.getType = function(typeSpec) {
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 define('gcli/argument', ['require', 'exports', 'module' ], function(require, exports, module) {
-var argument = exports;
 
 
 /**
  * Thinking out loud here:
  * Arguments are an area where we could probably refactor things a bit better.
  * The split process in Requisition creates a set of Arguments, which are then
  * assigned. The assign process sometimes converts them into subtypes of
  * Argument. We might consider that what gets assigned is _always_ one of the
@@ -1176,34 +1156,62 @@ Argument.prototype.merge = function(foll
   // Is it possible that this gets called when we're merging arguments
   // for the single string?
   return new Argument(
     this.text + this.suffix + following.prefix + following.text,
     this.prefix, following.suffix);
 };
 
 /**
- * Returns a new Argument like this one but with the text set to
- * <tt>replText</tt> and the end adjusted to fit.
- * @param replText Text to replace the old text value
- */
-Argument.prototype.beget = function(replText, options) {
+ * Returns a new Argument like this one but with various items changed.
+ * @param options Values to use in creating a new Argument.
+ * Warning: some implementations of beget make additions to the options
+ * argument. You should be aware of this in the unlikely event that you want to
+ * reuse 'options' arguments.
+ * Properties:
+ * - text: The new text value
+ * - prefixSpace: Should the prefix be altered to begin with a space?
+ * - prefixPostSpace: Should the prefix be altered to end with a space?
+ * - suffixSpace: Should the suffix be altered to end with a space?
+ * - type: Constructor to use in creating new instances. Default: Argument
+ */
+Argument.prototype.beget = function(options) {
+  var text = this.text;
   var prefix = this.prefix;
   var suffix = this.suffix;
 
-  // We need to add quotes when the replacement string has spaces or is empty
-  var quote = (replText.indexOf(' ') >= 0 || replText.length == 0) ?
-      '\'' : '';
-
-  if (options) {
-    prefix = (options.prefixSpace ? ' ' : '') + quote;
-    suffix = quote;
-  }
-
-  return new Argument(replText, prefix, suffix);
+  if (options.text != null) {
+    text = options.text;
+
+    // We need to add quotes when the replacement string has spaces or is empty
+    var needsQuote = text.indexOf(' ') >= 0 || text.length == 0;
+    if (needsQuote && /['"]/.test(prefix)) {
+      prefix = prefix + '\'';
+      suffix = '\'' + suffix;
+    }
+  }
+
+  if (options.prefixSpace && prefix.charAt(0) !== ' ') {
+    prefix = ' ' + prefix;
+  }
+
+  if (options.prefixPostSpace && prefix.charAt(prefix.length - 1) !== ' ') {
+    prefix = prefix + ' ';
+  }
+
+  if (options.suffixSpace && suffix.charAt(suffix.length - 1) !== ' ') {
+    suffix = suffix + ' ';
+  }
+
+  if (text === this.text && suffix === this.suffix && prefix === this.prefix) {
+    return this;
+  }
+
+  var type = options.type || Argument;
+  return new type(text, prefix, suffix);
 };
 
 /**
  * We need to keep track of which assignment we've been assigned to
  */
 Argument.prototype.assign = function(assignment) {
   this.assignment = assignment;
 };
@@ -1277,81 +1285,85 @@ Object.defineProperty(Argument.prototype
             'null' :
             this.assignment.param.name;
     return '<' + this.prefix + ':' + this.text + ':' + this.suffix + '>' +
         ' (a=' + assignStatus + ',' + ' t=' + this.type + ')';
   },
   enumerable: true
 });
 
-argument.Argument = Argument;
+exports.Argument = Argument;
 
 
 /**
  * BlankArgument is a marker that the argument wasn't typed but is there to
  * fill a slot. Assignments begin with their arg set to a BlankArgument.
  */
 function BlankArgument() {
   this.text = '';
   this.prefix = '';
   this.suffix = '';
 }
 
 BlankArgument.prototype = Object.create(Argument.prototype);
 
 BlankArgument.prototype.type = 'BlankArgument';
 
-argument.BlankArgument = BlankArgument;
+exports.BlankArgument = BlankArgument;
 
 
 /**
  * ScriptArgument is a marker that the argument is designed to be Javascript.
  * It also implements the special rules that spaces after the { or before the
  * } are part of the pre/suffix rather than the content, and that they are
  * never 'blank' so they can be used by Requisition._split() and not raise an
  * ERROR status due to being blank.
  */
 function ScriptArgument(text, prefix, suffix) {
   this.text = text !== undefined ? text : '';
   this.prefix = prefix !== undefined ? prefix : '';
   this.suffix = suffix !== undefined ? suffix : '';
 
-  while (this.text.charAt(0) === ' ') {
-    this.prefix = this.prefix + ' ';
-    this.text = this.text.substring(1);
-  }
-
-  while (this.text.charAt(this.text.length - 1) === ' ') {
-    this.suffix = ' ' + this.suffix;
-    this.text = this.text.slice(0, -1);
-  }
+  ScriptArgument._moveSpaces(this);
 }
 
 ScriptArgument.prototype = Object.create(Argument.prototype);
 
 ScriptArgument.prototype.type = 'ScriptArgument';
 
 /**
- * Returns a new Argument like this one but with the text set to
- * <tt>replText</tt> and the end adjusted to fit.
- * @param replText Text to replace the old text value
- */
-ScriptArgument.prototype.beget = function(replText, options) {
-  var prefix = this.prefix;
-  var suffix = this.suffix;
-
-  if (options && options.normalize) {
-    prefix = '{ ';
-    suffix = ' }';
-  }
-
-  return new ScriptArgument(replText, prefix, suffix);
-};
-
-argument.ScriptArgument = ScriptArgument;
+ * Private/Dangerous: Alters a ScriptArgument to move the spaces at the start
+ * or end of the 'text' into the prefix/suffix. With a string, " a " is 3 chars
+ * long, but with a ScriptArgument, { a } is only one char long.
+ * Arguments are generally supposed to be immutable, so this method should only
+ * be called on a ScriptArgument that isn't exposed to the outside world yet.
+ */
+ScriptArgument._moveSpaces = function(arg) {
+  while (arg.text.charAt(0) === ' ') {
+    arg.prefix = arg.prefix + ' ';
+    arg.text = arg.text.substring(1);
+  }
+
+  while (arg.text.charAt(arg.text.length - 1) === ' ') {
+    arg.suffix = ' ' + arg.suffix;
+    arg.text = arg.text.slice(0, -1);
+  }
+};
+
+/**
+ * As Argument.beget that implements the space rule documented in the ctor.
+ */
+ScriptArgument.prototype.beget = function(options) {
+  options.type = ScriptArgument;
+  var begotten = Argument.prototype.beget.call(this, options);
+  ScriptArgument._moveSpaces(begotten);
+  return begotten;
+};
+
+exports.ScriptArgument = ScriptArgument;
 
 
 /**
  * Commands like 'echo' with a single string argument, and used with the
  * special format like: 'echo a b c' effectively have a number of arguments
  * merged together.
  */
 function MergedArgument(args, start, end) {
@@ -1401,63 +1413,72 @@ MergedArgument.prototype.equals = functi
   }
 
   // We might need to add a check that args is the same here
 
   return this.text === that.text &&
        this.prefix === that.prefix && this.suffix === that.suffix;
 };
 
-argument.MergedArgument = MergedArgument;
+exports.MergedArgument = MergedArgument;
 
 
 /**
  * TrueNamedArguments are for when we have an argument like --verbose which
  * has a boolean value, and thus the opposite of '--verbose' is ''.
  */
-function TrueNamedArgument(name, arg) {
+function TrueNamedArgument(arg) {
   this.arg = arg;
-  this.text = arg ? arg.text : '--' + name;
-  this.prefix = arg ? arg.prefix : ' ';
-  this.suffix = arg ? arg.suffix : '';
+  this.text = arg.text;
+  this.prefix = arg.prefix;
+  this.suffix = arg.suffix;
 }
 
 TrueNamedArgument.prototype = Object.create(Argument.prototype);
 
 TrueNamedArgument.prototype.type = 'TrueNamedArgument';
 
 TrueNamedArgument.prototype.assign = function(assignment) {
   if (this.arg) {
     this.arg.assign(assignment);
   }
   this.assignment = assignment;
 };
 
 TrueNamedArgument.prototype.getArgs = function() {
-  // NASTY! getArgs has a fairly specific use: in removing used arguments
-  // from a command line. Unlike other arguments which are EITHER used
-  // in assignments directly OR grouped in things like MergedArguments,
-  // TrueNamedArgument is used raw from the UI, or composed of another arg
-  // from the CLI, so we return both here so they can both be removed.
-  return this.arg ? [ this, this.arg ] : [ this ];
+  return [ this.arg ];
 };
 
 TrueNamedArgument.prototype.equals = function(that) {
   if (this === that) {
     return true;
   }
   if (that == null || !(that instanceof TrueNamedArgument)) {
     return false;
   }
 
   return this.text === that.text &&
        this.prefix === that.prefix && this.suffix === that.suffix;
 };
 
-argument.TrueNamedArgument = TrueNamedArgument;
+/**
+ * As Argument.beget that rebuilds nameArg and valueArg
+ */
+TrueNamedArgument.prototype.beget = function(options) {
+  if (options.text) {
+    console.error('Can\'t change text of a TrueNamedArgument', this, options);
+  }
+
+  options.type = TrueNamedArgument;
+  var begotten = Argument.prototype.beget.call(this, options);
+  begotten.arg = new Argument(begotten.text, begotten.prefix, begotten.suffix);
+  return begotten;
+};
+
+exports.TrueNamedArgument = TrueNamedArgument;
 
 
 /**
  * FalseNamedArguments are for when we don't have an argument like --verbose
  * which has a boolean value, and thus the opposite of '' is '--verbose'.
  */
 function FalseNamedArgument() {
   this.text = '';
@@ -1480,58 +1501,73 @@ FalseNamedArgument.prototype.equals = fu
   if (that == null || !(that instanceof FalseNamedArgument)) {
     return false;
   }
 
   return this.text === that.text &&
        this.prefix === that.prefix && this.suffix === that.suffix;
 };
 
-argument.FalseNamedArgument = FalseNamedArgument;
+exports.FalseNamedArgument = FalseNamedArgument;
 
 
 /**
  * A named argument is for cases where we have input in one of the following
  * formats:
  * <ul>
  * <li>--param value
  * <li>-p value
  * </ul>
  * We model this as a normal argument but with a long prefix.
- */
-function NamedArgument(nameArg, valueArg) {
-  this.nameArg = nameArg;
-  this.valueArg = valueArg;
-
-  if (valueArg == null) {
+ *
+ * There are 2 ways to construct a NamedArgument. One using 2 Arguments which
+ * are taken to be the argument for the name (e.g. '--param') and one for the
+ * value to assign to that parameter.
+ * Alternatively, you can pass in the text/prefix/suffix values in the same
+ * way as an Argument is constructed. If you do this then you are expected to
+ * assign to nameArg and valueArg before exposing the new NamedArgument.
+ */
+function NamedArgument() {
+  if (typeof arguments[0] === 'string') {
+    this.nameArg = null;
+    this.valueArg = null;
+    this.text = arguments[0];
+    this.prefix = arguments[1];
+    this.suffix = arguments[2];
+  }
+  else if (arguments[1] == null) {
+    this.nameArg = arguments[0];
+    this.valueArg = null;
     this.text = '';
-    this.prefix = nameArg.toString();
+    this.prefix = this.nameArg.toString();
     this.suffix = '';
   }
   else {
-    this.text = valueArg.text;
-    this.prefix = nameArg.toString() + valueArg.prefix;
-    this.suffix = valueArg.suffix;
+    this.nameArg = arguments[0];
+    this.valueArg = arguments[1];
+    this.text = this.valueArg.text;
+    this.prefix = this.nameArg.toString() + this.valueArg.prefix;
+    this.suffix = this.valueArg.suffix;
   }
 }
 
 NamedArgument.prototype = Object.create(Argument.prototype);
 
 NamedArgument.prototype.type = 'NamedArgument';
 
 NamedArgument.prototype.assign = function(assignment) {
   this.nameArg.assign(assignment);
   if (this.valueArg != null) {
     this.valueArg.assign(assignment);
   }
   this.assignment = assignment;
 };
 
 NamedArgument.prototype.getArgs = function() {
-  return [ this.nameArg, this.valueArg ];
+  return this.valueArg ? [ this.nameArg, this.valueArg ] : [ this.nameArg ];
 };
 
 NamedArgument.prototype.equals = function(that) {
   if (this === that) {
     return true;
   }
   if (that == null) {
     return false;
@@ -1542,17 +1578,40 @@ NamedArgument.prototype.equals = functio
   }
 
   // We might need to add a check that nameArg and valueArg are the same
 
   return this.text === that.text &&
        this.prefix === that.prefix && this.suffix === that.suffix;
 };
 
-argument.NamedArgument = NamedArgument;
+/**
+ * As Argument.beget that rebuilds nameArg and valueArg
+ */
+NamedArgument.prototype.beget = function(options) {
+  options.type = NamedArgument;
+  var begotten = Argument.prototype.beget.call(this, options);
+
+  // Cut the prefix into |whitespace|non-whitespace|whitespace| so we can
+  // rebuild nameArg and valueArg from the parts
+  var matches = /^([\s]*)([^\s]*)([\s]*)$/.exec(begotten.prefix);
+
+  if (this.valueArg == null && begotten.text === '') {
+    begotten.nameArg = new Argument(matches[2], matches[1], matches[3]);
+    begotten.valueArg = null;
+  }
+  else {
+    begotten.nameArg = new Argument(matches[2], matches[1], '');
+    begotten.valueArg = new Argument(begotten.text, matches[3], begotten.suffix);
+  }
+
+  return begotten;
+};
+
+exports.NamedArgument = NamedArgument;
 
 
 /**
  * An argument the groups together a number of plain arguments together so they
  * can be jointly assigned to a single array parameter
  */
 function ArrayArgument() {
   this.args = [];
@@ -1615,17 +1674,17 @@ ArrayArgument.prototype.equals = functio
  * Helper when we're putting arguments back together
  */
 ArrayArgument.prototype.toString = function() {
   return '{' + this.args.map(function(arg) {
     return arg.toString();
   }, this).join(',') + '}';
 };
 
-argument.ArrayArgument = ArrayArgument;
+exports.ArrayArgument = ArrayArgument;
 
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -1792,29 +1851,40 @@ SelectionType.prototype._findPredictions
   // Cache lower case versions of all the option names
   for (i = 0; i < lookup.length; i++) {
     option = lookup[i];
     if (option._gcliLowerName == null) {
       option._gcliLowerName = option.name.toLowerCase();
     }
   }
 
+  // Exact hidden matches. If 'hidden: true' then we only allow exact matches
+  // All the tests after here check that !option.value.hidden
+  for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
+    option = lookup[i];
+    if (option.name === arg.text) {
+      this._addToPredictions(predictions, option, arg);
+    }
+  }
+
   // Start with prefix matching
   for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
     option = lookup[i];
-    if (option._gcliLowerName.indexOf(match) === 0) {
-      this._addToPredictions(predictions, option, arg);
+    if (option._gcliLowerName.indexOf(match) === 0 && !option.value.hidden) {
+      if (predictions.indexOf(option) === -1) {
+        this._addToPredictions(predictions, option, arg);
+      }
     }
   }
 
   // Try infix matching if we get less half max matched
   if (predictions.length < (maxPredictions / 2)) {
     for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) {
       option = lookup[i];
-      if (option._gcliLowerName.indexOf(match) !== -1) {
+      if (option._gcliLowerName.indexOf(match) !== -1 && !option.value.hidden) {
         if (predictions.indexOf(option) === -1) {
           this._addToPredictions(predictions, option, arg);
         }
       }
     }
   }
 
   // Try fuzzy matching if we don't get a prefix match
@@ -2186,19 +2256,16 @@ CommandType.prototype.lookup = function(
     return { name: command.name, value: command };
   }, this);
 };
 
 /**
  * Add an option to our list of predicted options
  */
 CommandType.prototype._addToPredictions = function(predictions, option, arg) {
-  if (option.value.hidden) {
-    return;
-  }
   // The command type needs to exclude sub-commands when the CLI
   // is blank, but include them when we're filtering. This hack
   // excludes matches when the filter text is '' and when the
   // name includes a space.
   if (arg.text.length !== 0 || option.name.indexOf(' ') === -1) {
     predictions.push(option);
   }
 };
@@ -2326,16 +2393,17 @@ function Command(commandSpec) {
 
   if (this.params == null) {
     this.params = [];
   }
   if (!Array.isArray(this.params)) {
     throw new Error('command.params must be an array in ' + this.name);
   }
 
+  this.hasNamedParameters = false;
   this.description = 'description' in this ? this.description : undefined;
   this.description = lookup(this.description, 'canonDescNone');
   this.manual = 'manual' in this ? this.manual : undefined;
   this.manual = lookup(this.manual);
 
   // At this point this.params has nested param groups. We want to flatten it
   // out and replace the param object literals with Parameter objects
   var paramSpecs = this.params;
@@ -2354,28 +2422,36 @@ function Command(commandSpec) {
 
   // In theory this could easily be made recursive, so param groups could
   // contain nested param groups. Current thinking is that the added
   // complexity for the UI probably isn't worth it, so this implementation
   // prevents nesting.
   paramSpecs.forEach(function(spec) {
     if (!spec.group) {
       if (usingGroups) {
-        console.error('Parameters can\'t come after param groups.' +
-            ' Ignoring ' + this.name + '/' + spec.name);
+        throw new Error('Parameters can\'t come after param groups.' +
+                        ' Ignoring ' + this.name + '/' + spec.name);
       }
       else {
         var param = new Parameter(spec, this, null);
         this.params.push(param);
+
+        if (!param.isPositionalAllowed) {
+          this.hasNamedParameters = true;
+        }
       }
     }
     else {
       spec.params.forEach(function(ispec) {
         var param = new Parameter(ispec, this, spec.group);
         this.params.push(param);
+
+        if (!param.isPositionalAllowed) {
+          this.hasNamedParameters = true;
+        }
       }, this);
 
       usingGroups = true;
     }
   }, this);
 }
 
 canon.Command = Command;
@@ -2390,69 +2466,69 @@ function Parameter(paramSpec, command, g
   this.paramSpec = paramSpec;
   this.name = this.paramSpec.name;
   this.type = this.paramSpec.type;
   this.groupName = groupName;
   this.defaultValue = this.paramSpec.defaultValue;
 
   if (!this.name) {
     throw new Error('In ' + this.command.name +
-      ': all params must have a name');
+                    ': all params must have a name');
   }
 
   var typeSpec = this.type;
   this.type = types.getType(typeSpec);
   if (this.type == null) {
     console.error('Known types: ' + types.getTypeNames().join(', '));
     throw new Error('In ' + this.command.name + '/' + this.name +
-      ': can\'t find type for: ' + JSON.stringify(typeSpec));
+                    ': can\'t find type for: ' + JSON.stringify(typeSpec));
   }
 
   // boolean parameters have an implicit defaultValue:false, which should
   // not be changed. See the docs.
   if (this.type instanceof BooleanType) {
     if (this.defaultValue !== undefined) {
-      console.error('In ' + this.command.name + '/' + this.name +
-          ': boolean parameters can not have a defaultValue.' +
-          ' Ignoring');
+      throw new Error('In ' + this.command.name + '/' + this.name +
+                      ': boolean parameters can not have a defaultValue.' +
+                      ' Ignoring');
     }
     this.defaultValue = false;
   }
 
   // Check the defaultValue for validity.
   // Both undefined and null get a pass on this test. undefined is used when
   // there is no defaultValue, and null is used when the parameter is
   // optional, neither are required to parse and stringify.
   if (this.defaultValue != null) {
     try {
       var defaultText = this.type.stringify(this.defaultValue);
       var defaultConversion = this.type.parseString(defaultText);
       if (defaultConversion.getStatus() !== Status.VALID) {
-        console.error('In ' + this.command.name + '/' + this.name +
-            ': Error round tripping defaultValue. status = ' +
-            defaultConversion.getStatus());
+        throw new Error('In ' + this.command.name + '/' + this.name +
+                        ': Error round tripping defaultValue. status = ' +
+                        defaultConversion.getStatus());
       }
     }
     catch (ex) {
-      console.error('In ' + this.command.name + '/' + this.name +
-        ': ' + ex);
+      throw new Error('In ' + this.command.name + '/' + this.name +
+                      ': ' + ex);
     }
   }
 
   // Some types (boolean, array) have a non 'undefined' blank value. Give the
   // type a chance to override the default defaultValue of undefined
   if (this.defaultValue === undefined) {
     this.defaultValue = this.type.getBlank().value;
   }
 
   // All parameters that can only be set via a named parameter must have a
   // non-undefined default value
   if (!this.isPositionalAllowed && this.defaultValue === undefined) {
-    console.error('In ' + this.command.name + '/' + this.name +
-            ': Missing defaultValue for optional parameter.');
+    throw new Error('In ' + this.command.name + '/' + this.name +
+                    ': Missing defaultValue for optional parameter.');
   }
 }
 
 /**
  * Does the given name uniquely identify this param (among the other params
  * in this command)
  * @param name The name to check
  */
@@ -2505,16 +2581,26 @@ Object.defineProperty(Parameter.prototyp
 Object.defineProperty(Parameter.prototype, 'isDataRequired', {
   get: function() {
     return this.defaultValue === undefined;
   },
   enumerable: true
 });
 
 /**
+ * Reflect the paramSpec 'hidden' property (dynamically so it can change)
+ */
+Object.defineProperty(Parameter.prototype, 'hidden', {
+  get: function() {
+    return this.paramSpec.hidden;
+  },
+  enumerable: true
+});
+
+/**
  * Are we allowed to assign data to this parameter using positional
  * parameters?
  */
 Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', {
   get: function() {
     return this.groupName == null;
   },
   enumerable: true
@@ -3930,52 +4016,64 @@ exports.JavascriptType = JavascriptType;
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gcli/types/node', ['require', 'exports', 'module' , 'gcli/host', 'gcli/l10n', 'gcli/types'], function(require, exports, module) {
+define('gcli/types/node', ['require', 'exports', 'module' , 'gcli/host', 'gcli/l10n', 'gcli/types', 'gcli/argument'], function(require, exports, module) {
 
 
 var host = require('gcli/host');
 var l10n = require('gcli/l10n');
 var types = require('gcli/types');
 var Type = require('gcli/types').Type;
 var Status = require('gcli/types').Status;
 var Conversion = require('gcli/types').Conversion;
+var BlankArgument = require('gcli/argument').BlankArgument;
 
 
 /**
  * Registration and de-registration.
  */
 exports.startup = function() {
   types.registerType(NodeType);
+  types.registerType(NodeListType);
 };
 
 exports.shutdown = function() {
   types.unregisterType(NodeType);
+  types.unregisterType(NodeListType);
 };
 
 /**
  * The object against which we complete, which is usually 'window' if it exists
  * but could be something else in non-web-content environments.
  */
 var doc;
 if (typeof document !== 'undefined') {
   doc = document;
 }
 
 /**
+ * For testing only.
+ * The fake empty NodeList used when there are no matches, we replace this with
+ * something that looks better as soon as we have a document, so not only
+ * should you not use this, but you shouldn't cache it either.
+ */
+exports._empty = [];
+
+/**
  * Setter for the document that contains the nodes we're matching
  */
 exports.setDocument = function(document) {
   doc = document;
+  exports._empty = doc.querySelectorAll('x>:root');
 };
 
 /**
  * Undo the effects of setDocument()
  */
 exports.unsetDocument = function() {
   doc = undefined;
 };
@@ -4036,16 +4134,76 @@ NodeType.prototype.parse = function(arg)
 
   return new Conversion(undefined, arg, Status.ERROR,
           l10n.lookupFormat('nodeParseMultiple', [ nodes.length ]));
 };
 
 NodeType.prototype.name = 'node';
 
 
+
+/**
+ * A CSS expression that refers to a node list.
+ *
+ * The 'allowEmpty' option ensures that we do not complain if the entered CSS
+ * selector is valid, but does not match any nodes. There is some overlap
+ * between this option and 'defaultValue'. What the user wants, in most cases,
+ * would be to use 'defaultText' (i.e. what is typed rather than the value that
+ * it represents). However this isn't a concept that exists yet and should
+ * probably be a part of GCLI if/when it does.
+ * All NodeListTypes have an automatic defaultValue of an empty NodeList so
+ * they can easily be used in named parameters.
+ */
+function NodeListType(typeSpec) {
+  if ('allowEmpty' in typeSpec && typeof typeSpec.allowEmpty !== 'boolean') {
+    throw new Error('Legal values for allowEmpty are [true|false]');
+  }
+
+  this.allowEmpty = typeSpec.allowEmpty;
+}
+
+NodeListType.prototype = Object.create(Type.prototype);
+
+NodeListType.prototype.getBlank = function() {
+  return new Conversion(exports._empty, new BlankArgument(), Status.VALID);
+};
+
+NodeListType.prototype.stringify = function(value) {
+  if (value == null) {
+    return '';
+  }
+  return value.__gcliQuery || 'Error';
+};
+
+NodeListType.prototype.parse = function(arg) {
+  if (arg.text === '') {
+    return new Conversion(undefined, arg, Status.INCOMPLETE);
+  }
+
+  var nodes;
+  try {
+    nodes = doc.querySelectorAll(arg.text);
+  }
+  catch (ex) {
+    return new Conversion(undefined, arg, Status.ERROR,
+            l10n.lookup('nodeParseSyntax'));
+  }
+
+  if (nodes.length === 0 && !this.allowEmpty) {
+    return new Conversion(undefined, arg, Status.INCOMPLETE,
+        l10n.lookup('nodeParseNone'));
+  }
+
+  host.flashNodes(nodes, false);
+  return new Conversion(nodes, arg, Status.VALID, '');
+};
+
+NodeListType.prototype.name = 'nodelist';
+
+
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
@@ -5072,16 +5230,57 @@ Assignment.prototype.getMessage = functi
  * @return An array of objects with name and value elements. For example:
  * [ { name:'bestmatch', value:foo1 }, { name:'next', value:foo2 }, ... ]
  */
 Assignment.prototype.getPredictions = function() {
   return this.conversion.getPredictions();
 };
 
 /**
+ * Accessor for a prediction by index.
+ * This is useful above <tt>getPredictions()[index]</tt> because it normalizes
+ * index to be within the bounds of the predictions, which means that the UI
+ * can maintain an index of which prediction to choose without caring how many
+ * predictions there are.
+ * @param index The index of the prediction to choose
+ */
+Assignment.prototype.getPredictionAt = function(index) {
+  if (index == null) {
+    index = 0;
+  }
+
+  if (this.isInName()) {
+    return undefined;
+  }
+
+  var predictions = this.getPredictions();
+  if (predictions.length === 0) {
+    return undefined;
+  }
+
+  index = index % predictions.length;
+  if (index < 0) {
+    index = predictions.length + index;
+  }
+  return predictions[index];
+};
+
+/**
+ * Some places want to take special action if we are in the name part of a
+ * named argument (i.e. the '--foo' bit).
+ * Currently this does not take actual cursor position into account, it just
+ * assumes that the cursor is at the end. In the future we will probably want
+ * to take this into account.
+ */
+Assignment.prototype.isInName = function() {
+  return this.conversion.arg.type === 'NamedArgument' &&
+         this.conversion.arg.prefix.slice(-1) !== ' ';
+};
+
+/**
  * Report on the status of the last parse() conversion.
  * We force mutations to happen through this method rather than have
  * setValue and setArgument functions to help maintain integrity when we
  * have ArrayArguments and don't want to get confused. This way assignments
  * are just containers for a conversion rather than things that store
  * a connection between an arg/value.
  * @see types.Conversion
  */
@@ -5118,17 +5317,18 @@ Assignment.prototype.ensureVisibleArgume
   // It should only be called when structural changes are happening in which
   // case we're going to ignore the event anyway. But on the other hand
   // perhaps this function shouldn't need to know how it is used, and should
   // do the inefficient thing.
   if (this.conversion.arg.type !== 'BlankArgument') {
     return false;
   }
 
-  var arg = this.conversion.arg.beget('', {
+  var arg = this.conversion.arg.beget({
+    text: '',
     prefixSpace: this.param instanceof CommandAssignment
   });
   this.conversion = this.param.type.parse(arg);
   this.conversion.assign(this);
 
   return true;
 };
 
@@ -5150,42 +5350,16 @@ Assignment.prototype.getStatus = functio
   if (!this.param.isDataRequired && this.arg.type === 'BlankArgument') {
     return Status.VALID;
   }
 
   return this.conversion.getStatus(arg);
 };
 
 /**
- * Replace the current value with the lower value if such a concept exists.
- */
-Assignment.prototype.decrement = function() {
-  var replacement = this.param.type.decrement(this.conversion.value);
-  if (replacement != null) {
-    var str = this.param.type.stringify(replacement);
-    var arg = this.conversion.arg.beget(str);
-    var conversion = new Conversion(replacement, arg);
-    this.setConversion(conversion);
-  }
-};
-
-/**
- * Replace the current value with the higher value if such a concept exists.
- */
-Assignment.prototype.increment = function() {
-  var replacement = this.param.type.increment(this.conversion.value);
-  if (replacement != null) {
-    var str = this.param.type.stringify(replacement);
-    var arg = this.conversion.arg.beget(str);
-    var conversion = new Conversion(replacement, arg);
-    this.setConversion(conversion);
-  }
-};
-
-/**
  * Helper when we're rebuilding command lines.
  */
 Assignment.prototype.toString = function() {
   return this.conversion.toString();
 };
 
 /**
  * For test/debug use only. The output from this function is subject to wanton
@@ -5393,22 +5567,16 @@ function Requisition(environment, doc) {
 
   this.commandOutputManager = canon.commandOutputManager;
 
   this.onAssignmentChange = util.createEvent('Requisition.onAssignmentChange');
   this.onTextChange = util.createEvent('Requisition.onTextChange');
 }
 
 /**
- * Some number that is higher than the most args we'll ever have. Would use
- * MAX_INTEGER if that made sense
- */
-var MORE_THAN_THE_MOST_ARGS_POSSIBLE = 1000000;
-
-/**
  * Avoid memory leaks
  */
 Requisition.prototype.destroy = function() {
   this.commandAssignment.onAssignmentChange.remove(this._commandAssignmentChanged, this);
   this.commandAssignment.onAssignmentChange.remove(this._assignmentChanged, this);
 
   delete this.document;
   delete this.environment;
@@ -5432,57 +5600,16 @@ Requisition.prototype._assignmentChanged
   this.onAssignmentChange(ev);
 
   // Both for argument position and the onTextChange event, we only care
   // about changes to the argument.
   if (ev.conversion.argEquals(ev.oldConversion)) {
     return;
   }
 
-  this._structuralChangeInProgress = true;
-
-  // Refactor? See bug 660765
-  // Do preceding arguments need to have dummy values applied so we don't
-  // get a hole in the command line?
-  var i;
-  if (ev.assignment.param.isPositionalAllowed) {
-    for (i = 0; i < ev.assignment.paramIndex; i++) {
-      var assignment = this.getAssignment(i);
-      if (assignment.param.isPositionalAllowed) {
-        if (assignment.ensureVisibleArgument()) {
-          this._args.push(assignment.arg);
-        }
-      }
-    }
-  }
-
-  // Remember where we found the first match
-  var index = MORE_THAN_THE_MOST_ARGS_POSSIBLE;
-  for (i = 0; i < this._args.length; i++) {
-    if (this._args[i].assignment === ev.assignment) {
-      if (i < index) {
-        index = i;
-      }
-      this._args.splice(i, 1);
-      i--;
-    }
-  }
-
-  if (index === MORE_THAN_THE_MOST_ARGS_POSSIBLE) {
-    this._args.push(ev.assignment.arg);
-  }
-  else {
-    // Is there a way to do this that doesn't involve a loop?
-    var newArgs = ev.conversion.arg.getArgs();
-    for (i = 0; i < newArgs.length; i++) {
-      this._args.splice(index + i, 0, newArgs[i]);
-    }
-  }
-  this._structuralChangeInProgress = false;
-
   this.onTextChange();
 };
 
 /**
  * When the command changes, we need to keep a bunch of stuff in sync
  */
 Requisition.prototype._commandAssignmentChanged = function(ev) {
   // Assignments fire AssignmentChange events on any change, including minor
@@ -5514,16 +5641,38 @@ Requisition.prototype._commandAssignment
 Requisition.prototype.getAssignment = function(nameOrNumber) {
   var name = (typeof nameOrNumber === 'string') ?
     nameOrNumber :
     Object.keys(this._assignments)[nameOrNumber];
   return this._assignments[name] || undefined;
 };
 
 /**
+ * There are a few places where we need to know what the 'next thing' is. What
+ * is the user going to be filling out next (assuming they don't enter a named
+ * argument). The next argument is the first in line that is both blank, and
+ * that can be filled in positionally.
+ * @return The next assignment to be used, or null if all the positional
+ * parameters have values.
+ */
+Requisition.prototype._getFirstBlankPositionalAssignment = function() {
+  var reply = null;
+  Object.keys(this._assignments).some(function(name) {
+    var assignment = this.getAssignment(name);
+    if (assignment.arg.type === 'BlankArgument' &&
+            assignment.param.isPositionalAllowed) {
+      reply = assignment;
+      return true; // i.e. break
+    }
+    return false;
+  }, this);
+  return reply;
+};
+
+/**
  * Where parameter name == assignment names - they are the same
  */
 Requisition.prototype.getParameterNames = function() {
   return Object.keys(this._assignments);
 };
 
 /**
  * A *shallow* clone of the assignments.
@@ -5599,29 +5748,44 @@ Requisition.prototype.getAssignments = f
 };
 
 /**
  * Alter the given assignment using the given arg. This function is better than
  * calling assignment.setConversion(assignment.param.type.parse(arg)) because
  * it adjusts the args in this requisition to keep things up to date
  */
 Requisition.prototype.setAssignment = function(assignment, arg) {
-  var originalArg = assignment.arg;
+  var originalArgs = assignment.arg.getArgs();
   var conversion = assignment.param.type.parse(arg);
   assignment.setConversion(conversion);
 
-  // If this argument isn't assigned to anything (i.e. it was created by
-  // assignment.setBlank) we need to add it into the _args array so
-  // requisition.toString can make sense
-  if (originalArg.type === 'BlankArgument') {
-    this._args.push(arg);
-  }
-  else {
-    var index = this._args.indexOf(originalArg);
-    this._args[index] = conversion.arg;
+  var replacementArgs = arg.getArgs();
+  var maxLen = Math.max(originalArgs.length, replacementArgs.length);
+  for (var i = 0; i < maxLen; i++) {
+    // If there are no more original args, or if the original arg was blank
+    // (i.e. not typed by the user), we'll just need to add at the end
+    if (i >= originalArgs.length || originalArgs[i].type === 'BlankArgument') {
+      this._args.push(replacementArgs[i]);
+      continue;
+    }
+
+    var index = this._args.indexOf(originalArgs[i]);
+    if (index === -1) {
+      console.error('Couldn\'t find ', originalArgs[i], ' in ', this._args);
+      throw new Error('Couldn\'t find ' + originalArgs[i]);
+    }
+
+    // If there are no more replacement args, we just remove the original args
+    // Otherwise swap original args and replacements
+    if (i >= replacementArgs.length) {
+      this._args.splice(index, 1);
+    }
+    else {
+      this._args[index] = replacementArgs[i];
+    }
   }
 };
 
 /**
  * Reset all the assignments to their default values
  */
 Requisition.prototype.setBlankArguments = function() {
   this.getAssignments().forEach(function(assignment) {
@@ -5640,74 +5804,92 @@ Requisition.prototype.setBlankArguments 
  * which should be set to start and end of the selection.
  * @param predictionChoice The index of the prediction that we should choose.
  * This number is not bounded by the size of the prediction array, we take the
  * modulus to get it within bounds
  */
 Requisition.prototype.complete = function(cursor, predictionChoice) {
   var assignment = this.getAssignmentAt(cursor.start);
 
-  var predictions = assignment.conversion.getPredictions();
-  if (predictions.length > 0) {
-    this.onTextChange.holdFire();
-
-    var prediction = assignment.conversion.getPredictionAt(predictionChoice);
-
+  this.onTextChange.holdFire();
+
+  var prediction = assignment.getPredictionAt(predictionChoice);
+  if (prediction == null) {
+    // No predictions generally means we shouldn't change anything on TAB, but
+    // TAB has the connotation of 'next thing' and when we're at the end of
+    // a thing that implies that we should add a space. i.e.
+    // 'help<TAB>' -> 'help '
+    // But we should only do this if the thing that we're 'completing' is valid
+    // and doesn't already end in a space.
+    if (assignment.arg.suffix.slice(-1) !== ' ' &&
+            assignment.getStatus() === Status.VALID) {
+      this._addSpace(assignment);
+    }
+
+    // Also add a space if we are in the name part of an assignment, however
+    // this time we don't want the 'push the space to the next assignment'
+    // logic, so we don't use addSpace
+    if (assignment.isInName()) {
+      var newArg = assignment.conversion.arg.beget({ prefixPostSpace: true });
+      this.setAssignment(assignment, newArg);
+    }
+  }
+  else {
     // Mutate this argument to hold the completion
-    var arg = assignment.arg.beget(prediction.name);
+    var arg = assignment.arg.beget({ text: prediction.name });
     this.setAssignment(assignment, arg);
 
-    if (prediction.incomplete) {
-      // This is the easy case - the prediction is incomplete - no need to add
-      // any spaces
-      return;
-    }
-
-    // The prediction reported !incomplete, which means it's complete so we
-    // should add a space to delimit this argument and let the user move-on.
-    // The question is, where does the space go? The obvious thing to do is to
-    // add it to the suffix of the completed argument, but that's wrong because
-    // spaces are attached to the start of the next argument rather than the
-    // end of the previous one (and this matters to getCurrentAssignment).
-    // However there might not be a next argument (if we've at the end of the
-    // input), in which case we really do use this one.
-    // Also if there is already a space in those positions, don't add another
-
-    var nextIndex = assignment.paramIndex + 1;
-    var nextAssignment = this.getAssignment(nextIndex);
-    if (nextAssignment) {
-      // Add a space onto the next argument (if there isn't one there already)
-      var nextArg = nextAssignment.conversion.arg;
-      if (nextArg.prefix.charAt(0) !== ' ') {
-        nextArg = new Argument(nextArg.text, ' ' + nextArg.prefix, nextArg.suffix);
-        this.setAssignment(nextAssignment, nextArg);
+    if (!prediction.incomplete) {
+      // The prediction is complete, add a space to let the user move-on
+      this._addSpace(assignment);
+
+      // Bug 779443 - Remove or explain the reparse
+      if (assignment instanceof UnassignedAssignment) {
+        this.update(this.toString());
       }
     }
-    else {
-      // There is no next argument, this must be the last assignment, so just
-      // add the space to the prefix of this argument
-      arg = assignment.conversion.arg;
-      if (arg.suffix.charAt(arg.suffix.length - 1) !== ' ') {
-        // It's tempting to think - "we're calling setAssignment twice in one
-        // call to complete, the first time to complete the text, the second
-        // to add a space, why not save the event cascade and do it once"
-        // However if we're setting up the command, the number of parameters
-        // changes as a result, so our call to getAssignment(nextIndex) will
-        // produce the wrong answer
-        arg = new Argument(arg.text, arg.prefix, arg.suffix + ' ');
-        this.setAssignment(assignment, arg);
-      }
-    }
-
-    if (assignment instanceof UnassignedAssignment) {
-      this.update(this.toString());
-    }
-
-    this.onTextChange();
-    this.onTextChange.resumeFire();
+  }
+
+  this.onTextChange();
+  this.onTextChange.resumeFire();
+};
+
+/**
+ * Pressing TAB sometimes requires that we add a space to denote that we're on
+ * to the 'next thing'.
+ * @param assignment The assignment to which to append the space
+ */
+Requisition.prototype._addSpace = function(assignment) {
+  var arg = assignment.conversion.arg.beget({ suffixSpace: true });
+  if (arg !== assignment.conversion.arg) {
+    this.setAssignment(assignment, arg);
+  }
+};
+
+/**
+ * Replace the current value with the lower value if such a concept exists.
+ */
+Requisition.prototype.decrement = function(assignment) {
+  var replacement = assignment.param.type.decrement(assignment.conversion.value);
+  if (replacement != null) {
+    var str = assignment.param.type.stringify(replacement);
+    var arg = assignment.conversion.arg.beget({ text: str });
+    this.setAssignment(assignment, arg);
+  }
+};
+
+/**
+ * Replace the current value with the higher value if such a concept exists.
+ */
+Requisition.prototype.increment = function(assignment) {
+  var replacement = assignment.param.type.increment(assignment.conversion.value);
+  if (replacement != null) {
+    var str = assignment.param.type.stringify(replacement);
+    var arg = assignment.conversion.arg.beget({ text: str });
+    this.setAssignment(assignment, arg);
   }
 };
 
 /**
  * Extract a canonical version of the input
  */
 Requisition.prototype.toCanonicalString = function() {
   var line = [];
@@ -5917,19 +6099,22 @@ Requisition.prototype.getAssignmentAt = 
     // otherwise it looks forwards
     if (arg.assignment.arg.type === 'NamedArgument') {
       // leave the argument as it is
     }
     else if (this._args.length > i + 1) {
       // first to the next argument
       assignment = this._args[i + 1].assignment;
     }
-    else if (assignment && assignment.paramIndex + 1 < this.assignmentCount) {
-      // then to the next assignment
-      assignment = this.getAssignment(assignment.paramIndex + 1);
+    else {
+      // then to the first blank positional parameter, leaving 'as is' if none
+      var nextAssignment = this._getFirstBlankPositionalAssignment();
+      if (nextAssignment != null) {
+        assignment = nextAssignment;
+      }
     }
 
     for (j = 0; j < arg.suffix.length; j++) {
       assignForPos.push(assignment);
     }
   }
 
   // Possible shortcut, we don't really need to go through all the args
@@ -6014,17 +6199,17 @@ Requisition.prototype.exec = function(in
   });
 
   this.commandOutputManager.onOutput({ output: output });
 
   try {
     var context = exports.createExecutionContext(this);
     var reply = command.exec(args, context);
 
-    if (reply != null && reply.isPromise) {
+    if (reply != null && typeof reply.then === 'function') {
       reply.then(
           function(data) { output.complete(data); },
           function(error) { output.error = true; output.complete(error); });
 
       output.promise = reply;
       // Add progress to our promise and add a handler for it here
       // See bug 659300
     }
@@ -6430,17 +6615,17 @@ Requisition.prototype._assign = function
       if (assignment.param.isKnownAs(args[i].text)) {
         var arg = args.splice(i, 1)[0];
         unassignedParams = unassignedParams.filter(function(test) {
           return test !== assignment.param.name;
         });
 
         // boolean parameters don't have values, default to false
         if (assignment.param.type instanceof BooleanType) {
-          arg = new TrueNamedArgument(null, arg);
+          arg = new TrueNamedArgument(arg);
         }
         else {
           var valueArg = null;
           if (i + 1 <= args.length) {
             valueArg = args.splice(i, 1)[0];
           }
           arg = new NamedArgument(arg, valueArg);
         }
@@ -6841,16 +7026,21 @@ FocusManager.prototype.addMonitoredEleme
     element: element,
     where: where,
     onFocus: function() { this._reportFocus(where); }.bind(this),
     onBlur: function() { this._reportBlur(where); }.bind(this)
   };
 
   element.addEventListener('focus', monitor.onFocus, true);
   element.addEventListener('blur', monitor.onBlur, true);
+
+  if (this._document.activeElement === element) {
+    this._reportFocus(where);
+  }
+
   this._monitoredElements.push(monitor);
 };
 
 /**
  * Undo the effects of addMonitoredElement()
  * @param element The element to stop tracking
  * @param where Optional source string for debugging only
  */
@@ -6958,19 +7148,19 @@ FocusManager.prototype._reportBlur = fun
  * The setting has changed
  */
 FocusManager.prototype._eagerHelperChanged = function() {
   this._checkShow();
 };
 
 /**
  * The inputter tells us about keyboard events so we can decide to delay
- * showing the tooltip element, (or if the keypress is F1, show it now)
- */
-FocusManager.prototype.onInputChange = function(ev) {
+ * showing the tooltip element
+ */
+FocusManager.prototype.onInputChange = function() {
   this._recentOutput = false;
   this._checkShow();
 };
 
 /**
  * Generally called for something like a F1 key press, when the user explicitly
  * wants help
  */
@@ -7054,25 +7244,25 @@ FocusManager.prototype._checkShow = func
 };
 
 /**
  * Calculate if we should be showing or hidden taking into account all the
  * available inputs
  */
 FocusManager.prototype._shouldShowTooltip = function() {
   if (!this._hasFocus) {
-    return { visible: false, reason: '!hasFocus' };
+    return { visible: false, reason: 'notHasFocus' };
   }
 
   if (eagerHelper.value === Eagerness.NEVER) {
-    return { visible: false, reason: 'eagerHelper !== NEVER' };
+    return { visible: false, reason: 'eagerHelperNever' };
   }
 
   if (eagerHelper.value === Eagerness.ALWAYS) {
-    return { visible: true, reason: 'eagerHelper !== ALWAYS' };
+    return { visible: true, reason: 'eagerHelperAlways' };
   }
 
   if (this._isError) {
     return { visible: true, reason: 'isError' };
   }
 
   if (this._helpRequested) {
     return { visible: true, reason: 'helpRequested' };
@@ -7086,17 +7276,17 @@ FocusManager.prototype._shouldShowToolti
 };
 
 /**
  * Calculate if we should be showing or hidden taking into account all the
  * available inputs
  */
 FocusManager.prototype._shouldShowOutput = function() {
   if (!this._hasFocus) {
-    return { visible: false, reason: '!hasFocus' };
+    return { visible: false, reason: 'notHasFocus' };
   }
 
   if (this._recentOutput) {
     return { visible: true, reason: 'recentOutput' };
   }
 
   return { visible: false, reason: 'default' };
 };
@@ -7195,17 +7385,17 @@ StringField.prototype.destroy = function
 StringField.prototype.setConversion = function(conversion) {
   this.arg = conversion.arg;
   this.element.value = conversion.arg.text;
   this.setMessage(conversion.message);
 };
 
 StringField.prototype.getConversion = function() {
   // This tweaks the prefix/suffix of the argument to fit
-  this.arg = this.arg.beget(this.element.value, { prefixSpace: true });
+  this.arg = this.arg.beget({ text: this.element.value, prefixSpace: true });
   return this.type.parse(this.arg);
 };
 
 StringField.claim = function(type) {
   return type instanceof StringType ? Field.MATCH : Field.BASIC;
 };
 
 
@@ -7250,17 +7440,17 @@ NumberField.prototype.destroy = function
 
 NumberField.prototype.setConversion = function(conversion) {
   this.arg = conversion.arg;
   this.element.value = conversion.arg.text;
   this.setMessage(conversion.message);
 };
 
 NumberField.prototype.getConversion = function() {
-  this.arg = this.arg.beget(this.element.value, { prefixSpace: true });
+  this.arg = this.arg.beget({ text: this.element.value, prefixSpace: true });
   return this.type.parse(this.arg);
 };
 
 
 /**
  * A field that uses a checkbox to toggle a boolean field
  */
 function BooleanField(type, options) {
@@ -7297,17 +7487,17 @@ BooleanField.prototype.setConversion = f
   this.element.checked = conversion.value;
   this.setMessage(conversion.message);
 };
 
 BooleanField.prototype.getConversion = function() {
   var arg;
   if (this.named) {
     arg = this.element.checked ?
-            new TrueNamedArgument(this.name) :
+            new TrueNamedArgument(new Argument(' --' + this.name)) :
             new FalseNamedArgument();
   }
   else {
     arg = new Argument(' ' + this.element.checked);
   }
   return this.type.parse(arg);
 };
 
@@ -7749,17 +7939,17 @@ exports.addField(BlankField);
  * limitations under the License.
  */
 
 define('gcli/ui/fields/javascript', ['require', 'exports', 'module' , 'gcli/util', 'gcli/argument', 'gcli/types/javascript', 'gcli/ui/fields/menu', 'gcli/ui/fields'], function(require, exports, module) {
 
 
 var util = require('gcli/util');
 
-var Argument = require('gcli/argument').Argument;
+var ScriptArgument = require('gcli/argument').ScriptArgument;
 var JavascriptType = require('gcli/types/javascript').JavascriptType;
 
 var Menu = require('gcli/ui/fields/menu').Menu;
 var Field = require('gcli/ui/fields').Field;
 var fields = require('gcli/ui/fields');
 
 
 /**
@@ -7776,17 +7966,17 @@ exports.shutdown = function() {
 
 /**
  * A field that allows editing of javascript
  */
 function JavascriptField(type, options) {
   Field.call(this, type, options);
 
   this.onInputChange = this.onInputChange.bind(this);
-  this.arg = new Argument('', '{ ', ' }');
+  this.arg = new ScriptArgument('', '{ ', ' }');
 
   this.element = util.createElement(this.document, 'div');
 
   this.input = util.createElement(this.document, 'input');
   this.input.type = 'text';
   this.input.addEventListener('keyup', this.onInputChange, false);
   this.input.classList.add('gcli-field');
   this.input.classList.add('gcli-field-javascript');
@@ -7794,17 +7984,17 @@ function JavascriptField(type, options) 
 
   this.menu = new Menu({
     document: this.document,
     field: true,
     type: type
   });
   this.element.appendChild(this.menu.element);
 
-  this.setConversion(this.type.parse(new Argument('')));
+  this.setConversion(this.type.parse(new ScriptArgument('')));
 
   this.onFieldChange = util.createEvent('JavascriptField.onFieldChange');
 
   // i.e. Register this.onItemClick as the default action for a menu click
   this.menu.onItemClick.add(this.itemClicked, this);
 }
 
 JavascriptField.prototype = Object.create(Field.prototype);
@@ -7864,17 +8054,17 @@ JavascriptField.prototype.onInputChange 
   this.item = ev.currentTarget.item;
   var conversion = this.getConversion();
   this.onFieldChange({ conversion: conversion });
   this.setMessage(conversion.message);
 };
 
 JavascriptField.prototype.getConversion = function() {
   // This tweaks the prefix/suffix of the argument to fit
-  this.arg = this.arg.beget(this.input.value, { normalize: true });
+  this.arg = new ScriptArgument(this.input.value, '{ ', ' }');
   return this.type.parse(this.arg);
 };
 
 JavascriptField.DEFAULT_VALUE = '__JavascriptField.DEFAULT_VALUE';
 
 
 });
 /*
@@ -8066,27 +8256,31 @@ Menu.prototype.setChoiceIndex = function
   }
 
   nodes.item(choice).classList.add('gcli-menu-highlight');
 };
 
 /**
  * Allow the inputter to use RETURN to chose the current menu item when
  * it can't execute the command line
+ * @return true if an item was 'clicked', false otherwise
  */
 Menu.prototype.selectChoice = function() {
   var selected = this.element.querySelector('.gcli-menu-highlight .gcli-menu-name');
-  if (selected) {
-    var name = selected.innerHTML;
-    var arg = new Argument(name);
-    arg.suffix = ' ';
-
-    var conversion = this.type.parse(arg);
-    this.onItemClick({ conversion: conversion });
-  }
+  if (!selected) {
+    return false;
+  }
+
+  var name = selected.innerHTML;
+  var arg = new Argument(name);
+  arg.suffix = ' ';
+
+  var conversion = this.type.parse(arg);
+  this.onItemClick({ conversion: conversion });
+  return true;
 };
 
 /**
  * Hide the menu
  */
 Menu.prototype.hide = function() {
   this.element.style.display = 'none';
 };
@@ -8234,17 +8428,17 @@ SelectionField.prototype._addOption = fu
   var option = util.createElement(this.document, 'option');
   option.innerHTML = item.name;
   option.value = item.index;
   this.element.appendChild(option);
 };
 
 
 /**
- * A field that allows editing of javascript
+ * A field that allows selection of one of a number of options
  */
 function SelectionTooltipField(type, options) {
   Field.call(this, type, options);
 
   this.onInputChange = this.onInputChange.bind(this);
   this.arg = new Argument();
 
   this.menu = new Menu({ document: this.document, type: type });
@@ -8292,33 +8486,34 @@ SelectionTooltipField.prototype.onInputC
   this.item = ev.currentTarget.item;
   var conversion = this.getConversion();
   this.onFieldChange({ conversion: conversion });
   this.setMessage(conversion.message);
 };
 
 SelectionTooltipField.prototype.getConversion = function() {
   // This tweaks the prefix/suffix of the argument to fit
-  this.arg = this.arg.beget('typed', { normalize: true });
+  this.arg = this.arg.beget({ text: this.input.value });
   return this.type.parse(this.arg);
 };
 
 /**
  * Allow the menu to highlight the correct prediction choice
  */
 SelectionTooltipField.prototype.setChoiceIndex = function(choice) {
   this.menu.setChoiceIndex(choice);
 };
 
 /**
  * Allow the inputter to use RETURN to chose the current menu item when
  * it can't execute the command line
+ * @return true if an item was 'clicked', false otherwise
  */
 SelectionTooltipField.prototype.selectChoice = function() {
-  this.menu.selectChoice();
+  return this.menu.selectChoice();
 };
 
 Object.defineProperty(SelectionTooltipField.prototype, 'isImportant', {
   get: function() {
     return this.type.name !== 'command';
   },
   enumerable: true
 });
@@ -9341,17 +9536,17 @@ Inputter.prototype.onKeyDown = function(
   if (ev.keyCode === KeyEvent.DOM_VK_F1 ||
       ev.keyCode === KeyEvent.DOM_VK_ESCAPE ||
       ev.keyCode === KeyEvent.DOM_VK_UP ||
       ev.keyCode === KeyEvent.DOM_VK_DOWN) {
     return;
   }
 
   if (this.focusManager) {
-    this.focusManager.onInputChange(ev);
+    this.focusManager.onInputChange();
   }
 
   if (ev.keyCode === KeyEvent.DOM_VK_TAB) {
     this.lastTabDownAt = 0;
     if (!ev.shiftKey) {
       ev.preventDefault();
       // Record the timestamp of this TAB down so onKeyUp can distinguish
       // focus from TAB in the CLI.
@@ -9389,20 +9584,20 @@ Inputter.prototype.onKeyUp = function(ev
     else if (this.element.value === '' || this._scrollingThroughHistory) {
       this._scrollingThroughHistory = true;
       this.requisition.update(this.history.backward());
     }
     else {
       // If the user is on a valid value, then we increment the value, but if
       // they've typed something that's not right we page through predictions
       if (this.assignment.getStatus() === Status.VALID) {
-        this.assignment.increment();
+        this.requisition.increment(assignment);
         // See notes on focusManager.onInputChange in onKeyDown
         if (this.focusManager) {
-          this.focusManager.onInputChange(ev);
+          this.focusManager.onInputChange();
         }
       }
       else {
         this.changeChoice(-1);
       }
     }
     return;
   }
@@ -9413,20 +9608,20 @@ Inputter.prototype.onKeyUp = function(ev
     }
     else if (this.element.value === '' || this._scrollingThroughHistory) {
       this._scrollingThroughHistory = true;
       this.requisition.update(this.history.forward());
     }
     else {
       // See notes above for the UP key
       if (this.assignment.getStatus() === Status.VALID) {
-        this.assignment.decrement();
+        this.requisition.decrement(assignment);
         // See notes on focusManager.onInputChange in onKeyDown
         if (this.focusManager) {
-          this.focusManager.onInputChange(ev);
+          this.focusManager.onInputChange();
         }
       }
       else {
         this.changeChoice(+1);
       }
     }
     return;
   }
@@ -9438,20 +9633,19 @@ Inputter.prototype.onKeyUp = function(ev
     if (worst === Status.VALID) {
       this._scrollingThroughHistory = false;
       this.history.add(this.element.value);
       this.requisition.exec();
     }
     else {
       // If we can't execute the command, but there is a menu choice to use
       // then use it.
-      this.tooltip.selectChoice();
-
-      // See bug 664135 - On pressing return with an invalid input, GCLI
-      // should select the incorrect part of the input for an easy fix
+      if (!this.tooltip.selectChoice()) {
+        this.focusManager.setError(true);
+      }
     }
 
     this._choice = null;
     return;
   }
 
   if (ev.keyCode === KeyEvent.DOM_VK_TAB && !ev.shiftKey) {
     // Being able to complete 'nothing' is OK if there is some context, but
@@ -9750,101 +9944,132 @@ Completer.prototype.update = function(ev
 Completer.prototype._getCompleterTemplateData = function() {
   var input = this.inputter.getInputState();
 
   // directTabText is for when the current input is a prefix of the completion
   // arrowTabText is for when we need to use an -> to show what will be used
   var directTabText = '';
   var arrowTabText = '';
   var current = this.requisition.getAssignmentAt(input.cursor.start);
+  var emptyParameters = [];
 
   if (input.typed.trim().length !== 0) {
-    var prediction = current.conversion.getPredictionAt(this.choice);
+    var cArg = current.arg;
+    var prediction = current.getPredictionAt(this.choice);
+
     if (prediction) {
       var tabText = prediction.name;
-      var existing = current.arg.text;
+      var existing = cArg.text;
+
+      // Normally the cursor being just before whitespace means that you are
+      // 'in' the previous argument, which means that the prediction is based
+      // on that argument, however NamedArguments break this by having 2 parts
+      // so we need to prepend the tabText with a space for NamedArguments,
+      // but only when there isn't already a space at the end of the prefix
+      // (i.e. ' --name' not ' --name ')
+      if (current.isInName()) {
+        tabText = ' ' + tabText;
+      }
 
       if (existing !== tabText) {
         // Decide to use directTabText or arrowTabText
         // Strip any leading whitespace from the user inputted value because the
         // tabText will never have leading whitespace.
         var inputValue = existing.replace(/^\s*/, '');
         var isStrictCompletion = tabText.indexOf(inputValue) === 0;
         if (isStrictCompletion && input.cursor.start === input.typed.length) {
           // Display the suffix of the prediction as the completion
           var numLeadingSpaces = existing.match(/^(\s*)/)[0].length;
 
           directTabText = tabText.slice(existing.length - numLeadingSpaces);
         }
         else {
           // Display the '-> prediction' at the end of the completer element
-          // These JS escapes are aka &nbsp;&rarr; the right arrow
-          arrowTabText = ' \u00a0\u21E5 ' + tabText;
+          // \u21E5 is the JS escape right arrow
+          arrowTabText = '\u21E5 ' + tabText;
         }
       }
     }
+    else {
+      // There's no prediction, but if this is a named argument that needs a
+      // value (that is without any) then we need to show that one is needed
+      // For example 'git commit --message ', clearly needs some more text
+      if (cArg.type === 'NamedArgument' && cArg.text === '') {
+        emptyParameters.push('<' + current.param.type.name + '>\u00a0');
+      }
+    }
+  }
+
+  // Add a space between the typed text (+ directTabText) and the hints,
+  // making sure we don't add 2 sets of padding
+  if (directTabText !== '') {
+    directTabText += '\u00a0';
+  }
+  else if (!this.requisition.typedEndsWithSeparator()) {
+    emptyParameters.unshift('\u00a0');
   }
 
   // statusMarkup is wrapper around requisition.getInputStatusMarkup converting
   // space to &nbsp; in the string member (for HTML display) and status to an
   // appropriate class name (i.e. lower cased, prefixed with gcli-in-)
   var statusMarkup = this.requisition.getInputStatusMarkup(input.cursor.start);
   statusMarkup.forEach(function(member) {
     member.string = member.string.replace(/ /g, '\u00a0'); // i.e. &nbsp;
     member.className = 'gcli-in-' + member.status.toString().toLowerCase();
   }, this);
 
   // Calculate the list of parameters to be filled in
-  var trailingSeparator = this.requisition.typedEndsWithSeparator();
   // We generate an array of emptyParameter markers for each positional
   // parameter to the current command.
   // Generally each emptyParameter marker begins with a space to separate it
   // from whatever came before, unless what comes before ends in a space.
-  // Also if we've got a directTabText prediction or we're in a NamedParameter
-  // then we don't want any text for that parameter at all.
-  // The algorithm to add spaces needs to take this into account.
-
-  var firstBlankParam = true;
-  var emptyParameters = [];
+
+  var command = this.requisition.commandAssignment.value;
+  var jsCommand = command && command.name === '{';
+
   this.requisition.getAssignments().forEach(function(assignment) {
+    // Named arguments are handled with a group [options] marker
     if (!assignment.param.isPositionalAllowed) {
       return;
     }
-    if (current.arg.type === 'NamedArgument') {
-      return;
-    }
-
+
+    // No hints if we've got content for this parameter
     if (assignment.arg.toString().trim() !== '') {
-      if (directTabText !== '') {
-        firstBlankParam = false;
-      }
       return;
     }
 
-    if (directTabText !== '' && firstBlankParam) {
-      firstBlankParam = false;
+    if (directTabText !== '' && current === assignment) {
       return;
     }
 
     var text = (assignment.param.isDataRequired) ?
-        '<' + assignment.param.name + '>' :
-        '[' + assignment.param.name + ']';
-
-    // Add a space if we don't have one at the end of the input or if
-    // this isn't the first param we've mentioned
-    if (!trailingSeparator || !firstBlankParam) {
-      text = '\u00a0' + text; // i.e. &nbsp;
-    }
-
-    firstBlankParam = false;
+        '<' + assignment.param.name + '>\u00a0' :
+        '[' + assignment.param.name + ']\u00a0';
+
     emptyParameters.push(text);
   }.bind(this));
 
-  var command = this.requisition.commandAssignment.value;
-  var jsCommand = command && command.name === '{';
+  var addOptionsMarker = false;
+  // We add an '[options]' marker when there are named parameters that are
+  // not filled in and not hidden, and we don't have any directTabText
+  if (command && command.hasNamedParameters) {
+    command.params.forEach(function(param) {
+      var arg = this.requisition.getAssignment(param.name).arg;
+      if (!param.isPositionalAllowed && !param.hidden
+              && arg.type === "BlankArgument") {
+        addOptionsMarker = true;
+      }
+    }, this);
+  }
+
+  if (addOptionsMarker) {
+    // Add an nbsp if we don't have one at the end of the input or if
+    // this isn't the first param we've mentioned
+    emptyParameters.push('[options]\u00a0');
+  }
 
   // Is the entered command a JS command with no closing '}'?
   // TWEAK: This code should be considered for promotion to Requisition
   var unclosedJs = jsCommand &&
       this.requisition.getAssignment(0).arg.suffix.indexOf('}') === -1;
 
   // The text for the 'jump to scratchpad' feature, or '' if it is disabled
   var link = this.scratchpad && jsCommand ? this.scratchpad.linkText : '';
@@ -10055,21 +10280,23 @@ Tooltip.prototype.choiceChanged = functi
     var choice = this.assignment.conversion.constrainPredictionIndex(ev.choice);
     this.field.setChoiceIndex(choice);
   }
 };
 
 /**
  * Allow the inputter to use RETURN to chose the current menu item when
  * it can't execute the command line
+ * @return true if there was a selection to use, false otherwise
  */
 Tooltip.prototype.selectChoice = function(ev) {
   if (this.field && this.field.selectChoice) {
-    this.field.selectChoice();
-  }
+    return this.field.selectChoice();
+  }
+  return false;
 };
 
 /**
  * Called by the onFieldChange event on the current Field
  */
 Tooltip.prototype.fieldChanged = function(ev) {
   this.assignment.setConversion(ev.conversion);
 
--- a/browser/devtools/commandline/test/Makefile.in
+++ b/browser/devtools/commandline/test/Makefile.in
@@ -7,40 +7,34 @@ DEPTH     = @DEPTH@
 topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_FILES = \
-  browser_gcli_addon.js \
-  browser_gcli_break.js \
-  browser_gcli_calllog.js \
-  browser_gcli_commands.js \
-  browser_gcli_cookie.js \
-  browser_gcli_dbg.js \
-  browser_gcli_edit.js \
-  browser_gcli_inspect.js \
-  browser_gcli_integrate.js \
-  browser_gcli_jsb.js \
-  browser_gcli_pagemod_export.js \
-  browser_gcli_pref.js \
-  browser_gcli_responsivemode.js \
-  browser_gcli_restart.js \
-  browser_gcli_settings.js \
+  browser_dbg_cmd_break.js \
+  browser_dbg_cmd.js \
+  browser_cmd_addon.js \
+  browser_cmd_calllog.js \
+  browser_cmd_calllog_chrome.js \
+  browser_cmd_commands.js \
+  browser_cmd_cookie.js \
+  browser_cmd_integrate.js \
+  browser_cmd_jsb.js \
+  browser_cmd_pagemod_export.js \
+  browser_cmd_pref.js \
+  browser_cmd_restart.js \
+  browser_cmd_settings.js \
   browser_gcli_web.js \
   head.js \
+  helpers.js \
   $(NULL)
 
 MOCHITEST_BROWSER_FILES += \
-  browser_gcli_break.html \
-  browser_gcli_inspect.html \
-  resources_dbg.html \
-  resources_inpage.js \
-  resources_inpage1.css \
-  resources_inpage2.css \
-  resources_jsb_script.js \
-  resources.html \
+  browser_dbg_cmd_break.html \
+  browser_dbg_cmd.html \
+  browser_cmd_pagemod_export.html \
+  browser_cmd_jsb_script.jsi \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
-
rename from browser/devtools/commandline/test/browser_gcli_addon.js
rename to browser/devtools/commandline/test/browser_cmd_addon.js
--- a/browser/devtools/commandline/test/browser_gcli_addon.js
+++ b/browser/devtools/commandline/test/browser_cmd_addon.js
@@ -1,43 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the addon commands works as they should
+
 function test() {
-  DeveloperToolbarTest.test("about:blank", function GAT_test() {
-    function GAT_ready() {
-      Services.obs.removeObserver(GAT_ready, "gcli_addon_commands_ready", false);
+  DeveloperToolbarTest.test("about:blank", [ GAT_test ]);
+}
+
+function GAT_test() {
+  var GAT_ready = DeveloperToolbarTest.checkCalled(function() {
+    Services.obs.removeObserver(GAT_ready, "gcli_addon_commands_ready", false);
+
+    helpers.setInput('addon list dictionary');
+    helpers.check({
+      input:  'addon list dictionary',
+      hints:                       '',
+      markup: 'VVVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID'
+    });
+
+    helpers.setInput('addon list extension');
+    helpers.check({
+      input:  'addon list extension',
+      hints:                      '',
+      markup: 'VVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID'
+    });
+
+    helpers.setInput('addon list locale');
+    helpers.check({
+      input:  'addon list locale',
+      hints:                   '',
+      markup: 'VVVVVVVVVVVVVVVVV',
+      status: 'VALID'
+    });
+
+    helpers.setInput('addon list plugin');
+    helpers.check({
+      input:  'addon list plugin',
+      hints:                   '',
+      markup: 'VVVVVVVVVVVVVVVVV',
+      status: 'VALID'
+    });
 
-      DeveloperToolbarTest.checkInputStatus({
-        typed: "addon list dictionary",
-        status: "VALID"
-      });
-      DeveloperToolbarTest.checkInputStatus({
-        typed: "addon list extension",
-        status: "VALID"
-      });
-      DeveloperToolbarTest.checkInputStatus({
-        typed: "addon list locale",
-        status: "VALID"
-      });
-      DeveloperToolbarTest.checkInputStatus({
-        typed: "addon list plugin",
-        status: "VALID"
-      });
-      DeveloperToolbarTest.checkInputStatus({
-        typed: "addon list theme",
-        status: "VALID"
-      });
-      DeveloperToolbarTest.checkInputStatus({
-        typed: "addon list all",
-        status: "VALID"
-      });
-      DeveloperToolbarTest.checkInputStatus({
-        typed: "addon disable Test_Plug-in_1.0.0.0",
-        status: "VALID"
-      });
-      DeveloperToolbarTest.checkInputStatus({
-        typed: "addon enable Test_Plug-in_1.0.0.0",
-        status: "VALID"
-      });
-      DeveloperToolbarTest.exec({ completed: false });
-      finish();
-    }
-    Services.obs.addObserver(GAT_ready, "gcli_addon_commands_ready", false);
+    helpers.setInput('addon list theme');
+    helpers.check({
+      input:  'addon list theme',
+      hints:                  '',
+      markup: 'VVVVVVVVVVVVVVVV',
+      status: 'VALID'
+    });
+
+    helpers.setInput('addon list all');
+    helpers.check({
+      input:  'addon list all',
+      hints:                '',
+      markup: 'VVVVVVVVVVVVVV',
+      status: 'VALID'
+    });
+
+    helpers.setInput('addon disable Test_Plug-in_1.0.0.0');
+    helpers.check({
+      input:  'addon disable Test_Plug-in_1.0.0.0',
+      hints:                                    '',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID'
+    });
+
+    helpers.setInput('addon disable WRONG');
+    helpers.check({
+      input:  'addon disable WRONG',
+      hints:                     '',
+      markup: 'VVVVVVVVVVVVVVEEEEE',
+      status: 'ERROR'
+    });
+
+    helpers.setInput('addon enable Test_Plug-in_1.0.0.0');
+    helpers.check({
+      input:  'addon enable Test_Plug-in_1.0.0.0',
+      hints:                                   '',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID',
+      args: {
+        command: { name: 'addon enable' },
+        name: { value: 'Test Plug-in', status: 'VALID' },
+      }
+    });
+
+    DeveloperToolbarTest.exec({ completed: false });
   });
+
+  Services.obs.addObserver(GAT_ready, "gcli_addon_commands_ready", false);
 }
rename from browser/devtools/commandline/test/browser_gcli_calllog.js
rename to browser/devtools/commandline/test/browser_cmd_calllog.js
--- a/browser/devtools/commandline/test/browser_gcli_calllog.js
+++ b/browser/devtools/commandline/test/browser_cmd_calllog.js
@@ -4,51 +4,54 @@
 // Tests that the calllog commands works as they should
 
 let imported = {};
 Components.utils.import("resource:///modules/HUDService.jsm", imported);
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-calllog";
 
 function test() {
-  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
-    testCallLogStatus();
-    testCallLogExec();
-    finish();
-  });
+  DeveloperToolbarTest.test(TEST_URI, [ testCallLogStatus, testCallLogExec ]);
 }
 
 function testCallLogStatus() {
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "calllog",
-    status: "ERROR"
+  helpers.setInput('calllog');
+  helpers.check({
+    input:  'calllog',
+    hints:         '',
+    markup: 'IIIIIII',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "calllog start",
-    status: "VALID",
-    emptyParameters: [ ]
+  helpers.setInput('calllog start');
+  helpers.check({
+    input:  'calllog start',
+    hints:               '',
+    markup: 'VVVVVVVVVVVVV',
+    status: 'VALID'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "calllog start",
-    status: "VALID",
-    emptyParameters: [ ]
+  helpers.setInput('calllog stop');
+  helpers.check({
+    input:  'calllog stop',
+    hints:              '',
+    markup: 'VVVVVVVVVVVV',
+    status: 'VALID'
   });
 }
 
 function testCallLogExec() {
   DeveloperToolbarTest.exec({
     typed: "calllog stop",
     args: { },
     outputMatch: /No call logging/,
   });
 
   let hud = null;
-  function onWebConsoleOpen(aSubject) {
+  var onWebConsoleOpen = DeveloperToolbarTest.checkCalled(function(aSubject) {
     Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
 
     aSubject.QueryInterface(Ci.nsISupportsString);
     hud = imported.HUDService.getHudReferenceById(aSubject.data);
     ok(hud.hudId in imported.HUDService.hudReferences, "console open");
 
     DeveloperToolbarTest.exec({
       typed: "calllog stop",
@@ -65,17 +68,17 @@ function testCallLogExec() {
     let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
     is(labels.length, 0, "no output in console");
 
     DeveloperToolbarTest.exec({
       typed: "console close",
       args: {},
       blankOutput: true,
     });
-  }
+  });
 
   Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false);
 
   DeveloperToolbarTest.exec({
     typed: "calllog start",
     args: { },
     outputMatch: /Call logging started/,
   });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_cmd_calllog_chrome.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the calllog commands works as they should
+
+let imported = {};
+Components.utils.import("resource:///modules/HUDService.jsm", imported);
+
+const TEST_URI = "data:text/html;charset=utf-8,cmd-calllog-chrome";
+
+function test() {
+  DeveloperToolbarTest.test(TEST_URI, function CLCTest(browser, tab) {
+    testCallLogStatus();
+    testCallLogExec();
+  });
+}
+
+function testCallLogStatus() {
+  DeveloperToolbarTest.checkInputStatus({
+    typed: "calllog",
+    status: "ERROR",
+    emptyParameters: [ " " ]
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed: "calllog chromestart content-variable window",
+    status: "VALID",
+    emptyParameters: [ " " ]
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed: "calllog chromestop",
+    status: "VALID",
+    emptyParameters: [ " " ]
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed: "calllog chromestart chrome-variable window",
+    status: "VALID",
+    emptyParameters: [ " " ]
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed: "calllog chromestop",
+    status: "VALID",
+    emptyParameters: [ " " ]
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed: "calllog chromestart javascript \"({a1: function() {this.a2()}," +
+           "a2: function() {}});\"",
+    status: "VALID",
+    emptyParameters: [ " " ]
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed: "calllog chromestop",
+    status: "VALID",
+    emptyParameters: [ " " ]
+  });
+}
+
+function testCallLogExec() {
+  DeveloperToolbarTest.exec({
+    typed: "calllog chromestop",
+    args: { },
+    outputMatch: /No call logging/,
+  });
+
+  function onWebConsoleOpen(aSubject) {
+    Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
+
+    aSubject.QueryInterface(Ci.nsISupportsString);
+    let hud = imported.HUDService.getHudReferenceById(aSubject.data);
+    ok(hud.hudId in imported.HUDService.hudReferences, "console open");
+
+    DeveloperToolbarTest.exec({
+      typed: "calllog chromestop",
+      args: { },
+      outputMatch: /Stopped call logging/,
+    });
+
+    DeveloperToolbarTest.exec({
+      typed: "calllog chromestart javascript XXX",
+      outputMatch: /following exception/,
+    });
+
+    DeveloperToolbarTest.exec({
+      typed: "console clear",
+      args: {},
+      blankOutput: true,
+    });
+
+    let labels = hud.jsterm.outputNode.querySelectorAll(".webconsole-msg-output");
+    is(labels.length, 0, "no output in console");
+
+    DeveloperToolbarTest.exec({
+      typed: "console close",
+      args: {},
+      blankOutput: true,
+    });
+
+    executeSoon(finish);
+  }
+  Services.obs.addObserver(onWebConsoleOpen, "web-console-created", false);
+
+  DeveloperToolbarTest.exec({
+    typed: "calllog chromestart javascript \"({a1: function() {this.a2()},a2: function() {}});\"",
+    outputMatch: /Call logging started/,
+  });
+}
rename from browser/devtools/commandline/test/browser_gcli_commands.js
rename to browser/devtools/commandline/test/browser_cmd_commands.js
--- a/browser/devtools/commandline/test/browser_gcli_commands.js
+++ b/browser/devtools/commandline/test/browser_cmd_commands.js
@@ -4,36 +4,28 @@
 // Test various GCLI commands
 
 let imported = {};
 Components.utils.import("resource:///modules/HUDService.jsm", imported);
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-commands";
 
 function test() {
-  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
-    testEcho();
-    testConsole(tab);
-
-    imported = undefined;
-    finish();
-  });
+  DeveloperToolbarTest.test(TEST_URI, [ testEcho, testConsole ]);
 }
 
 function testEcho() {
-  /*
   DeveloperToolbarTest.exec({
     typed: "echo message",
     args: { message: "message" },
     outputMatch: /^message$/,
   });
-  */
 }
 
-function testConsole(tab) {
+function testConsole(browser, tab) {
   let hud = null;
   function onWebConsoleOpen(aSubject) {
     Services.obs.removeObserver(onWebConsoleOpen, "web-console-created");
 
     aSubject.QueryInterface(Ci.nsISupportsString);
     hud = imported.HUDService.getHudReferenceById(aSubject.data);
     ok(hud.hudId in imported.HUDService.hudReferences, "console open");
 
@@ -65,11 +57,10 @@ function testConsole(tab) {
       typed: "console close",
       args: {},
       blankOutput: true,
     });
 
     ok(!(hud.hudId in imported.HUDService.hudReferences), "console closed");
 
     imported = undefined;
-    finish();
   }
 }
rename from browser/devtools/commandline/test/browser_gcli_cookie.js
rename to browser/devtools/commandline/test/browser_cmd_cookie.js
--- a/browser/devtools/commandline/test/browser_gcli_cookie.js
+++ b/browser/devtools/commandline/test/browser_cmd_cookie.js
@@ -1,53 +1,83 @@
 /* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the cookie commands works as they should
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-cookie";
 
 function test() {
-  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
-    testCookieCommands();
-    finish();
+  DeveloperToolbarTest.test(TEST_URI, [ testCookieCheck, testCookieExec ]);
+}
+
+function testCookieCheck() {
+  helpers.setInput('cookie');
+  helpers.check({
+    input:  'cookie',
+    hints:        '',
+    markup: 'IIIIII',
+    status: 'ERROR'
+  });
+
+  helpers.setInput('cookie lis');
+  helpers.check({
+    input:  'cookie lis',
+    hints:            't',
+    markup: 'IIIIIIVIII',
+    status: 'ERROR'
+  });
+
+  helpers.setInput('cookie list');
+  helpers.check({
+    input:  'cookie list',
+    hints:             '',
+    markup: 'VVVVVVVVVVV',
+    status: 'VALID'
+  });
+
+  helpers.setInput('cookie remove');
+  helpers.check({
+    input:  'cookie remove',
+    hints:               ' <key>',
+    markup: 'VVVVVVVVVVVVV',
+    status: 'ERROR'
+  });
+
+  helpers.setInput('cookie set');
+  helpers.check({
+    input:  'cookie set',
+    hints:            ' <key> <value> [options]',
+    markup: 'VVVVVVVVVV',
+    status: 'ERROR'
+  });
+
+  helpers.setInput('cookie set fruit');
+  helpers.check({
+    input:  'cookie set fruit',
+    hints:                  ' <value> [options]',
+    markup: 'VVVVVVVVVVVVVVVV',
+    status: 'ERROR'
+  });
+
+  helpers.setInput('cookie set fruit ban');
+  helpers.check({
+    input:  'cookie set fruit ban',
+    hints:                      ' [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVVV',
+    status: 'VALID',
+    args: {
+      key: { value: 'fruit' },
+      value: { value: 'ban' },
+      secure: { value: false },
+    }
   });
 }
 
-function testCookieCommands() {
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "cook",
-    directTabText: "ie",
-    status: "ERROR"
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "cookie l",
-    directTabText: "ist",
-    status: "ERROR"
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "cookie list",
-    status: "VALID",
-    emptyParameters: [ ]
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "cookie remove",
-    status: "ERROR",
-    emptyParameters: [ " <key>" ]
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "cookie set",
-    status: "ERROR",
-    emptyParameters: [ " <key>", " <value>" ],
-  });
-
+function testCookieExec() {
   DeveloperToolbarTest.exec({
     typed: "cookie set fruit banana",
     args: {
       key: "fruit",
       value: "banana",
       path: "/",
       domain: null,
       secure: false
rename from browser/devtools/commandline/test/browser_gcli_integrate.js
rename to browser/devtools/commandline/test/browser_cmd_integrate.js
rename from browser/devtools/commandline/test/browser_gcli_jsb.js
rename to browser/devtools/commandline/test/browser_cmd_jsb.js
--- a/browser/devtools/commandline/test/browser_gcli_jsb.js
+++ b/browser/devtools/commandline/test/browser_cmd_jsb.js
@@ -1,48 +1,57 @@
-function test() {
-  const TEST_URI = "http://example.com/browser/browser/devtools/commandline/" +
-                   "test/resources_jsb_script.js";
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the jsb command works as it should
 
-  DeveloperToolbarTest.test("about:blank", function GJT_test() {
-    /* Commented out by bug 774057, re-enable with un-hidden jsb command
-    DeveloperToolbarTest.exec({
-      typed: "jsb AAA",
-      outputMatch: /valid/
-    });
+const TEST_URI = "http://example.com/browser/browser/devtools/commandline/" +
+                 "test/browser_cmd_jsb_script.jsi";
 
-    gBrowser.addTabsProgressListener({
-      onProgressChange: function GJT_onProgressChange(aBrowser) {
-        gBrowser.removeTabsProgressListener(this);
+function test() {
+  DeveloperToolbarTest.test("about:blank", [ /*GJT_test*/ ]);
+}
 
-        let win = aBrowser._contentWindow;
-        let uri = win.document.location.href;
-        let result = win.atob(uri.replace(/.*,/, ""));
-
-        result = result.replace(/[\r\n]]/g, "\n");
+function GJT_test() {
+  helpers.setInput('jsb');
+  helpers.check({
+    input:  'jsb',
+    hints:     ' <url> [indentSize] [indentChar] [preserveNewlines] [preserveMaxNewlines] [jslintHappy] [braceStyle] [spaceBeforeConditional] [unescapeStrings]',
+    markup: 'VVV',
+    status: 'ERROR'
+  });
 
-        checkResult(result);
-        finish();
-      }
-    });
+  gBrowser.addTabsProgressListener({
+    onProgressChange: DeveloperToolbarTest.checkCalled(function GJT_onProgressChange(aBrowser) {
+      gBrowser.removeTabsProgressListener(this);
 
-    info("Checking beautification");
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "jsb " + TEST_URI + " 4 space true -1 false collapse true false",
-      status: "VALID"
-    });
-    DeveloperToolbarTest.exec({ completed: false });
+      let win = aBrowser._contentWindow;
+      let uri = win.document.location.href;
+      let result = win.atob(uri.replace(/.*,/, ""));
 
-    function checkResult(aResult) {
+      result = result.replace(/[\r\n]]/g, "\n");
+
       let correct = "function somefunc() {\n" +
                     "    for (let n = 0; n < 500; n++) {\n" +
                     "        if (n % 2 == 1) {\n" +
                     "            console.log(n);\n" +
                     "            console.log(n + 1);\n" +
                     "        }\n" +
                     "    }\n" +
                     "}";
-      is(aResult, correct, "JS has been correctly prettified");
-    }
-    */
-    finish();
+      is(result, correct, "JS has been correctly prettified");
+    })
   });
+
+  info("Checking beautification");
+
+  helpers.setInput('jsb ' + TEST_URI);
+  /*
+  helpers.check({
+    input:  'jsb',
+    hints:     ' [options]',
+    markup: 'VVV',
+    status: 'VALID'
+  });
+  */
+
+  DeveloperToolbarTest.exec({ completed: false });
 }
rename from browser/devtools/commandline/test/resources_jsb_script.js
rename to browser/devtools/commandline/test/browser_cmd_jsb_script.jsi
rename from browser/devtools/commandline/test/browser_gcli_inspect.html
rename to browser/devtools/commandline/test/browser_cmd_pagemod_export.html
rename from browser/devtools/commandline/test/browser_gcli_pagemod_export.js
rename to browser/devtools/commandline/test/browser_cmd_pagemod_export.js
--- a/browser/devtools/commandline/test/browser_gcli_pagemod_export.js
+++ b/browser/devtools/commandline/test/browser_cmd_pagemod_export.js
@@ -1,32 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the inspect command works as it should
 
-const TEST_URI = "http://example.com/browser/browser/devtools/commandline/test/browser_gcli_inspect.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/commandline/"+
+                 "test/browser_cmd_pagemod_export.html";
 
 function test() {
   let initialHtml = "";
 
-  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
-    initialHtml = content.document.documentElement.innerHTML;
+  DeveloperToolbarTest.test(TEST_URI, [
+    init,
+    testExportHtml,
+    testPageModReplace,
+    testPageModRemoveElement,
+    testPageModRemoveAttribute
+  ]);
 
-    testExportHtml();
-    testPageModReplace();
-    testPageModRemoveElement();
-    testPageModRemoveAttribute();
-    finish();
-  });
+  function init() {
+    initialHtml = content.document.documentElement.innerHTML;
+  }
 
   function testExportHtml() {
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "export html",
-      status: "VALID"
+    helpers.setInput('export html');
+    helpers.check({
+      input:  'export html',
+      hints:             '',
+      markup: 'VVVVVVVVVVV',
+      status: 'VALID'
     });
 
     let oldOpen = content.open;
     let openURL = "";
     content.open = function(aUrl) {
       openURL = aUrl;
     };
 
@@ -45,43 +51,46 @@ function test() {
     return content.document.documentElement.innerHTML;
   }
 
   function resetContent() {
     content.document.documentElement.innerHTML = initialHtml;
   }
 
   function testPageModReplace() {
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod replace",
-      emptyParameters: [" <search>", " <replace>", " [ignoreCase]",
-                        " [selector]", " [root]", " [attrOnly]",
-                        " [contentOnly]", " [attributes]"],
-      status: "ERROR"
+    helpers.setInput('pagemod replace');
+    helpers.check({
+      input:  'pagemod replace',
+      hints:                 ' <search> <replace> [ignoreCase] [selector] [root] [attrOnly] [contentOnly] [attributes]',
+      markup: 'VVVVVVVVVVVVVVV',
+      status: 'ERROR'
     });
 
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod replace some foo",
-      emptyParameters: [" [ignoreCase]", " [selector]", " [root]",
-                        " [attrOnly]", " [contentOnly]", " [attributes]"],
-      status: "VALID"
+    helpers.setInput('pagemod replace some foo');
+    helpers.check({
+      input:  'pagemod replace some foo',
+      hints:                          ' [ignoreCase] [selector] [root] [attrOnly] [contentOnly] [attributes]',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID'
     });
 
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod replace some foo true",
-      emptyParameters: [" [selector]", " [root]", " [attrOnly]",
-                        " [contentOnly]", " [attributes]"],
-      status: "VALID"
+    helpers.setInput('pagemod replace some foo true');
+    helpers.check({
+      input:  'pagemod replace some foo true',
+      hints:                               ' [selector] [root] [attrOnly] [contentOnly] [attributes]',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID'
     });
 
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod replace some foo true --attrOnly",
-      emptyParameters: [" [selector]", " [root]", " [contentOnly]",
-                        " [attributes]"],
-      status: "VALID"
+    helpers.setInput('pagemod replace some foo true --attrOnly');
+    helpers.check({
+      input:  'pagemod replace some foo true --attrOnly',
+      hints:                                          ' [selector] [root] [contentOnly] [attributes]',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID'
     });
 
     DeveloperToolbarTest.exec({
       typed: "pagemod replace sOme foOBar",
       outputMatch: /^[^:]+: 13\. [^:]+: 0\. [^:]+: 0\.\s*$/
     });
 
     is(getContent(), initialHtml, "no change in the page");
@@ -138,31 +147,38 @@ function test() {
           ".someclass changed to .foobarclass");
     isnot(getContent().indexOf('<p id="someid">#someid'), -1,
           "#someid did not change");
 
     resetContent();
   }
 
   function testPageModRemoveElement() {
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod remove",
-      status: "ERROR"
+    helpers.setInput('pagemod remove');
+    helpers.check({
+      input:  'pagemod remove',
+      hints:                '',
+      markup: 'IIIIIIIVIIIIII',
+      status: 'ERROR'
     });
 
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod remove element",
-      emptyParameters: [" <search>", " [root]", " [stripOnly]", " [ifEmptyOnly]"],
-      status: "ERROR"
+    helpers.setInput('pagemod remove element');
+    helpers.check({
+      input:  'pagemod remove element',
+      hints:                        ' <search> [root] [stripOnly] [ifEmptyOnly]',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVV',
+      status: 'ERROR'
     });
 
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod remove element foo",
-      emptyParameters: [" [root]", " [stripOnly]", " [ifEmptyOnly]"],
-      status: "VALID"
+    helpers.setInput('pagemod remove element foo');
+    helpers.check({
+      input:  'pagemod remove element foo',
+      hints:                            ' [root] [stripOnly] [ifEmptyOnly]',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID'
     });
 
     DeveloperToolbarTest.exec({
       typed: "pagemod remove element p",
       outputMatch: /^[^:]+: 3\. [^:]+: 3\.\s*$/
     });
 
     is(getContent().indexOf('<p class="someclass">'), -1, "p.someclass removed");
@@ -207,26 +223,42 @@ function test() {
     isnot(getContent().indexOf(".someclass"), -1, ".someclass still exists");
     isnot(getContent().indexOf("#someid"), -1, "#someid still exists");
     isnot(getContent().indexOf("<strong>p"), -1, "<strong> still exists");
 
     resetContent();
   }
 
   function testPageModRemoveAttribute() {
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod remove attribute",
-      emptyParameters: [" <searchAttributes>", " <searchElements>", " [root]", " [ignoreCase]"],
-      status: "ERROR"
+    helpers.setInput('pagemod remove attribute ');
+    helpers.check({
+      input:  'pagemod remove attribute ',
+      hints:                           '<searchAttributes> <searchElements> [root] [ignoreCase]',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVVVVV',
+      status: 'ERROR',
+      args: {
+        searchAttributes: { value: undefined, status: 'INCOMPLETE' },
+        searchElements: { value: undefined, status: 'INCOMPLETE' },
+        root: { value: undefined },
+        ignoreCase: { value: false },
+      }
     });
 
-    DeveloperToolbarTest.checkInputStatus({
-      typed: "pagemod remove attribute foo bar",
-      emptyParameters: [" [root]", " [ignoreCase]"],
-      status: "VALID"
+    helpers.setInput('pagemod remove attribute foo bar');
+    helpers.check({
+      input:  'pagemod remove attribute foo bar',
+      hints:                                  ' [root] [ignoreCase]',
+      markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+      status: 'VALID',
+      args: {
+        searchAttributes: { value: 'foo' },
+        searchElements: { value: 'bar' },
+        root: { value: undefined },
+        ignoreCase: { value: false },
+      }
     });
 
     DeveloperToolbarTest.exec({
       typed: "pagemod remove attribute foo bar",
       outputMatch: /^[^:]+: 0\. [^:]+: 0\.\s*$/
     });
 
     is(getContent(), initialHtml, "nothing changed in the page");
old mode 100755
new mode 100644
rename from browser/devtools/commandline/test/browser_gcli_pref.js
rename to browser/devtools/commandline/test/browser_cmd_pref.js
--- a/browser/devtools/commandline/test/browser_gcli_pref.js
+++ b/browser/devtools/commandline/test/browser_cmd_pref.js
@@ -17,29 +17,26 @@ imports.XPCOMUtils.defineLazyGetter(impo
 imports.XPCOMUtils.defineLazyGetter(imports, "supportsString", function() {
   return Components.classes["@mozilla.org/supports-string;1"]
           .createInstance(Components.interfaces.nsISupportsString);
 });
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-pref";
 
 function test() {
-  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
-    setup();
-
-    testPrefSetEnable();
-    testPrefStatus();
-    testPrefBoolExec();
-    testPrefNumberExec();
-    testPrefStringExec();
-    testPrefSetDisable();
-
-    shutdown();
-    finish();
-  });
+  DeveloperToolbarTest.test(TEST_URI, [
+    setup,
+    testPrefSetEnable,
+    testPrefStatus,
+    testPrefBoolExec,
+    testPrefNumberExec,
+    testPrefStringExec,
+    testPrefSetDisable,
+    shutdown
+  ]);
 }
 
 let tiltEnabledOrig = undefined;
 let tabSizeOrig = undefined;
 let remoteHostOrig = undefined;
 
 function setup() {
   Components.utils.import("resource://gre/modules/devtools/Require.jsm", imports);
@@ -67,102 +64,113 @@ function shutdown() {
   tiltEnabledOrig = undefined;
   tabSizeOrig = undefined;
   remoteHostOrig = undefined;
 
   imports = undefined;
 }
 
 function testPrefStatus() {
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref s",
-    markup: "IIIIVI",
-    status: "ERROR",
-    directTabText: "et"
+  helpers.setInput('pref');
+  helpers.check({
+    input:  'pref',
+    hints:      '',
+    markup: 'IIII',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref show",
-    markup: "VVVVVVVVV",
-    status: "ERROR",
-    emptyParameters: [ " <setting>" ]
+  helpers.setInput('pref s');
+  helpers.check({
+    input:  'pref s',
+    hints:        'et',
+    markup: 'IIIIVI',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref show tempTBo",
-    markup: "VVVVVVVVVVEEEEEEE",
-    status: "ERROR",
-    emptyParameters: [ ]
+  helpers.setInput('pref sh');
+  helpers.check({
+    input:  'pref sh',
+    hints:         'ow',
+    markup: 'IIIIVII',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref show devtools.toolbar.ena",
-    markup: "VVVVVVVVVVIIIIIIIIIIIIIIIIIIII",
-    directTabText: "bled",
-    status: "ERROR",
-    emptyParameters: [ ]
+  helpers.setInput('pref show ');
+  helpers.check({
+    input:  'pref show ',
+    markup: 'VVVVVVVVVV',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref show hideIntro",
-    markup: "VVVVVVVVVVIIIIIIIII",
-    directTabText: "",
-    arrowTabText: "devtools.gcli.hideIntro",
-    status: "ERROR",
-    emptyParameters: [ ]
+  helpers.setInput('pref show usetexttospeech');
+  helpers.check({
+    input:  'pref show usetexttospeech',
+    hints:                           ' -> accessibility.usetexttospeech',
+    markup: 'VVVVVVVVVVIIIIIIIIIIIIIII',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref show devtools.toolbar.enabled",
-    markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
-    status: "VALID",
-    emptyParameters: [ ]
+  helpers.setInput('pref show devtools.til');
+  helpers.check({
+    input:  'pref show devtools.til',
+    hints:                        't.enabled',
+    markup: 'VVVVVVVVVVIIIIIIIIIIII',
+    status: 'ERROR',
+    tooltipState: 'true:importantFieldFlag',
+    args: {
+      setting: { value: undefined, status: 'INCOMPLETE' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref show devtools.tilt.enabled 4",
-    markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE",
-    directTabText: "",
-    status: "ERROR",
-    emptyParameters: [ ]
+  helpers.setInput('pref reset devtools.tilt.enabled');
+  helpers.check({
+    input:  'pref reset devtools.tilt.enabled',
+    hints:                                  '',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+    status: 'VALID'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref show devtools.tilt.enabled",
-    markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
-    status: "VALID",
-    emptyParameters: [ ]
+  helpers.setInput('pref show devtools.tilt.enabled 4');
+  helpers.check({
+    input:  'pref show devtools.tilt.enabled 4',
+    hints:                                   '',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref reset devtools.tilt.enabled",
-    markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
-    status: "VALID",
-    emptyParameters: [ ]
+  helpers.setInput('pref set devtools.tilt.enabled 4');
+  helpers.check({
+    input:  'pref set devtools.tilt.enabled 4',
+    hints:                                  '',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE',
+    status: 'ERROR',
+    args: {
+      setting: { arg: ' devtools.tilt.enabled' },
+      value: { status: 'ERROR', message: 'Can\'t use \'4\'.' },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref set devtools.tilt.enabled 4",
-    markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVE",
-    status: "ERROR",
-    emptyParameters: [ ]
+  helpers.setInput('pref set devtools.editor.tabsize 4');
+  helpers.check({
+    input:  'pref set devtools.editor.tabsize 4',
+    hints:                                    '',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+    status: 'VALID',
+    args: {
+      setting: { arg: ' devtools.editor.tabsize' },
+      value: { value: 4 },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref set devtools.editor.tabsize 4",
-    markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
-    status: "VALID",
-    emptyParameters: [ ]
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "pref list",
-    markup: "EEEEVEEEE",
-    status: "ERROR",
-    emptyParameters: [ ]
+  helpers.setInput('pref list');
+  helpers.check({
+    input:  'pref list',
+    hints:           '',
+    markup: 'EEEEVEEEE',
+    status: 'ERROR'
   });
 }
 
 function testPrefSetEnable() {
   DeveloperToolbarTest.exec({
     typed: "pref set devtools.editor.tabsize 9",
     args: {
       setting: imports.settings.getSetting("devtools.editor.tabsize"),
rename from browser/devtools/commandline/test/browser_gcli_restart.js
rename to browser/devtools/commandline/test/browser_cmd_restart.js
--- a/browser/devtools/commandline/test/browser_gcli_restart.js
+++ b/browser/devtools/commandline/test/browser_cmd_restart.js
@@ -1,48 +1,32 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that restart command works properly (input wise)
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-command-restart";
 
 function test() {
-  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
-    testRestart();
-    finish();
-  });
+  DeveloperToolbarTest.test(TEST_URI, [ testRestart ]);
 }
 
 function testRestart() {
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "restart",
-    markup: "VVVVVVV",
-    status: "VALID",
-    emptyParameters: [ " [nocache]" ],
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "restart ",
-    markup: "VVVVVVVV",
-    status: "VALID",
-    directTabText: "false"
+  helpers.setInput('restart');
+  helpers.check({
+    input:  'restart',
+    markup: 'VVVVVVV',
+    status: 'VALID',
+    args: {
+      nocache: { value: false },
+    }
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "restart t",
-    markup: "VVVVVVVVI",
-    status: "ERROR",
-    directTabText: "rue"
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "restart --nocache",
-    markup: "VVVVVVVVVVVVVVVVV",
-    status: "VALID"
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed:  "restart --noca",
-    markup: "VVVVVVVVEEEEEE",
-    status: "ERROR",
+  helpers.setInput('restart --nocache');
+  helpers.check({
+    input:  'restart --nocache',
+    markup: 'VVVVVVVVVVVVVVVVV',
+    status: 'VALID',
+    args: {
+      nocache: { value: true },
+    }
   });
 }
old mode 100755
new mode 100644
rename from browser/devtools/commandline/test/browser_gcli_settings.js
rename to browser/devtools/commandline/test/browser_cmd_settings.js
--- a/browser/devtools/commandline/test/browser_gcli_settings.js
+++ b/browser/devtools/commandline/test/browser_cmd_settings.js
@@ -17,24 +17,17 @@ imports.XPCOMUtils.defineLazyGetter(impo
 imports.XPCOMUtils.defineLazyGetter(imports, "supportsString", function() {
   return Components.classes["@mozilla.org/supports-string;1"]
           .createInstance(Components.interfaces.nsISupportsString);
 });
 
 const TEST_URI = "data:text/html;charset=utf-8,gcli-settings";
 
 function test() {
-  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
-    setup();
-
-    testSettings();
-
-    shutdown();
-    finish();
-  });
+  DeveloperToolbarTest.test(TEST_URI, [ setup, testSettings, shutdown ]);
 }
 
 let tiltEnabled = undefined;
 let tabSize = undefined;
 let remoteHost = undefined;
 
 let tiltEnabledOrig = undefined;
 let tabSizeOrig = undefined;
rename from browser/devtools/commandline/test/resources_dbg.html
rename to browser/devtools/commandline/test/browser_dbg_cmd.html
rename from browser/devtools/commandline/test/browser_gcli_dbg.js
rename to browser/devtools/commandline/test/browser_dbg_cmd.js
--- a/browser/devtools/commandline/test/browser_gcli_dbg.js
+++ b/browser/devtools/commandline/test/browser_dbg_cmd.js
@@ -1,67 +1,72 @@
 function test() {
-  const TEST_URI = TEST_BASE_HTTP + "resources_dbg.html";
+  const TEST_URI = "http://example.com/browser/browser/devtools/commandline/" +
+                   "test/browser_dbg_cmd.html";
 
-  DeveloperToolbarTest.test(TEST_URI, function GAT_test() {
-    let pane = DebuggerUI.toggleDebugger();
-    ok(pane, "toggleDebugger() should return a pane.");
-    let frame = pane._frame;
+  DeveloperToolbarTest.test(TEST_URI, function() {
+    testDbgCmd();
+  });
+}
 
-    frame.addEventListener("Debugger:Connecting", function dbgConnected(aEvent) {
-      frame.removeEventListener("Debugger:Connecting", dbgConnected, true);
+function testDbgCmd() {
+  let pane = DebuggerUI.toggleDebugger();
+  ok(pane, "toggleDebugger() should return a pane.");
+  let frame = pane._frame;
+
+  frame.addEventListener("Debugger:Connecting", function dbgConnected(aEvent) {
+    frame.removeEventListener("Debugger:Connecting", dbgConnected, true);
 
-      // Wait for the initial resume...
-      aEvent.target.ownerDocument.defaultView.gClient
-          .addOneTimeListener("resumed", function() {
+    // Wait for the initial resume...
+    aEvent.target.ownerDocument.defaultView.gClient
+        .addOneTimeListener("resumed", function() {
 
-        info("Starting tests.");
+      info("Starting tests.");
 
-        let contentDoc = content.window.document;
-        let output = contentDoc.querySelector("input[type=text]");
-        let btnDoit = contentDoc.querySelector("input[type=button]");
+      let contentDoc = content.window.document;
+      let output = contentDoc.querySelector("input[type=text]");
+      let btnDoit = contentDoc.querySelector("input[type=button]");
 
-        cmd("dbg interrupt", function() {
-          ok(true, "debugger is paused");
-          pane.contentWindow.gClient.addOneTimeListener("resumed", function() {
-            ok(true, "debugger continued");
-            pane.contentWindow.gClient.addOneTimeListener("paused", function() {
+      cmd("dbg interrupt", function() {
+        ok(true, "debugger is paused");
+        pane.contentWindow.gClient.addOneTimeListener("resumed", function() {
+          ok(true, "debugger continued");
+          pane.contentWindow.gClient.addOneTimeListener("paused", function() {
+            cmd("dbg step in", function() {
               cmd("dbg step in", function() {
                 cmd("dbg step in", function() {
-                  cmd("dbg step in", function() {
-                    is(output.value, "step in", "debugger stepped in");
-                    cmd("dbg step over", function() {
-                      is(output.value, "step over", "debugger stepped over");
-                      cmd("dbg step out", function() {
-                        is(output.value, "step out", "debugger stepped out");
+                  is(output.value, "step in", "debugger stepped in");
+                  cmd("dbg step over", function() {
+                    is(output.value, "step over", "debugger stepped over");
+                    cmd("dbg step out", function() {
+                      is(output.value, "step out", "debugger stepped out");
+                      cmd("dbg continue", function() {
                         cmd("dbg continue", function() {
-                          cmd("dbg continue", function() {
-                            is(output.value, "dbg continue", "debugger continued");
-                            pane.contentWindow.gClient.close(function() {
-                              finish();
-                            });
+                          is(output.value, "dbg continue", "debugger continued");
+                          pane.contentWindow.gClient.close(function() {
+                            finish();
                           });
                         });
                       });
                     });
                   });
                 });
               });
             });
-            EventUtils.sendMouseEvent({type:"click"}, btnDoit);
           });
-          DeveloperToolbarTest.exec({
-            typed: "dbg continue",
-            blankOutput: true
-          });
+          EventUtils.sendMouseEvent({type:"click"}, btnDoit);
+        });
+        DeveloperToolbarTest.exec({
+          typed: "dbg continue",
+          blankOutput: true
         });
       });
+    });
 
-      function cmd(aTyped, aCallback) {
-        pane.contentWindow.gClient.addOneTimeListener("paused", aCallback);
-        DeveloperToolbarTest.exec({
-          typed: aTyped,
-          blankOutput: true
-        });
-      }
-    });
+    function cmd(aTyped, aCallback) {
+      pane.contentWindow.gClient.addOneTimeListener("paused", aCallback);
+      DeveloperToolbarTest.exec({
+        typed: aTyped,
+        blankOutput: true
+      });
+    }
   });
 }
rename from browser/devtools/commandline/test/browser_gcli_break.html
rename to browser/devtools/commandline/test/browser_dbg_cmd_break.html
rename from browser/devtools/commandline/test/browser_gcli_break.js
rename to browser/devtools/commandline/test/browser_dbg_cmd_break.js
--- a/browser/devtools/commandline/test/browser_gcli_break.js
+++ b/browser/devtools/commandline/test/browser_dbg_cmd_break.js
@@ -1,82 +1,120 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the break command works as it should
 
-const TEST_URI = "http://example.com/browser/browser/devtools/commandline/test/browser_gcli_break.html";
+const TEST_URI = "http://example.com/browser/browser/devtools/commandline/" +
+                 "test/browser_dbg_cmd_break.html";
 
 function test() {
-  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
-    testBreakCommands();
-  });
+  DeveloperToolbarTest.test(TEST_URI, [ testBreakCommands ]);
 }
 
 function testBreakCommands() {
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "brea",
-    directTabText: "k",
-    status: "ERROR"
+  helpers.setInput('break');
+  helpers.check({
+    input:  'break',
+    hints:       '',
+    markup: 'IIIII',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "break",
-    status: "ERROR"
+  helpers.setInput('break add');
+  helpers.check({
+    input:  'break add',
+    hints:           '',
+    markup: 'IIIIIVIII',
+    status: 'ERROR'
   });
 
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "break add",
-    status: "ERROR"
-  });
-
-  DeveloperToolbarTest.checkInputStatus({
-    typed: "break add line",
-    emptyParameters: [ " <file>", " <line>" ],
-    status: "ERROR"
+  helpers.setInput('break add line');
+  helpers.check({
+    input:  'break add line',
+    hints:                ' <file> <line>',
+    markup: 'VVVVVVVVVVVVVV',
+    status: 'ERROR'
   });
 
   let pane = DebuggerUI.toggleDebugger();
-  pane._frame.addEventListener("Debugger:Connecting", function dbgConnected() {
+
+  var dbgConnected = DeveloperToolbarTest.checkCalled(function() {
     pane._frame.removeEventListener("Debugger:Connecting", dbgConnected, true);
 
     // Wait for the initial resume.
     let client = pane.contentWindow.gClient;
-    client.addOneTimeListener("resumed", function() {
-      client.activeThread.addOneTimeListener("framesadded", function() {
-        DeveloperToolbarTest.checkInputStatus({
-          typed: "break add line " + TEST_URI + " " + content.wrappedJSObject.line0,
-          status: "VALID"
+
+    var resumed = DeveloperToolbarTest.checkCalled(function() {
+
+      var framesAdded = DeveloperToolbarTest.checkCalled(function() {
+        helpers.setInput('break add line ' + TEST_URI + ' ' + content.wrappedJSObject.line0);
+        helpers.check({
+          hints: '',
+          status: 'VALID',
+          args: {
+            file: { value: TEST_URI },
+            line: { value: content.wrappedJSObject.line0 },
+          }
         });
+
         DeveloperToolbarTest.exec({
           args: {
             type: 'line',
             file: TEST_URI,
             line: content.wrappedJSObject.line0
           },
           completed: false
         });
 
-        DeveloperToolbarTest.checkInputStatus({
-          typed: "break list",
-          status: "VALID"
+        helpers.setInput('break list');
+        helpers.check({
+          input:  'break list',
+          hints:            '',
+          markup: 'VVVVVVVVVV',
+          status: 'VALID'
         });
+
         DeveloperToolbarTest.exec();
 
-        client.activeThread.resume(function() {
-          DeveloperToolbarTest.checkInputStatus({
-            typed: "break del 0",
-            status: "VALID"
+        var cleanup = DeveloperToolbarTest.checkCalled(function() {
+          helpers.setInput('break del 9');
+          helpers.check({
+            input:  'break del 9',
+            hints:             '',
+            markup: 'VVVVVVVVVVE',
+            status: 'ERROR',
+            args: {
+              breakid: { status: 'ERROR', message: '9 is greater than maximum allowed: 0.' },
+            }
           });
+
+          helpers.setInput('break del 0');
+          helpers.check({
+            input:  'break del 0',
+            hints:             '',
+            markup: 'VVVVVVVVVVV',
+            status: 'VALID',
+            args: {
+              breakid: { value: 0 },
+            }
+          });
+
           DeveloperToolbarTest.exec({
             args: { breakid: 0 },
             completed: false
           });
+        });
 
-          finish();
-        });
+        client.activeThread.resume(cleanup);
       });
 
+      client.activeThread.addOneTimeListener("framesadded", framesAdded);
+
       // Trigger newScript notifications using eval.
       content.wrappedJSObject.firstCall();
     });
-  }, true);
+
+    client.addOneTimeListener("resumed", resumed);
+  });
+
+  pane._frame.addEventListener("Debugger:Connecting", dbgConnected, true);
 }
--- a/browser/devtools/commandline/test/browser_gcli_web.js
+++ b/browser/devtools/commandline/test/browser_gcli_web.js
@@ -148,28 +148,63 @@ define('gclitest/index', ['require', 'ex
     options = options || {};
     if (options.settings != null) {
       settings.setDefaults(options.settings);
     }
 
     window.display = new Display(options);
     var requisition = window.display.requisition;
 
-    exports.run({
-      window: window,
-      display: window.display,
-      hideExec: true
-    });
-
-    window.testCommands = function() {
-      require([ 'gclitest/mockCommands' ], function(mockCommands) {
-        mockCommands.setup();
-      });
-    };
-    window.testCommands();
+    // setTimeout keeps stack traces clear of RequireJS frames
+    window.setTimeout(function() {
+      var options = {
+        window: window,
+        display: window.display,
+        hideExec: true
+      };
+      exports.run(options);
+
+      window.createDebugCheck = function() {
+        require([ 'gclitest/helpers' ], function(helpers) {
+          helpers.setup(options);
+          console.log(helpers._createDebugCheck());
+          helpers.shutdown(options);
+        });
+      };
+
+      window.summaryJson = function() {
+        var args = [ 'Requisition: ' ];
+        var summary = display.requisition._summaryJson;
+        Object.keys(summary).forEach(function(name) {
+          args.push(' ' + name + '=');
+          args.push(summary[name]);
+        });
+        console.log.apply(console, args);
+
+        console.log('Focus: ' +
+                    'tooltip=', display.focusManager._shouldShowTooltip(),
+                    'output=', display.focusManager._shouldShowOutput());
+      };
+
+      document.addEventListener('keyup', function(ev) {
+        if (ev.keyCode === 113 /*F2*/) {
+          window.createDebugCheck();
+        }
+        if (ev.keyCode === 115 /*F4*/) {
+          window.summaryJson();
+        }
+      }, true);
+
+      window.testCommands = function() {
+        require([ 'gclitest/mockCommands' ], function(mockCommands) {
+          mockCommands.setup();
+        });
+      };
+      window.testCommands();
+    }, 10);
 
     return {
       /**
        * The exact shape of the object returned by exec is likely to change in
        * the near future. If you do use it, please expect your code to break.
        */
       exec: requisition.exec.bind(requisition),
       update: requisition.update.bind(requisition),
@@ -189,37 +224,40 @@ define('gclitest/index', ['require', 'ex
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testCanon', 'gclitest/testCli', 'gclitest/testCompletion', 'gclitest/testExec', 'gclitest/testHelp', 'gclitest/testHistory', 'gclitest/testInputter', 'gclitest/testIncomplete', 'gclitest/testIntro', 'gclitest/testJs', 'gclitest/testKeyboard', 'gclitest/testPref', 'gclitest/testRequire', 'gclitest/testResource', 'gclitest/testScratchpad', 'gclitest/testSettings', 'gclitest/testSpell', 'gclitest/testSplit', 'gclitest/testTokenize', 'gclitest/testTooltip', 'gclitest/testTypes', 'gclitest/testUtil'], function(require, exports, module) {
+define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testCanon', 'gclitest/testCli', 'gclitest/testCompletion', 'gclitest/testExec', 'gclitest/testFocus', 'gclitest/testHelp', 'gclitest/testHistory', 'gclitest/testInputter', 'gclitest/testIncomplete', 'gclitest/testIntro', 'gclitest/testJs', 'gclitest/testKeyboard', 'gclitest/testMenu', 'gclitest/testNode', 'gclitest/testPref', 'gclitest/testRequire', 'gclitest/testResource', 'gclitest/testScratchpad', 'gclitest/testSettings', 'gclitest/testSpell', 'gclitest/testSplit', 'gclitest/testTokenize', 'gclitest/testTooltip', 'gclitest/testTypes', 'gclitest/testUtil'], 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/testCanon', require('gclitest/testCanon'));
   examiner.addSuite('gclitest/testCli', require('gclitest/testCli'));
   examiner.addSuite('gclitest/testCompletion', require('gclitest/testCompletion'));
   examiner.addSuite('gclitest/testExec', require('gclitest/testExec'));
+  examiner.addSuite('gclitest/testFocus', require('gclitest/testFocus'));
   examiner.addSuite('gclitest/testHelp', require('gclitest/testHelp'));
   examiner.addSuite('gclitest/testHistory', require('gclitest/testHistory'));
   examiner.addSuite('gclitest/testInputter', require('gclitest/testInputter'));
   examiner.addSuite('gclitest/testIncomplete', require('gclitest/testIncomplete'));
   examiner.addSuite('gclitest/testIntro', require('gclitest/testIntro'));
   examiner.addSuite('gclitest/testJs', require('gclitest/testJs'));
   examiner.addSuite('gclitest/testKeyboard', require('gclitest/testKeyboard'));
+  examiner.addSuite('gclitest/testMenu', require('gclitest/testMenu'));
+  examiner.addSuite('gclitest/testNode', require('gclitest/testNode'));
   examiner.addSuite('gclitest/testPref', require('gclitest/testPref'));
   examiner.addSuite('gclitest/testRequire', require('gclitest/testRequire'));
   examiner.addSuite('gclitest/testResource', require('gclitest/testResource'));
   examiner.addSuite('gclitest/testScratchpad', require('gclitest/testScratchpad'));
   examiner.addSuite('gclitest/testSettings', require('gclitest/testSettings'));
   examiner.addSuite('gclitest/testSpell', require('gclitest/testSpell'));
   examiner.addSuite('gclitest/testSplit', require('gclitest/testSplit'));
   examiner.addSuite('gclitest/testTokenize', require('gclitest/testTokenize'));
@@ -738,16 +776,24 @@ define('test/status', ['require', 'expor
  */
 
 define('gclitest/testCanon', ['require', 'exports', 'module' , 'gclitest/helpers', 'gcli/canon', 'test/assert'], function(require, exports, module) {
 
   var helpers = require('gclitest/helpers');
   var canon = require('gcli/canon');
   var test = require('test/assert');
 
+  exports.setup = function(options) {
+    helpers.setup(options);
+  };
+
+  exports.shutdown = function(options) {
+    helpers.shutdown(options);
+  };
+
   exports.testAddRemove = function(options) {
     var startCount = canon.getCommands().length;
     var events = 0;
 
     var canonChange = function(ev) {
       events++;
     };
     canon.onCanonChange.add(canonChange);
@@ -779,17 +825,19 @@ define('gclitest/testCanon', ['require',
       typed: 'testadd',
       outputMatch: /^2$/
     });
 
     canon.removeCommand('testadd');
 
     test.is(canon.getCommands().length, startCount, 'remove command success');
     test.is(events, 3, 'remove event');
-    helpers.status(options, {
+
+    helpers.setInput('testadd');
+    helpers.check({
       typed: 'testadd',
       status: 'ERROR'
     });
 
     canon.addCommand({
       name: 'testadd',
       exec: function() {
         return 3;
@@ -804,17 +852,19 @@ define('gclitest/testCanon', ['require',
     });
 
     canon.removeCommand({
       name: 'testadd'
     });
 
     test.is(canon.getCommands().length, startCount, 'reremove command success');
     test.is(events, 5, 'reremove event');
-    helpers.status(options, {
+
+    helpers.setInput('testadd');
+    helpers.check({
       typed: 'testadd',
       status: 'ERROR'
     });
 
     canon.removeCommand({ name: 'nonexistant' });
     test.is(canon.getCommands().length, startCount, 'nonexistant1 command success');
     test.is(events, 5, 'nonexistant1 event');
 
@@ -843,269 +893,288 @@ define('gclitest/testCanon', ['require',
  */
 
 define('gclitest/helpers', ['require', 'exports', 'module' , 'test/assert', 'gcli/util'], function(require, exports, module) {
 
 
 var test = require('test/assert');
 var util = require('gcli/util');
 
-
-var cachedOptions = undefined;
-
-exports.setup = function(opts) {
-  cachedOptions = opts;
-};
-
-exports.shutdown = function(opts) {
-  cachedOptions = undefined;
+var helpers = exports;
+
+helpers._display = undefined;
+
+helpers.setup = function(options) {
+  helpers._display = options.display;
+};
+
+helpers.shutdown = function(options) {
+  helpers._display = undefined;
 };
 
 /**
- * Check that we can parse command input.
- * Doesn't execute the command, just checks that we grok the input properly:
- *
- * helpers.status({
- *   // Test inputs
- *   typed: "ech",           // Required
- *   cursor: 3,              // Optional cursor position
- *
- *   // Thing to check
- *   status: "INCOMPLETE",   // One of "VALID", "ERROR", "INCOMPLETE"
- *   emptyParameters: [ "<message>" ], // Still to type
- *   directTabText: "o",     // Simple completion text
- *   arrowTabText: "",       // When the completion is not an extension
- *   markup: "VVVIIIEEE",    // What state should the error markup be in
- * });
+ * Various functions to return the actual state of the command line
  */
-exports.status = function(options, checks) {
-  var requisition = options.display.requisition;
-  var inputter = options.display.inputter;
-  var completer = options.display.completer;
-
-  if (checks.typed) {
-    inputter.setInput(checks.typed);
+helpers._actual = {
+  input: function() {
+    return helpers._display.inputter.element.value;
+  },
+
+  hints: function() {
+    var templateData = helpers._display.completer._getCompleterTemplateData();
+    var actualHints = templateData.directTabText +
+                      templateData.emptyParameters.join('') +
+                      templateData.arrowTabText;
+    return actualHints.replace(/\u00a0/g, ' ')
+                      .replace(/\u21E5/, '->')
+                      .replace(/ $/, '');
+  },
+
+  markup: function() {
+    var cursor = helpers._display.inputter.element.selectionStart;
+    var statusMarkup = helpers._display.requisition.getInputStatusMarkup(cursor);
+    return statusMarkup.map(function(s) {
+      return Array(s.string.length + 1).join(s.status.toString()[0]);
+    }).join('');
+  },
+
+  cursor: function() {
+    return helpers._display.inputter.element.selectionStart;
+  },
+
+  current: function() {
+    return helpers._display.requisition.getAssignmentAt(helpers._actual.cursor()).param.name;
+  },
+
+  status: function() {
+    return helpers._display.requisition.getStatus().toString();
+  },
+
+  outputState: function() {
+    var outputData = helpers._display.focusManager._shouldShowOutput();
+    return outputData.visible + ':' + outputData.reason;
+  },
+
+  tooltipState: function() {
+    var tooltipData = helpers._display.focusManager._shouldShowTooltip();
+    return tooltipData.visible + ':' + tooltipData.reason;
+  }
+};
+
+helpers._directToString = [ 'boolean', 'undefined', 'number' ];
+
+helpers._createDebugCheck = function() {
+  var requisition = helpers._display.requisition;
+  var command = requisition.commandAssignment.value;
+  var input = helpers._actual.input();
+  var padding = Array(input.length + 1).join(' ');
+
+  var output = '';
+  output += 'helpers.setInput(\'' + input + '\');\n';
+  output += 'helpers.check({\n';
+  output += '  input:  \'' + input + '\',\n';
+  output += '  hints:  ' + padding + '\'' + helpers._actual.hints() + '\',\n';
+  output += '  markup: \'' + helpers._actual.markup() + '\',\n';
+  output += '  cursor: ' + helpers._actual.cursor() + ',\n';
+  output += '  current: \'' + helpers._actual.current() + '\',\n';
+  output += '  status: \'' + helpers._actual.status() + '\',\n';
+  output += '  outputState: \'' + helpers._actual.outputState() + '\',\n';
+
+  if (command) {
+    output += '  tooltipState: \'' + helpers._actual.tooltipState() + '\',\n';
+    output += '  args: {\n';
+    output += '    command: { name: \'' + command.name + '\' },\n';
+
+    requisition.getAssignments().forEach(function(assignment) {
+      output += '    ' + assignment.param.name + ': { ';
+
+      if (typeof assignment.value === 'string') {
+        output += 'value: \'' + assignment.value + '\', ';
+      }
+      else if (helpers._directToString.indexOf(typeof assignment.value) !== -1) {
+        output += 'value: ' + assignment.value + ', ';
+      }
+      else if (assignment.value === null) {
+        output += 'value: ' + assignment.value + ', ';
+      }
+      else {
+        output += '/*value:' + assignment.value + ',*/ ';
+      }
+
+      output += 'arg: \'' + assignment.arg + '\', ';
+      output += 'status: \'' + assignment.getStatus().toString() + '\', ';
+      output += 'message: \'' + assignment.getMessage() + '\'';
+      output += ' },\n';
+    });
+
+    output += '  }\n';
   }
   else {
-    test.ok(false, "Missing typed for " + JSON.stringify(checks));
-    return;
-  }
-
-  if (checks.cursor) {
-    inputter.setCursor(checks.cursor);
-  }
-
-  if (checks.status) {
-    test.is(requisition.getStatus().toString(),
-            checks.status,
-            "status for " + checks.typed);
+    output += '  tooltipState: \'' + helpers._actual.tooltipState() + '\'\n';
   }
-
-  var data = completer._getCompleterTemplateData();
-  if (checks.emptyParameters != null) {
-    var realParams = data.emptyParameters;
-    test.is(realParams.length,
-            checks.emptyParameters.length,
-            'emptyParameters.length for \'' + checks.typed + '\'');
-
-    if (realParams.length === checks.emptyParameters.length) {
-      for (var i = 0; i < realParams.length; i++) {
-        test.is(realParams[i].replace(/\u00a0/g, ' '),
-                checks.emptyParameters[i],
-                'emptyParameters[' + i + '] for \'' + checks.typed + '\'');
-      }
-    }
-  }
-
-  if (checks.markup) {
-    var cursor = checks.cursor ? checks.cursor.start : checks.typed.length;
-    var statusMarkup = requisition.getInputStatusMarkup(cursor);
-    var actualMarkup = statusMarkup.map(function(s) {
-      return Array(s.string.length + 1).join(s.status.toString()[0]);
-    }).join('');
-
-    test.is(checks.markup,
-            actualMarkup,
-            'markup for ' + checks.typed);
-  }
-
-  if (checks.directTabText) {
-    test.is(data.directTabText,
-            checks.directTabText,
-            'directTabText for \'' + checks.typed + '\'');
-  }
-
-  if (checks.arrowTabText) {
-    test.is(' \u00a0\u21E5 ' + checks.arrowTabText,
-            data.arrowTabText,
-            'arrowTabText for \'' + checks.typed + '\'');
-  }
+  output += '});';
+
+  return output;
 };
 
 /**
  * We're splitting status into setup() which alters the state of the system
  * and check() which ensures that things are in the right place afterwards.
  */
-exports.setInput = function(typed, cursor) {
-  cachedOptions.display.inputter.setInput(typed);
+helpers.setInput = function(typed, cursor) {
+  helpers._display.inputter.setInput(typed);
 
   if (cursor) {
-    cachedOptions.display.inputter.setCursor({ start: cursor, end: cursor });
+    helpers._display.inputter.setCursor({ start: cursor, end: cursor });
   }
+
+  helpers._display.focusManager.onInputChange();
+};
+
+/**
+ * Simulate focusing the input field
+ */
+helpers.focusInput = function() {
+  helpers._display.inputter.focus();
 };
 
 /**
  * Simulate pressing TAB in the input field
  */
-exports.pressTab = function() {
-  // requisition.complete({ start: 5, end: 5 }, 0);
-
+helpers.pressTab = function() {
+  helpers.pressKey(9 /*KeyEvent.DOM_VK_TAB*/);
+};
+
+/**
+ * Simulate pressing RETURN in the input field
+ */
+helpers.pressReturn = function() {
+  helpers.pressKey(13 /*KeyEvent.DOM_VK_RETURN*/);
+};
+
+/**
+ * Simulate pressing a key by keyCode in the input field
+ */
+helpers.pressKey = function(keyCode) {
   var fakeEvent = {
-    keyCode: util.KeyEvent.DOM_VK_TAB,
+    keyCode: keyCode,
     preventDefault: function() { },
     timeStamp: new Date().getTime()
   };
-  cachedOptions.display.inputter.onKeyDown(fakeEvent);
-  cachedOptions.display.inputter.onKeyUp(fakeEvent);
+  helpers._display.inputter.onKeyDown(fakeEvent);
+  helpers._display.inputter.onKeyUp(fakeEvent);
 };
 
 /**
  * check() is the new status. Similar API except that it doesn't attempt to
  * alter the display/requisition at all, and it makes extra checks.
  * Available checks:
  *   input: The text displayed in the input field
  *   cursor: The position of the start of the cursor
  *   status: One of "VALID", "ERROR", "INCOMPLETE"
- *   emptyParameters: Array of parameters still to type. e.g. [ "<message>" ]
- *   directTabText: Simple completion text
- *   arrowTabText: When the completion is not an extension (without arrow)
+ *   hints: The hint text, i.e. a concatenation of the directTabText, the
+ *     emptyParameters and the arrowTabText. The text as inserted into the UI
+ *     will include NBSP and Unicode RARR characters, these should be
+ *     represented using normal space and '->' for the arrow
  *   markup: What state should the error markup be in. e.g. "VVVIIIEEE"
  *   args: Maps of checks to make against the arguments:
  *     value: i.e. assignment.value (which ignores defaultValue)
  *     type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned
  *           Care should be taken with this since it's something of an
  *           implementation detail
  *     arg: The toString value of the argument
  *     status: i.e. assignment.getStatus
  *     message: i.e. assignment.getMessage
  *     name: For commands - checks assignment.value.name
  */
-exports.check = function(checks) {
-  var requisition = cachedOptions.display.requisition;
-  var completer = cachedOptions.display.completer;
-  var actual = completer._getCompleterTemplateData();
-
-  if (checks.input) {
-    test.is(cachedOptions.display.inputter.element.value,
-            checks.input,
-            'input');
+helpers.check = function(checks) {
+  if ('input' in checks) {
+    test.is(helpers._actual.input(), checks.input, 'input');
+  }
+
+  if ('cursor' in checks) {
+    test.is(helpers._actual.cursor(), checks.cursor, 'cursor');
   }
 
-  if (checks.cursor) {
-    test.is(cachedOptions.display.inputter.element.selectionStart,
-            checks.cursor,
-            'cursor');
+  if ('current' in checks) {
+    test.is(helpers._actual.current(), checks.current, 'current');
   }
 
-  if (checks.status) {
-    test.is(requisition.getStatus().toString(),
-            checks.status,
-            'status');
+  if ('status' in checks) {
+    test.is(helpers._actual.status(), checks.status, 'status');
   }
 
-  if (checks.markup) {
-    var cursor = cachedOptions.display.inputter.element.selectionStart;
-    var statusMarkup = requisition.getInputStatusMarkup(cursor);
-    var actualMarkup = statusMarkup.map(function(s) {
-      return Array(s.string.length + 1).join(s.status.toString()[0]);
-    }).join('');
-
-    test.is(checks.markup,
-            actualMarkup,
-            'markup');
+  if ('markup' in checks) {
+    test.is(helpers._actual.markup(), checks.markup, 'markup');
+  }
+
+  if ('hints' in checks) {
+    test.is(helpers._actual.hints(), checks.hints, 'hints');
   }
 
-  if (checks.emptyParameters) {
-    var actualParams = actual.emptyParameters;
-    test.is(actualParams.length,
-            checks.emptyParameters.length,
-            'emptyParameters.length');
-
-    if (actualParams.length === checks.emptyParameters.length) {
-      for (var i = 0; i < actualParams.length; i++) {
-        test.is(actualParams[i].replace(/\u00a0/g, ' '),
-                checks.emptyParameters[i],
-                'emptyParameters[' + i + ']');
-      }
-    }
+  if ('tooltipState' in checks) {
+    test.is(helpers._actual.tooltipState(), checks.tooltipState, 'tooltipState');
   }
 
-  if (checks.directTabText) {
-    test.is(actual.directTabText,
-            checks.directTabText,
-            'directTabText');
+  if ('outputState' in checks) {
+    test.is(helpers._actual.outputState(), checks.outputState, 'outputState');
   }
 
-  if (checks.arrowTabText) {
-    test.is(actual.arrowTabText,
-            ' \u00a0\u21E5 ' + checks.arrowTabText,
-            'arrowTabText');
-  }
-
-  if (checks.args) {
+  if (checks.args != null) {
+    var requisition = helpers._display.requisition;
     Object.keys(checks.args).forEach(function(paramName) {
       var check = checks.args[paramName];
 
       var assignment;
       if (paramName === 'command') {
         assignment = requisition.commandAssignment;
       }
       else {
         assignment = requisition.getAssignment(paramName);
       }
 
       if (assignment == null) {
         test.ok(false, 'Unknown arg: ' + paramName);
         return;
       }
 
-      if (check.value) {
+      if ('value' in check) {
         test.is(assignment.value,
                 check.value,
-                'arg[\'' + paramName + '\'].value');
+                'arg.' + paramName + '.value');
       }
 
-      if (check.name) {
+      if ('name' in check) {
         test.is(assignment.value.name,
                 check.name,
-                'arg[\'' + paramName + '\'].name');
+                'arg.' + paramName + '.name');
       }
 
-      if (check.type) {
+      if ('type' in check) {
         test.is(assignment.arg.type,
                 check.type,
-                'arg[\'' + paramName + '\'].type');
+                'arg.' + paramName + '.type');
       }
 
-      if (check.arg) {
+      if ('arg' in check) {
         test.is(assignment.arg.toString(),
                 check.arg,
-                'arg[\'' + paramName + '\'].arg');
+                'arg.' + paramName + '.arg');
       }
 
-      if (check.status) {
+      if ('status' in check) {
         test.is(assignment.getStatus().toString(),
                 check.status,
-                'arg[\'' + paramName + '\'].status');
+                'arg.' + paramName + '.status');
       }
 
-      if (check.message) {
+      if ('message' in check) {
         test.is(assignment.getMessage(),
                 check.message,
-                'arg[\'' + paramName + '\'].message');
+                'arg.' + paramName + '.message');
       }
     });
   }
 };
 
 /**
  * Execute a command:
  *
@@ -1114,17 +1183,17 @@ exports.check = function(checks) {
  *   typed: "echo hi",        // Optional, uses existing if undefined
  *
  *   // Thing to check
  *   args: { message: "hi" }, // Check that the args were understood properly
  *   outputMatch: /^hi$/,     // Regex to test against textContent of output
  *   blankOutput: true,       // Special checks when there is no output
  * });
  */
-exports.exec = function(options, tests) {
+helpers.exec = function(options, tests) {
   var requisition = options.display.requisition;
   var inputter = options.display.inputter;
 
   tests = tests || {};
 
   if (tests.typed) {
     inputter.setInput(tests.typed);
   }
@@ -1175,17 +1244,17 @@ exports.exec = function(options, tests) 
     return;
   }
 
   var div = options.window.document.createElement('div');
   output.toDom(div);
   var displayed = div.textContent.trim();
 
   if (tests.outputMatch) {
-    function doTest(match, against) {
+    var doTest = function(match, against) {
       if (!match.test(against)) {
         test.ok(false, "html output for " + typed + " against " + match.source);
         console.log("Actual textContent");
         console.log(against);
       }
     }
     if (Array.isArray(tests.outputMatch)) {
       tests.outputMatch.forEach(function(match) {
@@ -1710,16 +1779,17 @@ exports.setup = function() {
   exports.option1.type = types.getType('string');
   exports.option2.type = types.getType('number');
 
   types.registerType(exports.optionType);
   types.registerType(exports.optionValue);
 
   canon.addCommand(exports.tsv);
   canon.addCommand(exports.tsr);
+  canon.addCommand(exports.tso);
   canon.addCommand(exports.tse);
   canon.addCommand(exports.tsj);
   canon.addCommand(exports.tsb);
   canon.addCommand(exports.tss);
   canon.addCommand(exports.tsu);
   canon.addCommand(exports.tsn);
   canon.addCommand(exports.tsnDif);
   canon.addCommand(exports.tsnExt);
@@ -1728,22 +1798,25 @@ exports.setup = function() {
   canon.addCommand(exports.tsnExtend);
   canon.addCommand(exports.tsnDeep);
   canon.addCommand(exports.tsnDeepDown);
   canon.addCommand(exports.tsnDeepDownNested);
   canon.addCommand(exports.tsnDeepDownNestedCmd);
   canon.addCommand(exports.tselarr);
   canon.addCommand(exports.tsm);
   canon.addCommand(exports.tsg);
+  canon.addCommand(exports.tshidden);
   canon.addCommand(exports.tscook);
+  canon.addCommand(exports.tslong);
 };
 
 exports.shutdown = function() {
   canon.removeCommand(exports.tsv);
   canon.removeCommand(exports.tsr);
+  canon.removeCommand(exports.tso);
   canon.removeCommand(exports.tse);
   canon.removeCommand(exports.tsj);
   canon.removeCommand(exports.tsb);
   canon.removeCommand(exports.tss);
   canon.removeCommand(exports.tsu);
   canon.removeCommand(exports.tsn);
   canon.removeCommand(exports.tsnDif);
   canon.removeCommand(exports.tsnExt);
@@ -1752,17 +1825,19 @@ exports.shutdown = function() {
   canon.removeCommand(exports.tsnExtend);
   canon.removeCommand(exports.tsnDeep);
   canon.removeCommand(exports.tsnDeepDown);
   canon.removeCommand(exports.tsnDeepDownNested);
   canon.removeCommand(exports.tsnDeepDownNestedCmd);
   canon.removeCommand(exports.tselarr);
   canon.removeCommand(exports.tsm);
   canon.removeCommand(exports.tsg);
+  canon.removeCommand(exports.tshidden);
   canon.removeCommand(exports.tscook);
+  canon.removeCommand(exports.tslong);
 
   types.deregisterType(exports.optionType);
   types.deregisterType(exports.optionValue);
 };
 
 
 exports.option1 = { type: types.getType('string') };
 exports.option2 = { type: types.getType('number') };
@@ -1825,19 +1900,34 @@ exports.tsv = {
 };
 
 exports.tsr = {
   name: 'tsr',
   params: [ { name: 'text', type: 'string' } ],
   exec: createExec('tsr')
 };
 
+exports.tso = {
+  name: 'tso',
+  params: [ { name: 'text', type: 'string', defaultValue: null } ],
+  exec: createExec('tso')
+};
+
 exports.tse = {
   name: 'tse',
-  params: [ { name: 'node', type: 'node' } ],
+  params: [
+    { name: 'node', type: 'node' },
+    {
+      group: 'options',
+      params: [
+        { name: 'nodes', type: { name: 'nodelist' } },
+        { name: 'nodes2', type: { name: 'nodelist', allowEmpty: true } }
+      ]
+    }
+  ],
   exec: createExec('tse')
 };
 
 exports.tsj = {
   name: 'tsj',
   params: [ { name: 'javascript', type: 'javascript' } ],
   exec: createExec('tsj')
 };
@@ -1906,16 +1996,48 @@ exports.tsnDeepDownNested = {
   name: 'tsn deep down nested',
 };
 
 exports.tsnDeepDownNestedCmd = {
   name: 'tsn deep down nested cmd',
   exec: createExec('tsnDeepDownNestedCmd')
 };
 
+exports.tshidden = {
+  name: 'tshidden',
+  hidden: true,
+  params: [
+    {
+      group: 'Options',
+      params: [
+        {
+          name: 'visible',
+          type: 'string',
+          defaultValue: null,
+          description: 'visible'
+        },
+        {
+          name: 'invisiblestring',
+          type: 'string',
+          description: 'invisiblestring',
+          defaultValue: null,
+          hidden: true
+        },
+        {
+          name: 'invisibleboolean',
+          type: 'boolean',
+          description: 'invisibleboolean',
+          hidden: true
+        }
+      ]
+    }
+  ],
+  exec: createExec('tshidden')
+};
+
 exports.tselarr = {
   name: 'tselarr',
   params: [
     { name: 'num', type: { name: 'selection', data: [ '1', '2', '3' ] } },
     { name: 'arr', type: { name: 'array', subtype: 'string' } },
   ],
   exec: createExec('tselarr')
 };
@@ -2012,16 +2134,78 @@ exports.tscook = {
           description: 'tscookSecureDesc'
         }
       ]
     }
   ],
   exec: createExec('tscook')
 };
 
+exports.tslong = {
+  name: 'tslong',
+  description: 'long param tests to catch problems with the jsb command',
+  returnValue:'string',
+  params: [
+    {
+      name: 'msg',
+      type: 'string',
+      description: 'msg Desc'
+    },
+    {
+      group: "Options Desc",
+      params: [
+        {
+          name: 'num',
+          type: 'number',
+          description: 'num Desc',
+          defaultValue: 2
+        },
+        {
+          name: 'sel',
+          type: {
+            name: 'selection',
+            lookup: [
+              { name: "space", value: " " },
+              { name: "tab", value: "\t" }
+            ]
+          },
+          description: 'sel Desc',
+          defaultValue: ' ',
+        },
+        {
+          name: 'bool',
+          type: 'boolean',
+          description: 'bool Desc'
+        },
+        {
+          name: 'num2',
+          type: 'number',
+          description: 'num2 Desc',
+          defaultValue: -1
+        },
+        {
+          name: 'bool2',
+          type: 'boolean',
+          description: 'bool2 Desc'
+        },
+        {
+          name: 'sel2',
+          type: {
+            name: 'selection',
+            data: ['collapse', 'expand', 'end-expand', 'expand-strict']
+          },
+          description: 'sel2 Desc',
+          defaultValue: "collapse"
+        }
+      ]
+    }
+  ],
+  exec: createExec('tslong')
+};
+
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -2056,285 +2240,356 @@ exports.shutdown = function(options) {
 exports.testActivate = function(options) {
   if (!options.display) {
     test.log('No display. Skipping activate tests');
     return;
   }
 
   helpers.setInput('');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ''
   });
 
   helpers.setInput(' ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ''
   });
 
   helpers.setInput('tsr');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <text>' ]
+    hints: ' <text>'
   });
 
   helpers.setInput('tsr ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ '<text>' ]
+    hints: '<text>'
   });
 
   helpers.setInput('tsr b');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ''
   });
 
   helpers.setInput('tsb');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' [toggle]' ]
+    hints: ' [toggle]'
   });
 
   helpers.setInput('tsm');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <abc>', ' <txt>', ' <num>' ]
+    hints: ' <abc> <txt> <num>'
   });
 
   helpers.setInput('tsm ');
   helpers.check({
-    emptyParameters: [ ' <txt>', ' <num>' ],
-    arrowTabText: '',
-    directTabText: 'a'
+    hints: 'a <txt> <num>'
   });
 
   helpers.setInput('tsm a');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <txt>', ' <num>' ]
+    hints: ' <txt> <num>'
   });
 
   helpers.setInput('tsm a ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ '<txt>', ' <num>' ]
+    hints: '<txt> <num>'
   });
 
   helpers.setInput('tsm a  ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ '<txt>', ' <num>' ]
+    hints: '<txt> <num>'
   });
 
   helpers.setInput('tsm a  d');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <num>' ]
+    hints: ' <num>'
   });
 
   helpers.setInput('tsm a "d d"');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <num>' ]
+    hints: ' <num>'
   });
 
   helpers.setInput('tsm a "d ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <num>' ]
+    hints: ' <num>'
   });
 
   helpers.setInput('tsm a "d d" ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ '<num>' ]
+    hints: '<num>'
   });
 
   helpers.setInput('tsm a "d d ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <num>' ]
+    hints: ' <num>'
   });
 
   helpers.setInput('tsm d r');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <num>' ]
+    hints: ' <num>'
   });
 
   helpers.setInput('tsm a d ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ '<num>' ]
+    hints: '<num>'
   });
 
   helpers.setInput('tsm a d 4');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ''
   });
 
   helpers.setInput('tsg');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: [ ' <solo>' ]
+    hints: ' <solo> [options]'
   });
 
   helpers.setInput('tsg ');
   helpers.check({
-    emptyParameters: [],
-    arrowTabText: '',
-    directTabText: 'aaa'
+    hints: 'aaa [options]'
   });
 
   helpers.setInput('tsg a');
   helpers.check({
-    emptyParameters: [],
-    arrowTabText: '',
-    directTabText: 'aa'
+    hints: 'aa [options]'
   });
 
   helpers.setInput('tsg b');
   helpers.check({
-    emptyParameters: [],
-    arrowTabText: '',
-    directTabText: 'bb'
+    hints: 'bb [options]'
   });
 
   helpers.setInput('tsg d');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ' [options]'
   });
 
   helpers.setInput('tsg aa');
   helpers.check({
-    emptyParameters: [],
-    arrowTabText: '',
-    directTabText: 'a'
+    hints: 'a [options]'
   });
 
   helpers.setInput('tsg aaa');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ' [options]'
   });
 
   helpers.setInput('tsg aaa ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: '[options]'
   });
 
   helpers.setInput('tsg aaa d');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ' [options]'
   });
 
   helpers.setInput('tsg aaa dddddd');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ' [options]'
   });
 
   helpers.setInput('tsg aaa dddddd ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: '[options]'
   });
 
   helpers.setInput('tsg aaa "d');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ' [options]'
   });
 
   helpers.setInput('tsg aaa "d d');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ' [options]'
   });
 
   helpers.setInput('tsg aaa "d d"');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ' [options]'
   });
 
   helpers.setInput('tsn ex ');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ''
   });
 
   helpers.setInput('selarr');
   helpers.check({
-    directTabText: '',
-    emptyParameters: [],
-    arrowTabText: 'tselarr'
+    hints: ' -> tselarr'
   });
 
   helpers.setInput('tselar 1');
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ''
   });
 
   helpers.setInput('tselar 1', 7);
   helpers.check({
-    directTabText: '',
-    arrowTabText: '',
-    emptyParameters: []
+    hints: ''
   });
 
   helpers.setInput('tselar 1', 6);
   helpers.check({
-    directTabText: '',
-    emptyParameters: [],
-    arrowTabText: 'tselarr'
+    hints: ' -> tselarr'
   });
 
   helpers.setInput('tselar 1', 5);
   helpers.check({
-    directTabText: '',
-    emptyParameters: [],
-    arrowTabText: 'tselarr'
+    hints: ' -> tselarr'
+  });
+};
+
+exports.testLong = function(options) {
+  helpers.setInput('tslong --sel');
+  helpers.check({
+    input:  'tslong --sel',
+    hints:              ' <selection> <msg> [options]',
+    markup: 'VVVVVVVIIIII'
+  });
+
+  helpers.pressTab();
+  helpers.check({
+    input:  'tslong --sel ',
+    hints:               'space <msg> [options]',
+    markup: 'VVVVVVVIIIIIV'
+  });
+
+  helpers.setInput('tslong --sel ');
+  helpers.check({
+    input:  'tslong --sel ',
+    hints:               'space <msg> [options]',
+    markup: 'VVVVVVVIIIIIV'
+  });
+
+  helpers.setInput('tslong --sel s');
+  helpers.check({
+    input:  'tslong --sel s',
+    hints:                'pace <msg> [options]',
+    markup: 'VVVVVVVIIIIIVI'
+  });
+
+  helpers.setInput('tslong --num ');
+  helpers.check({
+    input:  'tslong --num ',
+    hints:               '<number> <msg> [options]',
+    markup: 'VVVVVVVIIIIIV'
+  });
+
+  helpers.setInput('tslong --num 42');
+  helpers.check({
+    input:  'tslong --num 42',
+    hints:                 ' <msg> [options]',
+    markup: 'VVVVVVVVVVVVVVV'
+  });
+
+  helpers.setInput('tslong --num 42 ');
+  helpers.check({
+    input:  'tslong --num 42 ',
+    hints:                  '<msg> [options]',
+    markup: 'VVVVVVVVVVVVVVVV'
+  });
+
+  helpers.setInput('tslong --num 42 --se');
+  helpers.check({
+    input:  'tslong --num 42 --se',
+    hints:                      'l <msg> [options]',
+    markup: 'VVVVVVVVVVVVVVVVIIII'
+  });
+
+  helpers.pressTab();
+  helpers.check({
+    input:  'tslong --num 42 --sel ',
+    hints:                        'space <msg> [options]',
+    markup: 'VVVVVVVVVVVVVVVVIIIIIV'
+  });
+
+  helpers.pressTab();
+  helpers.check({
+    input:  'tslong --num 42 --sel space ',
+    hints:                              '<msg> [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV'
+  });
+
+  helpers.setInput('tslong --num 42 --sel ');
+  helpers.check({
+    input:  'tslong --num 42 --sel ',
+    hints:                        'space <msg> [options]',
+    markup: 'VVVVVVVVVVVVVVVVIIIIIV'
+  });
+
+  helpers.setInput('tslong --num 42 --sel space ');
+  helpers.check({
+    input:  'tslong --num 42 --sel space ',
+    hints:                              '<msg> [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV'
+  });
+};
+
+exports.testNoTab = function(options) {
+  helpers.setInput('tss');
+  helpers.pressTab();
+  helpers.check({
+    input:  'tss ',
+    markup: 'VVVV',
+    hints: ''
+  });
+
+  helpers.pressTab();
+  helpers.check({
+    input:  'tss ',
+    markup: 'VVVV',
+    hints: ''
+  });
+
+  helpers.setInput('xxxx');
+  helpers.check({
+    input:  'xxxx',
+    markup: 'EEEE',
+    hints: ''
+  });
+
+  helpers.pressTab();
+  helpers.check({
+    input:  'xxxx',
+    markup: 'EEEE',
+    hints: ''
+  });
+};
+
+exports.testOutstanding = function(options) {
+  // See bug 779800
+  /*
+  helpers.setInput('tsg --txt1 ddd ');
+  helpers.check({
+    input:  'tsg --txt1 ddd ',
+    hints:                 'aaa [options]',
+    markup: 'VVVVVVVVVVVVVVV'
+  });
+  */
+};
+
+exports.testCompleteIntoOptional = function(options) {
+  // From bug 779816
+  helpers.setInput('tso ');
+  helpers.check({
+    typed:  'tso ',
+    hints:      '[text]',
+    markup: 'VVVV',
+    status: 'VALID'
+  });
+
+  helpers.setInput('tso');
+  helpers.pressTab();
+  helpers.check({
+    typed:  'tso ',
+    hints:      '[text]',
+    markup: 'VVVV',
+    status: 'VALID'
   });
 };
 
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
@@ -2359,16 +2614,17 @@ var canon = require('gcli/canon');
 var mockCommands = require('gclitest/mockCommands');
 var nodetype = require('gcli/types/node');
 
 var test = require('test/assert');
 
 var actualExec;
 var actualOutput;
 var hideExec = false;
+var skip = 'skip';
 
 exports.setup = function() {
   mockCommands.setup();
   mockCommands.onCommandExec.add(commandExeced);
   canon.commandOutputManager.onOutput.add(commandOutputed);
 };
 
 exports.shutdown = function() {
@@ -2406,16 +2662,20 @@ function exec(command, expectedArgs) {
   }
 
   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 (expectedArg === skip) {
+      return;
+    }
+
     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);
@@ -2461,17 +2721,17 @@ exports.testExec = function(options) {
   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 });
+  exec('tse :root', { node: mockBody, nodes: skip, nodes2: skip });
   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' ] });
@@ -2492,23 +2752,79 @@ var mockDoc = {
     if (css === ':root') {
       return {
         length: 1,
         item: function(i) {
           return mockBody;
         }
       };
     }
-    throw new Error('mockDoc.querySelectorAll(\'' + css + '\') error');
+    else {
+      return {
+        length: 0,
+        item: function() { return null; }
+      };
+    }
   }
 };
 
 
 });
 /*
+ * 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/testFocus', ['require', 'exports', 'module' , 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
+
+
+var helpers = require('gclitest/helpers');
+var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+  mockCommands.setup();
+  helpers.setup(options);
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+  helpers.shutdown(options);
+};
+
+exports.testBasic = function(options) {
+  helpers.focusInput();
+  helpers.exec(options, 'help');
+
+  helpers.setInput('tsn deep');
+  helpers.check({
+    input:  'tsn deep',
+    hints:          '',
+    markup: 'IIIVIIII',
+    cursor: 8,
+    status: 'ERROR',
+    outputState: 'false:default',
+    tooltipState: 'false:default'
+  });
+
+  helpers.pressReturn();
+  helpers.check({
+    input:  'tsn deep',
+    hints:          '',
+    markup: 'IIIVIIII',
+    cursor: 8,
+    status: 'ERROR',
+    outputState: 'false:default',
+    tooltipState: 'true:isError'
+  });
+};
+
+
+});
+/*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  * http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -2518,36 +2834,65 @@ var mockDoc = {
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 define('gclitest/testHelp', ['require', 'exports', 'module' , 'gclitest/helpers'], function(require, exports, module) {
 
   var helpers = require('gclitest/helpers');
 
+  exports.setup = function(options) {
+    helpers.setup(options);
+  };
+
+  exports.shutdown = function(options) {
+    helpers.shutdown(options);
+  };
+
   exports.testHelpStatus = function(options) {
-    helpers.status(options, {
+    helpers.setInput('help');
+    helpers.check({
       typed:  'help',
+      hints:      ' [search]',
       markup: 'VVVV',
-      status: 'VALID',
-      emptyParameters: [ " [search]" ]
+      status: 'VALID'
+    });
+
+    helpers.setInput('help ');
+    helpers.check({
+      typed:  'help ',
+      hints:       '[search]',
+      markup: 'VVVVV',
+      status: 'VALID'
     });
 
-    helpers.status(options, {
+    // From bug 779816
+    helpers.setInput('help');
+    helpers.pressTab();
+    helpers.check({
+      typed:  'help ',
+      hints:       '[search]',
+      markup: 'VVVVV',
+      status: 'VALID'
+    });
+
+    helpers.setInput('help foo');
+    helpers.check({
       typed:  'help foo',
       markup: 'VVVVVVVV',
       status: 'VALID',
-      emptyParameters: [ ]
+      hints:  ''
     });
 
-    helpers.status(options, {
+    helpers.setInput('help foo bar');
+    helpers.check({
       typed:  'help foo bar',
       markup: 'VVVVVVVVVVVV',
       status: 'VALID',
-      emptyParameters: [ ]
+      hints:  ''
     });
   };
 
   exports.testHelpExec = function(options) {
     if (options.isFirefox) {
       helpers.exec(options, {
         typed: 'help',
         args: { search: null },
@@ -2830,156 +3175,140 @@ exports.testBasic = function(options) {
 };
 
 exports.testCompleted = function(options) {
   helpers.setInput('tsela');
   helpers.pressTab();
   helpers.check({
     args: {
       command: { name: 'tselarr', type: 'Argument' },
-      num: { type: 'Argument' },
+      num: { type: 'BlankArgument' },
       arr: { type: 'ArrayArgument' },
     }
   });
 
   helpers.setInput('tsn dif ');
   helpers.check({
     input:  'tsn dif ',
+    hints:          '<text>',
     markup: 'VVVVVVVV',
     cursor: 8,
-    directTabText: '',
-    arrowTabText: '',
     status: 'ERROR',
-    emptyParameters: [ '<text>' ],
     args: {
       command: { name: 'tsn dif', type: 'MergedArgument' },
       text: { type: 'BlankArgument', status: 'INCOMPLETE' }
     }
   });
 
   helpers.setInput('tsn di');
   helpers.pressTab();
   helpers.check({
     input:  'tsn dif ',
+    hints:          '<text>',
     markup: 'VVVVVVVV',
     cursor: 8,
-    directTabText: '',
-    arrowTabText: '',
     status: 'ERROR',
-    emptyParameters: [ '<text>' ],
     args: {
       command: { name: 'tsn dif', type: 'Argument' },
-      text: { type: 'Argument', status: 'INCOMPLETE' }
+      text: { type: 'BlankArgument', status: 'INCOMPLETE' }
     }
   });
 
   // The above 2 tests take different routes to 'tsn dif '. The results should
   // be similar. The difference is in args.command.type.
 
   helpers.setInput('tsg -');
   helpers.check({
     input:  'tsg -',
+    hints:       '-txt1 <solo> [options]',
     markup: 'VVVVI',
     cursor: 5,
-    directTabText: '-txt1',
-    arrowTabText: '',
     status: 'ERROR',
-    emptyParameters: [ ],
     args: {
       solo: { value: undefined, status: 'INCOMPLETE' },
       txt1: { value: undefined, status: 'VALID' },
-      bool: { value: undefined, status: 'VALID' },
+      bool: { value: false, status: 'VALID' },
       txt2: { value: undefined, status: 'VALID' },
       num: { value: undefined, status: 'VALID' }
     }
   });
 
   helpers.pressTab();
   helpers.check({
     input:  'tsg --txt1 ',
+    hints:             '<string> <solo> [options]',
     markup: 'VVVVIIIIIIV',
     cursor: 11,
-    directTabText: '',
-    arrowTabText: '',
     status: 'ERROR',
-    emptyParameters: [ ], // Bug 770830: '<txt1>', ' <solo>'
     args: {
       solo: { value: undefined, status: 'INCOMPLETE' },
       txt1: { value: undefined, status: 'INCOMPLETE' },
-      bool: { value: undefined, status: 'VALID' },
+      bool: { value: false, status: 'VALID' },
       txt2: { value: undefined, status: 'VALID' },
       num: { value: undefined, status: 'VALID' }
     }
   });
 
   helpers.setInput('tsg --txt1 fred');
   helpers.check({
     input:  'tsg --txt1 fred',
+    hints:                 ' <solo> [options]',
     markup: 'VVVVVVVVVVVVVVV',
-    directTabText: '',
-    arrowTabText: '',
     status: 'ERROR',
-    emptyParameters: [ ], // Bug 770830: ' <solo>'
     args: {
       solo: { value: undefined, status: 'INCOMPLETE' },
       txt1: { value: 'fred', status: 'VALID' },
-      bool: { value: undefined, status: 'VALID' },
+      bool: { value: false, status: 'VALID' },
       txt2: { value: undefined, status: 'VALID' },
       num: { value: undefined, status: 'VALID' }
     }
   });
 
   helpers.setInput('tscook key value --path path --');
   helpers.check({
     input:  'tscook key value --path path --',
+    hints:                                 'domain [options]',
     markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVII',
-    directTabText: 'domain',
-    arrowTabText: '',
     status: 'ERROR',
-    emptyParameters: [ ],
     args: {
       key: { value: 'key', status: 'VALID' },
       value: { value: 'value', status: 'VALID' },
       path: { value: 'path', status: 'VALID' },
       domain: { value: undefined, status: 'VALID' },
       secure: { value: false, status: 'VALID' }
     }
   });
 
   helpers.setInput('tscook key value --path path --domain domain --');
   helpers.check({
     input:  'tscook key value --path path --domain domain --',
+    hints:                                                 'secure [options]',
     markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVII',
-    directTabText: 'secure',
-    arrowTabText: '',
     status: 'ERROR',
-    emptyParameters: [ ],
     args: {
       key: { value: 'key', status: 'VALID' },
       value: { value: 'value', status: 'VALID' },
       path: { value: 'path', status: 'VALID' },
       domain: { value: 'domain', status: 'VALID' },
       secure: { value: false, status: 'VALID' }
     }
   });
 };
 
 exports.testCase = function(options) {
   helpers.setInput('tsg AA');
   helpers.check({
     input:  'tsg AA',
+    hints:        ' [options] -> aaa',
     markup: 'VVVVII',
-    directTabText: '',
-    arrowTabText: 'aaa',
     status: 'ERROR',
-    emptyParameters: [ ],
     args: {
       solo: { value: undefined, text: 'AA', status: 'INCOMPLETE' },
       txt1: { value: undefined, status: 'VALID' },
-      bool: { value: undefined, status: 'VALID' },
+      bool: { value: false, status: 'VALID' },
       txt2: { value: undefined, status: 'VALID' },
       num: { value: undefined, status: 'VALID' }
     }
   });
 };
 
 exports.testIncomplete = function(options) {
   var requisition = options.display.requisition;
@@ -3005,16 +3334,129 @@ exports.testIncomplete = function(option
   });
   test.is(requisition._unassigned[0], requisition.getAssignmentAt(5),
           'unassigned -');
   test.is(requisition._unassigned.length, 1, 'single unassigned - tsg -');
   test.is(requisition._unassigned[0].param.type.isIncompleteName, true,
           'unassigned.isIncompleteName: tsg -');
 };
 
+exports.testHidden = function(options) {
+  helpers.setInput('tshidde');
+  helpers.check({
+    input:  'tshidde',
+    markup: 'EEEEEEE',
+    status: 'ERROR',
+    hints:  '',
+  });
+
+  helpers.setInput('tshidden');
+  helpers.check({
+    input:  'tshidden',
+    hints:          ' [options]',
+    markup: 'VVVVVVVV',
+    status: 'VALID',
+    args: {
+      visible: { value: undefined, status: 'VALID' },
+      invisiblestring: { value: undefined, status: 'VALID' },
+      invisibleboolean: { value: false, status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tshidden --vis');
+  helpers.check({
+    input:  'tshidden --vis',
+    hints:                'ible [options]',
+    markup: 'VVVVVVVVVIIIII',
+    status: 'ERROR',
+    args: {
+      visible: { value: undefined, status: 'VALID' },
+      invisiblestring: { value: undefined, status: 'VALID' },
+      invisibleboolean: { value: false, status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tshidden --invisiblestrin');
+  helpers.check({
+    input:  'tshidden --invisiblestrin',
+    hints:                           ' [options]',
+    markup: 'VVVVVVVVVEEEEEEEEEEEEEEEE',
+    status: 'ERROR',
+    args: {
+      visible: { value: undefined, status: 'VALID' },
+      invisiblestring: { value: undefined, status: 'VALID' },
+      invisibleboolean: { value: false, status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tshidden --invisiblestring');
+  helpers.check({
+    input:  'tshidden --invisiblestring',
+    hints:                            ' <string> [options]',
+    markup: 'VVVVVVVVVIIIIIIIIIIIIIIIII',
+    status: 'ERROR',
+    args: {
+      visible: { value: undefined, status: 'VALID' },
+      invisiblestring: { value: undefined, status: 'INCOMPLETE' },
+      invisibleboolean: { value: false, status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tshidden --invisiblestring x');
+  helpers.check({
+    input:  'tshidden --invisiblestring x',
+    hints:                              ' [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+    status: 'VALID',
+    args: {
+      visible: { value: undefined, status: 'VALID' },
+      invisiblestring: { value: 'x', status: 'VALID' },
+      invisibleboolean: { value: false, status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tshidden --invisibleboolea');
+  helpers.check({
+    input:  'tshidden --invisibleboolea',
+    hints:                            ' [options]',
+    markup: 'VVVVVVVVVEEEEEEEEEEEEEEEEE',
+    status: 'ERROR',
+    args: {
+      visible: { value: undefined, status: 'VALID' },
+      invisiblestring: { value: undefined, status: 'VALID' },
+      invisibleboolean: { value: false, status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tshidden --invisibleboolean');
+  helpers.check({
+    input:  'tshidden --invisibleboolean',
+    hints:                             ' [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVV',
+    status: 'VALID',
+    args: {
+      visible: { value: undefined, status: 'VALID' },
+      invisiblestring: { value: undefined, status: 'VALID' },
+      invisibleboolean: { value: true, status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tshidden --visible xxx');
+  helpers.check({
+    input:  'tshidden --visible xxx',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVV',
+    status: 'VALID',
+    hints:  '',
+    args: {
+      visible: { value: 'xxx', status: 'VALID' },
+      invisiblestring: { value: undefined, status: 'VALID' },
+      invisibleboolean: { value: false, status: 'VALID' }
+    }
+  });
+};
 
 });
 /*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
@@ -3028,34 +3470,44 @@ exports.testIncomplete = function(option
  * limitations under the License.
  */
 
 define('gclitest/testIntro', ['require', 'exports', 'module' , 'gclitest/helpers', 'test/assert'], function(require, exports, module) {
 
   var helpers = require('gclitest/helpers');
   var test = require('test/assert');
 
+  exports.setup = function(options) {
+    helpers.setup(options);
+  };
+
+  exports.shutdown = function(options) {
+    helpers.shutdown(options);
+  };
+
   exports.testIntroStatus = function(options) {
     if (options.isFirefox) {
       test.log('Skipping testIntroStatus in Firefox.');
       return;
     }
 
-    helpers.status(options, {
+    helpers.setInput('intro');
+    helpers.check({
       typed:  'intro',
       markup: 'VVVVV',
       status: 'VALID',
-      emptyParameters: [ ]
+      hints: ''
     });
 
-    helpers.status(options, {
+    helpers.setInput('intro foo');
+    helpers.check({
       typed:  'intro foo',
       markup: 'VVVVVVEEE',
       status: 'ERROR',
-      emptyParameters: [ ]
+      hints: ''
     });
   };
 
   exports.testIntroExec = function(options) {
     if (options.isFirefox) {
       test.log('Skipping testIntroExec in Firefox.');
       return;
     }
@@ -3325,21 +3777,21 @@ function check(initial, action, after, c
   }
   var assignment = requisition.getAssignmentAt(cursor);
   switch (action) {
     case COMPLETES_TO:
       requisition.complete({ start: cursor, end: cursor }, choice);
       break;
 
     case KEY_UPS_TO:
-      assignment.increment();
+      requisition.increment(assignment);
       break;
 
     case KEY_DOWNS_TO:
-      assignment.decrement();
+      requisition.decrement(assignment);
       break;
   }
 
   test.is(after, requisition.toString(),
           initial + ' + ' + action + ' -> ' + after);
 
   if (expectedCursor != null) {
     if (inputter) {
@@ -3444,16 +3896,336 @@ exports.testIncrDecr = function() {
   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/testMenu', ['require', 'exports', 'module' , 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
+
+
+var helpers = require('gclitest/helpers');
+var mockCommands = require('gclitest/mockCommands');
+
+
+exports.setup = function(options) {
+  mockCommands.setup();
+  helpers.setup(options);
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+  helpers.shutdown(options);
+};
+
+exports.testOptions = function(options) {
+  helpers.setInput('tslong');
+  helpers.check({
+    input:  'tslong',
+    markup: 'VVVVVV',
+    status: 'ERROR',
+    hints: ' <msg> [options]',
+    args: {
+      msg: { value: undefined, status: 'INCOMPLETE' },
+      num: { value: undefined, status: 'VALID' },
+      sel: { value: undefined, status: 'VALID' },
+      bool: { value: false, status: 'VALID' },
+      bool2: { value: false, status: 'VALID' },
+      sel2: { value: undefined, status: 'VALID' },
+      num2: { value: undefined, status: 'VALID' }
+    }
+  });
+};
+
+
+});
+
+/*
+ * 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/testNode', ['require', 'exports', 'module' , 'test/assert', 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) {
+
+
+var test = require('test/assert');
+var helpers = require('gclitest/helpers');
+var mockCommands = require('gclitest/mockCommands');
+
+
+exports.setup = function(options) {
+  mockCommands.setup();
+  helpers.setup(options);
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+  helpers.shutdown(options);
+};
+
+exports.testNode = function(options) {
+  var requisition = options.display.requisition;
+
+  helpers.setInput('tse ');
+  helpers.check({
+    input:  'tse ',
+    hints:      '<node> [options]',
+    markup: 'VVVV',
+    cursor: 4,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: { status: 'INCOMPLETE', message: '' },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse :');
+  helpers.check({
+    input:  'tse :',
+    hints:       ' [options]',
+    markup: 'VVVVE',
+    cursor: 5,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        arg: ' :',
+        status: 'ERROR',
+        message: 'Syntax error in CSS query'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse :root');
+  helpers.check({
+    input:  'tse :root',
+    hints:           ' [options]',
+    markup: 'VVVVVVVVV',
+    cursor: 9,
+    current: 'node',
+    status: 'VALID',
+    args: {
+      command: { name: 'tse' },
+      node: { arg: ' :root', status: 'VALID' },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse :root ');
+  helpers.check({
+    input:  'tse :root ',
+    hints:            '[options]',
+    markup: 'VVVVVVVVVV',
+    cursor: 10,
+    current: 'node',
+    status: 'VALID',
+    args: {
+      command: { name: 'tse' },
+      node: { arg: ' :root ', status: 'VALID' },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+  test.is(requisition.getAssignment('node').value.tagName,
+          'HTML',
+          'root id');
+
+  helpers.setInput('tse #gcli-nomatch');
+  helpers.check({
+    input:  'tse #gcli-nomatch',
+    hints:                   ' [options]',
+    markup: 'VVVVIIIIIIIIIIIII',
+    cursor: 17,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        value: undefined,
+        arg: ' #gcli-nomatch',
+        status: 'INCOMPLETE',
+        message: 'No matches'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse #');
+  helpers.check({
+    input:  'tse #',
+    hints:       ' [options]',
+    markup: 'VVVVE',
+    cursor: 5,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        value: undefined,
+        arg: ' #',
+        status: 'ERROR',
+        message: 'Syntax error in CSS query'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse .');
+  helpers.check({
+    input:  'tse .',
+    hints:       ' [options]',
+    markup: 'VVVVE',
+    cursor: 5,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        value: undefined,
+        arg: ' .',
+        status: 'ERROR',
+        message: 'Syntax error in CSS query'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+
+  helpers.setInput('tse *');
+  helpers.check({
+    input:  'tse *',
+    hints:       ' [options]',
+    markup: 'VVVVE',
+    cursor: 5,
+    current: 'node',
+    status: 'ERROR',
+    args: {
+      command: { name: 'tse' },
+      node: {
+        value: undefined,
+        arg: ' *',
+        status: 'ERROR',
+        // message: 'Too many matches (128)'
+      },
+      nodes: { status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+};
+
+exports.testNodes = function(options) {
+  var requisition = options.display.requisition;
+
+  helpers.setInput('tse :root --nodes *');
+  helpers.check({
+    input:  'tse :root --nodes *',
+    hints:                       ' [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVV',
+    current: 'nodes',
+    status: 'VALID',
+    args: {
+      command: { name: 'tse' },
+      node: { arg: ' :root', status: 'VALID' },
+      nodes: { arg: ' --nodes *', status: 'VALID' },
+      nodes2: { status: 'VALID' }
+    }
+  });
+  test.is(requisition.getAssignment('node').value.tagName,
+          'HTML',
+          '#gcli-input id');
+
+  helpers.setInput('tse :root --nodes2 div');
+  helpers.check({
+    input:  'tse :root --nodes2 div',
+    hints:                       ' [options]',
+    markup: 'VVVVVVVVVVVVVVVVVVVVVV',
+    cursor: 22,
+    current: 'nodes2',
+    status: 'VALID',
+    args: {
+      command: { name: 'tse' },
+      node: { arg: ' :root', status: 'VALID' },
+      nodes: { status: 'VALID' },
+      nodes2: { arg: ' --nodes2 div', status: 'VALID' }
+    }
+  });
+  test.is(requisition.getAssignment('node').value.tagName,
+          'HTML',
+          'root id');
+
+  helpers.setInput('tse --nodes ffff');
+  helpers.check({
+    input:  'tse --nodes ffff',
+    hints:                  ' <node> [options]',
+    markup: 'VVVVIIIIIIIVIIII',
+    cursor: 16,
+    current: 'nodes',
+    status: 'ERROR',
+    outputState: 'false:default',
+    tooltipState: 'true:isError',
+    args: {
+      command: { name: 'tse' },
+      node: { value: undefined, arg: '', status: 'INCOMPLETE', message: '' },
+      nodes: { value: undefined, arg: ' --nodes ffff', status: 'INCOMPLETE', message: 'No matches' },
+      nodes2: { arg: '', status: 'VALID', message: '' },
+    }
+  });
+  /*
+  test.is(requisition.getAssignment('nodes2').value.constructor.name,
+          'NodeList',
+          '#gcli-input id');
+  */
+
+  helpers.setInput('tse --nodes2 ffff');
+  helpers.check({
+    input:  'tse --nodes2 ffff',
+    hints:                   ' <node> [options]',
+    markup: 'VVVVVVVVVVVVVVVVV',
+    cursor: 17,
+    current: 'nodes2',
+    status: 'ERROR',
+    outputState: 'false:default',
+    tooltipState: 'false:default',
+    args: {
+      command: { name: 'tse' },
+      node: { value: undefined, arg: '', status: 'INCOMPLETE', message: '' },
+      nodes: { arg: '', status: 'VALID', message: '' },
+      nodes2: { arg: ' --nodes2 ffff', status: 'VALID', message: '' },
+    }
+  });
+  /*
+  test.is(requisition.getAssignment('nodes').value.constructor.name,
+          'NodeList',
+          '#gcli-input id');
+  test.is(requisition.getAssignment('nodes2').value.constructor.name,
+          'NodeList',
+          '#gcli-input id');
+  */
+};
+
+
+});
+/*
  * Copyright 2012, Mozilla Foundation and contributors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  * http://www.apache.org/licenses/LICENSE-2.0
  *
@@ -3469,145 +4241,156 @@ define('gclitest/testPref', ['require', 
 
 var pref = require('gcli/commands/pref');
 var helpers = require('gclitest/helpers');
 var mockSettings = require('gclitest/mockSettings');
 var test = require('test/assert');
 
 
 exports.setup = function(options) {
+  helpers.setup(options);
+
   if (!options.isFirefox) {
     mockSettings.setup();
   }
   else {
     test.log('Skipping testPref in Firefox.');
   }
 };
 
 exports.shutdown = function(options) {
+  helpers.shutdown(options);
+
   if (!options.isFirefox) {
     mockSettings.shutdown();
   }
 };
 
 exports.testPrefShowStatus = function(options) {
   if (options.isFirefox) {
     test.log('Skipping testPrefShowStatus in Firefox.');
     return;
   }
 
-  helpers.status(options, {
+  helpers.setInput('pref s');
+  helpers.check({
     typed:  'pref s',
+    hints:        'et',
     markup: 'IIIIVI',
-    status: 'ERROR',
-    directTabText: 'et'
+    status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show');
+  helpers.check({
     typed:  'pref show',
+    hints:           ' <setting>',
     markup: 'VVVVVVVVV',
-    status: 'ERROR',
-    emptyParameters: [ ' <setting>' ]
+    status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show ');
+  helpers.check({
     typed:  'pref show ',
+    hints:            'allowSet',
     markup: 'VVVVVVVVVV',
-    status: 'ERROR',
-    emptyParameters: [ ]
+    status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show tempTBo');
+  helpers.check({
     typed:  'pref show tempTBo',
+    hints:                   'ol',
     markup: 'VVVVVVVVVVIIIIIII',
-    directTabText: 'ol',
-    status: 'ERROR',
-    emptyParameters: [ ]
+    status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show tempTBool');
+  helpers.check({
     typed:  'pref show tempTBool',
     markup: 'VVVVVVVVVVVVVVVVVVV',
-    directTabText: '',
     status: 'VALID',
-    emptyParameters: [ ]
+    hints:  ''
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show tempTBool 4');
+  helpers.check({
     typed:  'pref show tempTBool 4',
     markup: 'VVVVVVVVVVVVVVVVVVVVE',
-    directTabText: '',
     status: 'ERROR',
-    emptyParameters: [ ]
+    hints:  ''
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref show tempNumber 4');
+  helpers.check({
     typed:  'pref show tempNumber 4',
     markup: 'VVVVVVVVVVVVVVVVVVVVVE',
-    directTabText: '',
     status: 'ERROR',
-    emptyParameters: [ ]
+    hints:  ''
   });
 };
 
 exports.testPrefSetStatus = function(options) {
   if (options.isFirefox) {
     test.log('Skipping testPrefSetStatus in Firefox.');
     return;
   }
 
-  helpers.status(options, {
+  helpers.setInput('pref s');
+  helpers.check({
     typed:  'pref s',
+    hints:        'et',
     markup: 'IIIIVI',
     status: 'ERROR',
-    directTabText: 'et'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set');
+  helpers.check({
     typed:  'pref set',
+    hints:          ' <setting> <value>',
     markup: 'VVVVVVVV',
-    status: 'ERROR',
-    emptyParameters: [ ' <setting>', ' <value>' ]
+    status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref xxx');
+  helpers.check({
     typed:  'pref xxx',
     markup: 'EEEEVEEE',
     status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set ');
+  helpers.check({
     typed:  'pref set ',
+    hints:           'allowSet <value>',
     markup: 'VVVVVVVVV',
-    status: 'ERROR',
-    emptyParameters: [ ' <value>' ]
+    status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set tempTBo');
+  helpers.check({
     typed:  'pref set tempTBo',
+    hints:                  'ol <value>',
     markup: 'VVVVVVVVVIIIIIII',
-    directTabText: 'ol',
-    status: 'ERROR',
-    emptyParameters: [ ' <value>' ]
+    status: 'ERROR'
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set tempTBool 4');
+  helpers.check({
     typed:  'pref set tempTBool 4',
     markup: 'VVVVVVVVVVVVVVVVVVVE',
-    directTabText: '',
     status: 'ERROR',
-    emptyParameters: [ ]
+    hints: ''
   });
 
-  helpers.status(options, {
+  helpers.setInput('pref set tempNumber 4');
+  helpers.check({
     typed:  'pref set tempNumber 4',
     markup: 'VVVVVVVVVVVVVVVVVVVVV',
-    directTabText: '',
     status: 'VALID',
-    emptyParameters: [ ]
+    hints: ''
   });
 };
 
 exports.testPrefExec = function(options) {
   if (options.isFirefox) {
     test.log('Skipping testPrefExec in Firefox.');
     return;
   }
@@ -4801,16 +5584,20 @@ exports.testDefault = function(options) 
     // boolean and array types are exempt from needing undefined blank values
     if (type.name === 'boolean') {
       test.is(blank, false, 'blank boolean is false');
     }
     else if (type.name === 'array') {
       test.ok(Array.isArray(blank), 'blank array is array');
       test.is(blank.length, 0, 'blank array is empty');
     }
+    else if (type.name === 'nodelist') {
+      test.ok(typeof blank.item, 'function', 'blank.item is function');
+      test.is(blank.length, 0, 'blank nodelist is empty');
+    }
     else {
       test.is(blank, undefined, 'default defined for ' + type.name);
     }
   });
 };
 
 exports.testNullDefault = function(options) {
   forEachType({ defaultValue: null }, function(type) {
@@ -5554,23 +6341,26 @@ let testModuleNames = [
   'test/assert',
   'test/status',
   'gclitest/testCanon',
   'gclitest/helpers',
   'gclitest/testCli',
   'gclitest/mockCommands',
   'gclitest/testCompletion',
   'gclitest/testExec',
+  'gclitest/testFocus',
   'gclitest/testHelp',
   'gclitest/testHistory',
   'gclitest/testInputter',
   'gclitest/testIncomplete',
   'gclitest/testIntro',
   'gclitest/testJs',
   'gclitest/testKeyboard',
+  'gclitest/testMenu',
+  'gclitest/testNode',
   'gclitest/testPref',
   'gclitest/mockSettings',
   'gclitest/testRequire',
   'gclitest/requirable',
   'gclitest/testResource',
   'gclitest/testScratchpad',
   'gclitest/testSettings',
   'gclitest/testSpell',
--- a/browser/devtools/commandline/test/head.js
+++ b/browser/devtools/commandline/test/head.js
@@ -6,16 +6,20 @@ const TEST_BASE_HTTP = "http://example.c
 const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/commandline/test/";
 
 let console = (function() {
   let tempScope = {};
   Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
   return tempScope.console;
 })();
 
+// Import the GCLI test helper
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "/helpers.js", this);
+
 /**
  * Open a new tab at a URL and call a callback on load
  */
 function addTab(aURL, aCallback)
 {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = gBrowser.addTab();
@@ -34,523 +38,8 @@ function addTab(aURL, aCallback)
 
 registerCleanupFunction(function tearDown() {
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 
   console = undefined;
 });
-
-/**
- * Various functions for testing DeveloperToolbar.
- * Parts of this code exist in:
- * - browser/devtools/commandline/test/head.js
- * - browser/devtools/shared/test/head.js
- */
-let DeveloperToolbarTest = { };
-
-/**
- * Paranoid DeveloperToolbar.show();
- */
-DeveloperToolbarTest.show = function DTT_show(aCallback) {
-  if (DeveloperToolbar.visible) {
-    ok(false, "DeveloperToolbar.visible at start of openDeveloperToolbar");
-  }
-  else {
-    DeveloperToolbar.show(true, aCallback);
-  }
-};
-
-/**
- * Paranoid DeveloperToolbar.hide();
- */
-DeveloperToolbarTest.hide = function DTT_hide() {
-  if (!DeveloperToolbar.visible) {
-    ok(false, "!DeveloperToolbar.visible at start of closeDeveloperToolbar");
-  }
-  else {
-    DeveloperToolbar.display.inputter.setInput("");
-    DeveloperToolbar.hide();
-  }
-};
-
-/**
- * check() is the new status. Similar API except that it doesn't attempt to
- * alter the display/requisition at all, and it makes extra checks.
- * Test inputs
- *   typed: The text to type at the input
- * Available checks:
- *   input: The text displayed in the input field
- *   cursor: The position of the start of the cursor
- *   status: One of "VALID", "ERROR", "INCOMPLETE"
- *   emptyParameters: Array of parameters still to type. e.g. [ "<message>" ]
- *   directTabText: Simple completion text
- *   arrowTabText: When the completion is not an extension (without arrow)
- *   markup: What state should the error markup be in. e.g. "VVVIIIEEE"
- *   args: Maps of checks to make against the arguments:
- *     value: i.e. assignment.value (which ignores defaultValue)
- *     type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned
- *           Care should be taken with this since it's something of an
- *           implementation detail
- *     arg: The toString value of the argument
- *     status: i.e. assignment.getStatus
- *     message: i.e. assignment.getMessage
- *     name: For commands - checks assignment.value.name
- */
-DeveloperToolbarTest.checkInputStatus = function DTT_checkInputStatus(checks) {
-  if (!checks.emptyParameters) {
-    checks.emptyParameters = [];
-  }
-  if (!checks.directTabText) {
-    checks.directTabText = '';
-  }
-  if (!checks.arrowTabText) {
-    checks.arrowTabText = '';
-  }
-
-  var display = DeveloperToolbar.display;
-
-  if (checks.typed) {
-    display.inputter.setInput(checks.typed);
-  }
-  else {
-    ok(false, "Missing typed for " + JSON.stringify(checks));
-    return;
-  }
-
-  if (checks.cursor) {
-    display.inputter.setCursor(checks.cursor)
-  }
-
-  var cursor = checks.cursor ? checks.cursor.start : checks.typed.length;
-
-  var requisition = display.requisition;
-  var completer = display.completer;
-  var actual = completer._getCompleterTemplateData();
-
-  /*
-  if (checks.input) {
-    is(display.inputter.element.value,
-            checks.input,
-            'input');
-  }
-
-  if (checks.cursor) {
-    is(display.inputter.element.selectionStart,
-            checks.cursor,
-            'cursor');
-  }
-  */
-
-  if (checks.status) {
-    is(requisition.getStatus().toString(),
-            checks.status,
-            'status');
-  }
-
-  if (checks.markup) {
-    var statusMarkup = requisition.getInputStatusMarkup(cursor);
-    var actualMarkup = statusMarkup.map(function(s) {
-      return Array(s.string.length + 1).join(s.status.toString()[0]);
-    }).join('');
-
-    is(checks.markup,
-            actualMarkup,
-            'markup');
-  }
-
-  if (checks.emptyParameters) {
-    var actualParams = actual.emptyParameters;
-    is(actualParams.length,
-            checks.emptyParameters.length,
-            'emptyParameters.length');
-
-    if (actualParams.length === checks.emptyParameters.length) {
-      for (var i = 0; i < actualParams.length; i++) {
-        is(actualParams[i].replace(/\u00a0/g, ' '),
-                checks.emptyParameters[i],
-                'emptyParameters[' + i + ']');
-      }
-    }
-  }
-
-  if (checks.directTabText) {
-    is(actual.directTabText,
-            checks.directTabText,
-            'directTabText');
-  }
-
-  if (checks.arrowTabText) {
-    is(actual.arrowTabText,
-            ' \u00a0\u21E5 ' + checks.arrowTabText,
-            'arrowTabText');
-  }
-
-  if (checks.args) {
-    Object.keys(checks.args).forEach(function(paramName) {
-      var check = checks.args[paramName];
-
-      var assignment;
-      if (paramName === 'command') {
-        assignment = requisition.commandAssignment;
-      }
-      else {
-        assignment = requisition.getAssignment(paramName);
-      }
-
-      if (assignment == null) {
-        ok(false, 'Unknown parameter: ' + paramName);
-        return;
-      }
-
-      if (check.value) {
-        is(assignment.value,
-                check.value,
-                'checkStatus value for ' + paramName);
-      }
-
-      if (check.name) {
-        is(assignment.value.name,
-                check.name,
-                'checkStatus name for ' + paramName);
-      }
-
-      if (check.type) {
-        is(assignment.arg.type,
-                check.type,
-                'checkStatus type for ' + paramName);
-      }
-
-      if (check.arg) {
-        is(assignment.arg.toString(),
-                check.arg,
-                'checkStatus arg for ' + paramName);
-      }
-
-      if (check.status) {
-        is(assignment.getStatus().toString(),
-                check.status,
-                'checkStatus status for ' + paramName);
-      }
-
-      if (check.message) {
-        is(assignment.getMessage(),
-                check.message,
-                'checkStatus message for ' + paramName);
-      }
-    });
-  }
-};
-
-/**
- * Execute a command:
- *
- * DeveloperToolbarTest.exec({
- *   // Test inputs
- *   typed: "echo hi",        // Optional, uses existing if undefined
- *
- *   // Thing to check
- *   args: { message: "hi" }, // Check that the args were understood properly
- *   outputMatch: /^hi$/,     // RegExp to test against textContent of output
- *                            // (can also be array of RegExps)
- *   blankOutput: true,       // Special checks when there is no output
- * });
- */
-DeveloperToolbarTest.exec = function DTT_exec(tests) {
-  tests = tests || {};
-
-  if (tests.typed) {
-    DeveloperToolbar.display.inputter.setInput(tests.typed);
-  }
-
-  let typed = DeveloperToolbar.display.inputter.getInputState().typed;
-  let output = DeveloperToolbar.display.requisition.exec();
-
-  is(typed, output.typed, 'output.command for: ' + typed);
-
-  if (tests.completed !== false) {
-    ok(output.completed, 'output.completed false for: ' + typed);
-  }
-  else {
-    // It is actually an error if we say something is async and it turns
-    // out not to be? For now we're saying 'no'
-    // ok(!output.completed, 'output.completed true for: ' + typed);
-  }
-
-  if (tests.args != null) {
-    is(Object.keys(tests.args).length, Object.keys(output.args).length,
-       'arg count for ' + typed);
-
-    Object.keys(output.args).forEach(function(arg) {
-      let expectedArg = tests.args[arg];
-      let actualArg = output.args[arg];
-
-      if (typeof expectedArg === 'function') {
-        ok(expectedArg(actualArg), 'failed test func. ' + typed + '/' + arg);
-      }
-      else {
-        if (Array.isArray(expectedArg)) {
-          if (!Array.isArray(actualArg)) {
-            ok(false, 'actual is not an array. ' + typed + '/' + arg);
-            return;
-          }
-
-          is(expectedArg.length, actualArg.length,
-                  'array length: ' + typed + '/' + arg);
-          for (let i = 0; i < expectedArg.length; i++) {
-            is(expectedArg[i], actualArg[i],
-                    'member: "' + typed + '/' + arg + '/' + i);
-          }
-        }
-        else {
-          is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg);
-        }
-      }
-    });
-  }
-
-  let displayed = DeveloperToolbar.outputPanel._div.textContent;
-
-  if (tests.outputMatch) {
-    function doTest(match, against) {
-      if (!match.test(against)) {
-        ok(false, "html output for " + typed + " against " + match.source +
-                " (textContent sent to info)");
-        info("Actual textContent");
-        info(against);
-      }
-    }
-    if (Array.isArray(tests.outputMatch)) {
-      tests.outputMatch.forEach(function(match) {
-        doTest(match, displayed);
-      });
-    }
-    else {
-      doTest(tests.outputMatch, displayed);
-    }
-  }
-
-  if (tests.blankOutput != null) {
-    if (!/^$/.test(displayed)) {
-      ok(false, "html output for " + typed + " (textContent sent to info)");
-      info("Actual textContent");
-      info(displayed);
-    }
-  }
-};
-
-/**
- * Quick wrapper around the things you need to do to run DeveloperToolbar
- * command tests:
- * - Set the pref 'devtools.toolbar.enabled' to true
- * - Add a tab pointing at |uri|
- * - Open the DeveloperToolbar
- * - Register a cleanup function to undo the above
- * - Run the tests
- *
- * @param uri The uri of a page to load. Can be 'about:blank' or 'data:...'
- * @param testFunc A function containing the tests to run. This should
- * arrange for 'finish()' to be called on completion.
- */
-DeveloperToolbarTest.test = function DTT_test(uri, testFunc) {
-  let menuItem = document.getElementById("menu_devToolbar");
-  let command = document.getElementById("Tools:DevToolbar");
-  let appMenuItem = document.getElementById("appmenu_devToolbar");
-
-  registerCleanupFunction(function() {
-    DeveloperToolbarTest.hide();
-
-    // a.k.a Services.prefs.clearUserPref("devtools.toolbar.enabled");
-    if (menuItem) {
-      menuItem.hidden = true;
-    }
-    if (command) {
-      command.setAttribute("disabled", "true");
-    }
-    if (appMenuItem) {
-      appMenuItem.hidden = true;
-    }
-
-    // leakHunt({ DeveloperToolbar: DeveloperToolbar });
-  });
-
-  // a.k.a: Services.prefs.setBoolPref("devtools.toolbar.enabled", true);
-  if (menuItem) {
-    menuItem.hidden = false;
-  }
-  if (command) {
-    command.removeAttribute("disabled");
-  }
-  if (appMenuItem) {
-    appMenuItem.hidden = false;
-  }
-
-  addTab(uri, function(browser, tab) {
-    DeveloperToolbarTest.show(function() {
-
-      try {
-        testFunc(browser, tab);
-      }
-      catch (ex) {
-        ok(false, "" + ex);
-        console.error(ex);
-        finish();
-        throw ex;
-      }
-    });
-  });
-};
-
-
-/**
- * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
- * Usage:
- * leakHunt({
- *   thing: thing,
- *   otherthing: otherthing
- * });
- */
-
-var noRecurse = [
-  /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/,
-  /^Window$/, /^Document$/,
-  /^XULDocument$/, /^XULElement$/,
-  /^DOMWindow$/, /^HTMLDocument$/, /^HTML.*Element$/
-];
-
-var hide = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/ ];
-
-function leakHunt(root, path, seen) {
-  path = path || [];
-  seen = seen || [];
-
-  try {
-    var output = leakHuntInner(root, path, seen);
-    output.forEach(function(line) {
-      dump(line + '\n');
-    });
-  }
-  catch (ex) {
-    dump(ex + '\n');
-  }
-}
-
-function leakHuntInner(root, path, seen) {
-  var prefix = new Array(path.length).join('  ');
-
-  var reply = [];
-  function log(msg) {
-    reply.push(msg);
-  }
-
-  var direct
-  try {
-    direct = Object.keys(root);
-  }
-  catch (ex) {
-    log(prefix + '  Error enumerating: ' + ex);
-    return reply;
-  }
-
-  for (var prop in root) {
-    var newPath = path.slice();
-    newPath.push(prop);
-    prefix = new Array(newPath.length).join('  ');
-
-    var data;
-    try {
-      data = root[prop];
-    }
-    catch (ex) {
-      log(prefix + prop + '  Error reading: ' + ex);
-      continue;
-    }
-
-    var recurse = true;
-    var message = getType(data);
-
-    if (matchesAnyPattern(message, hide)) {
-      continue;
-    }
-
-    if (message === 'function' && direct.indexOf(prop) == -1) {
-      continue;
-    }
-
-    if (message === 'string') {
-      var extra = data.length > 10 ? data.substring(0, 9) + '_' : data;
-      message += ' "' + extra.replace(/\n/g, "|") + '"';
-      recurse = false;
-    }
-    else if (matchesAnyPattern(message, noRecurse)) {
-      message += ' (no recurse)'
-      recurse = false;
-    }
-    else if (seen.indexOf(data) !== -1) {
-      message += ' (already seen)';
-      recurse = false;
-    }
-
-    if (recurse) {
-      seen.push(data);
-      var lines = leakHuntInner(data, newPath, seen);
-      if (lines.length == 0) {
-        if (message !== 'function') {
-          log(prefix + prop + ' = ' + message + ' { }');
-        }
-      }
-      else {
-        log(prefix + prop + ' = ' + message + ' {');
-        lines.forEach(function(line) {
-          reply.push(line);
-        });
-        log(prefix + '}');
-      }
-    }
-    else {
-      log(prefix + prop + ' = ' + message);
-    }
-  }
-
-  return reply;
-}
-
-function matchesAnyPattern(str, patterns) {
-  var match = false;
-  patterns.forEach(function(pattern) {
-    if (str.match(pattern)) {
-      match = true;
-    }
-  });
-  return match;
-}
-
-function getType(data) {
-  if (data === null) {
-    return 'null';
-  }
-  if (data === undefined) {
-    return 'undefined';
-  }
-
-  var type = typeof data;
-  if (type === 'object' || type === 'Object') {
-    type = getCtorName(data);
-  }
-
-  return type;
-}
-
-function getCtorName(aObj) {
-  try {
-    if (aObj.constructor && aObj.constructor.name) {
-      return aObj.constructor.name;
-    }
-  }
-  catch (ex) {
-    return 'UnknownObject';
-  }
-
-  // If that fails, use Objects toString which sometimes gives something
-  // better than 'Object', and at least defaults to Object if nothing better
-  return Object.prototype.toString.call(aObj).slice(8, -1);
-}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/helpers.js
@@ -0,0 +1,881 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+/*
+ *
+ *  DO NOT ALTER THIS FILE WITHOUT KEEPING IT IN SYNC WITH THE OTHER COPIES
+ *  OF THIS FILE.
+ *
+ *  UNAUTHORIZED ALTERATION WILL RESULT IN THE ALTEREE BEING SENT TO SIT ON
+ *  THE NAUGHTY STEP.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *  FOR A LONG TIME.
+ *
+ */
+
+
+/*
+ * Use as a JSM
+ * ------------
+ * helpers._createDebugCheck() and maybe other functions in this file can be
+ * useful at runtime, so it is possible to use helpers.js as a JSM.
+ * Copy commandline/test/helpers.js to shared/helpers.jsm, and then add to
+ * DeveloperToolbar.jsm the following:
+ *
+ * XPCOMUtils.defineLazyModuleGetter(this, "helpers",
+ *                                 "resource:///modules/devtools/helpers.jsm");
+ *
+ * At the bottom of DeveloperToolbar.prototype._onload add this:
+ *
+ * var options = { display: this.display };
+ * this._input.onkeypress = function(ev) {
+ *   helpers.setup(options);
+ *   dump(helpers._createDebugCheck() + '\n\n');
+ * };
+ *
+ * Now GCLI will emit output on every keypress that both explains the state
+ * of GCLI and can be run as a test case.
+ */
+
+var EXPORTED_SYMBOLS = [ 'helpers' ];
+
+var test = { };
+
+/**
+ * Various functions for testing DeveloperToolbar.
+ * Parts of this code exist in:
+ * - browser/devtools/commandline/test/head.js
+ * - browser/devtools/shared/test/head.js
+ */
+let DeveloperToolbarTest = { };
+
+/**
+ * Paranoid DeveloperToolbar.show();
+ */
+DeveloperToolbarTest.show = function DTT_show(aCallback) {
+  if (DeveloperToolbar.visible) {
+    ok(false, "DeveloperToolbar.visible at start of openDeveloperToolbar");
+  }
+  else {
+    DeveloperToolbar.show(true, aCallback);
+  }
+};
+
+/**
+ * Paranoid DeveloperToolbar.hide();
+ */
+DeveloperToolbarTest.hide = function DTT_hide() {
+  if (!DeveloperToolbar.visible) {
+    ok(false, "!DeveloperToolbar.visible at start of closeDeveloperToolbar");
+  }
+  else {
+    DeveloperToolbar.display.inputter.setInput("");
+    DeveloperToolbar.hide();
+  }
+};
+
+/**
+ * check() is the new status. Similar API except that it doesn't attempt to
+ * alter the display/requisition at all, and it makes extra checks.
+ * Test inputs
+ *   typed: The text to type at the input
+ * Available checks:
+ *   input: The text displayed in the input field
+ *   cursor: The position of the start of the cursor
+ *   status: One of "VALID", "ERROR", "INCOMPLETE"
+ *   emptyParameters: Array of parameters still to type. e.g. [ "<message>" ]
+ *   directTabText: Simple completion text
+ *   arrowTabText: When the completion is not an extension (without arrow)
+ *   markup: What state should the error markup be in. e.g. "VVVIIIEEE"
+ *   args: Maps of checks to make against the arguments:
+ *     value: i.e. assignment.value (which ignores defaultValue)
+ *     type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned
+ *           Care should be taken with this since it's something of an
+ *           implementation detail
+ *     arg: The toString value of the argument
+ *     status: i.e. assignment.getStatus
+ *     message: i.e. assignment.getMessage
+ *     name: For commands - checks assignment.value.name
+ */
+DeveloperToolbarTest.checkInputStatus = function DTT_checkInputStatus(checks) {
+  if (!checks.emptyParameters) {
+    checks.emptyParameters = [];
+  }
+  if (!checks.directTabText) {
+    checks.directTabText = '';
+  }
+  if (!checks.arrowTabText) {
+    checks.arrowTabText = '';
+  }
+
+  var display = DeveloperToolbar.display;
+
+  if (checks.typed) {
+    info('Starting tests for ' + checks.typed);
+    display.inputter.setInput(checks.typed);
+  }
+  else {
+    ok(false, "Missing typed for " + JSON.stringify(checks));
+    return;
+  }
+
+  if (checks.cursor) {
+    display.inputter.setCursor(checks.cursor)
+  }
+
+  var cursor = checks.cursor ? checks.cursor.start : checks.typed.length;
+
+  var requisition = display.requisition;
+  var completer = display.completer;
+  var actual = completer._getCompleterTemplateData();
+
+  /*
+  if (checks.input) {
+    is(display.inputter.element.value,
+            checks.input,
+            'input');
+  }
+
+  if (checks.cursor) {
+    is(display.inputter.element.selectionStart,
+            checks.cursor,
+            'cursor');
+  }
+  */
+
+  if (checks.status) {
+    is(requisition.getStatus().toString(),
+            checks.status,
+            'status');
+  }
+
+  if (checks.markup) {
+    var statusMarkup = requisition.getInputStatusMarkup(cursor);
+    var actualMarkup = statusMarkup.map(function(s) {
+      return Array(s.string.length + 1).join(s.status.toString()[0]);
+    }).join('');
+
+    is(checks.markup,
+            actualMarkup,
+            'markup');
+  }
+
+  if (checks.emptyParameters) {
+    var actualParams = actual.emptyParameters;
+    is(actualParams.length,
+            checks.emptyParameters.length,
+            'emptyParameters.length');
+
+    if (actualParams.length === checks.emptyParameters.length) {
+      for (var i = 0; i < actualParams.length; i++) {
+        is(actualParams[i].replace(/\u00a0/g, ' '),
+                checks.emptyParameters[i],
+                'emptyParameters[' + i + ']');
+      }
+    }
+    else {
+      info('Expected: [ \"' + actualParams.join('", "') + '" ]');
+    }
+  }
+
+  if (checks.directTabText) {
+    is(actual.directTabText,
+            checks.directTabText,
+            'directTabText');
+  }
+
+  if (checks.arrowTabText) {
+    is(actual.arrowTabText,
+            ' \u00a0\u21E5 ' + checks.arrowTabText,
+            'arrowTabText');
+  }
+
+  if (checks.args) {
+    Object.keys(checks.args).forEach(function(paramName) {
+      var check = checks.args[paramName];
+
+      var assignment;
+      if (paramName === 'command') {
+        assignment = requisition.commandAssignment;
+      }
+      else {
+        assignment = requisition.getAssignment(paramName);
+      }
+
+      if (assignment == null) {
+        ok(false, 'Unknown parameter: ' + paramName);
+        return;
+      }
+
+      if (check.value) {
+        is(assignment.value,
+                check.value,
+                'checkStatus value for ' + paramName);
+      }
+
+      if (check.name) {
+        is(assignment.value.name,
+                check.name,
+                'checkStatus name for ' + paramName);
+      }
+
+      if (check.type) {
+        is(assignment.arg.type,
+                check.type,
+                'checkStatus type for ' + paramName);
+      }
+
+      if (check.arg) {
+        is(assignment.arg.toString(),
+                check.arg,
+                'checkStatus arg for ' + paramName);
+      }
+
+      if (check.status) {
+        is(assignment.getStatus().toString(),
+                check.status,
+                'checkStatus status for ' + paramName);
+      }
+
+      if (check.message) {
+        is(assignment.getMessage(),
+                check.message,
+                'checkStatus message for ' + paramName);
+      }
+    });
+  }
+};
+
+/**
+ * Execute a command:
+ *
+ * DeveloperToolbarTest.exec({
+ *   // Test inputs
+ *   typed: "echo hi",        // Optional, uses existing if undefined
+ *
+ *   // Thing to check
+ *   args: { message: "hi" }, // Check that the args were understood properly
+ *   outputMatch: /^hi$/,     // RegExp to test against textContent of output
+ *                            // (can also be array of RegExps)
+ *   blankOutput: true,       // Special checks when there is no output
+ * });
+ */
+DeveloperToolbarTest.exec = function DTT_exec(tests) {
+  tests = tests || {};
+
+  if (tests.typed) {
+    DeveloperToolbar.display.inputter.setInput(tests.typed);
+  }
+
+  let typed = DeveloperToolbar.display.inputter.getInputState().typed;
+  let output = DeveloperToolbar.display.requisition.exec();
+
+  is(typed, output.typed, 'output.command for: ' + typed);
+
+  if (tests.completed !== false) {
+    ok(output.completed, 'output.completed false for: ' + typed);
+  }
+  else {
+    // It is actually an error if we say something is async and it turns
+    // out not to be? For now we're saying 'no'
+    // ok(!output.completed, 'output.completed true for: ' + typed);
+  }
+
+  if (tests.args != null) {
+    is(Object.keys(tests.args).length, Object.keys(output.args).length,
+       'arg count for ' + typed);
+
+    Object.keys(output.args).forEach(function(arg) {
+      let expectedArg = tests.args[arg];
+      let actualArg = output.args[arg];
+
+      if (typeof expectedArg === 'function') {
+        ok(expectedArg(actualArg), 'failed test func. ' + typed + '/' + arg);
+      }
+      else {
+        if (Array.isArray(expectedArg)) {
+          if (!Array.isArray(actualArg)) {
+            ok(false, 'actual is not an array. ' + typed + '/' + arg);
+            return;
+          }
+
+          is(expectedArg.length, actualArg.length,
+                  'array length: ' + typed + '/' + arg);
+          for (let i = 0; i < expectedArg.length; i++) {
+            is(expectedArg[i], actualArg[i],
+                    'member: "' + typed + '/' + arg + '/' + i);
+          }
+        }
+        else {
+          is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg);
+        }
+      }
+    });
+  }
+
+  let displayed = DeveloperToolbar.outputPanel._div.textContent;
+
+  if (tests.outputMatch) {
+    var doTest = function(match, against) {
+      if (!match.test(against)) {
+        ok(false, "html output for " + typed + " against " + match.source +
+                " (textContent sent to info)");
+        info("Actual textContent");
+        info(against);
+      }
+    }
+    if (Array.isArray(tests.outputMatch)) {
+      tests.outputMatch.forEach(function(match) {
+        doTest(match, displayed);
+      });
+    }
+    else {
+      doTest(tests.outputMatch, displayed);
+    }
+  }
+
+  if (tests.blankOutput != null) {
+    if (!/^$/.test(displayed)) {
+      ok(false, "html output for " + typed + " (textContent sent to info)");
+      info("Actual textContent");
+      info(displayed);
+    }
+  }
+};
+
+/**
+ * Quick wrapper around the things you need to do to run DeveloperToolbar
+ * command tests:
+ * - Set the pref 'devtools.toolbar.enabled' to true
+ * - Add a tab pointing at |uri|
+ * - Open the DeveloperToolbar
+ * - Register a cleanup function to undo the above
+ * - Run the tests
+ *
+ * @param uri The uri of a page to load. Can be 'about:blank' or 'data:...'
+ * @param target Either a function or array of functions containing the tests
+ * to run. If an array of test function is passed then we will clear up after
+ * the tests have completed. If a single test function is passed then this
+ * function should arrange for 'finish()' to be called on completion.
+ */
+DeveloperToolbarTest.test = function DTT_test(uri, target) {
+  let menuItem = document.getElementById("menu_devToolbar");
+  let command = document.getElementById("Tools:DevToolbar");
+  let appMenuItem = document.getElementById("appmenu_devToolbar");
+
+  registerCleanupFunction(function() {
+    DeveloperToolbarTest.hide();
+
+    // a.k.a Services.prefs.clearUserPref("devtools.toolbar.enabled");
+    if (menuItem) {
+      menuItem.hidden = true;
+    }
+    if (command) {
+      command.setAttribute("disabled", "true");
+    }
+    if (appMenuItem) {
+      appMenuItem.hidden = true;
+    }
+
+    // leakHunt({ DeveloperToolbar: DeveloperToolbar });
+  });
+
+  // a.k.a: Services.prefs.setBoolPref("devtools.toolbar.enabled", true);
+  if (menuItem) {
+    menuItem.hidden = false;
+  }
+  if (command) {
+    command.removeAttribute("disabled");
+  }
+  if (appMenuItem) {
+    appMenuItem.hidden = false;
+  }
+
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  content.location = uri;
+
+  let tab = gBrowser.selectedTab;
+  let browser = gBrowser.getBrowserForTab(tab);
+
+  var onTabLoad = function() {
+    browser.removeEventListener("load", onTabLoad, true);
+
+    DeveloperToolbarTest.show(function() {
+      if (helpers) {
+        helpers.setup({ display: DeveloperToolbar.displa