Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 14 Mar 2013 21:45:34 -0400
changeset 124843 8f5b1f9f580492af9096e903ef225d7ffc8297f7
parent 124833 3e1241c6d2ced86d238f08d7573b2cfdaa53747c (current diff)
parent 124842 0f7261e288f21f400e55e7681f43c1009ce03e42 (diff)
child 124844 116a46d187fe9b0b7376348efd803acee9ed5258
child 125015 6d587302645ad19a586d6f4fc056f6c7252899f8
child 125144 9aba82c4908b86953c138bec784f6f54b74a6fbe
child 125209 a4d096b5a2f99a6bcd61f640c9be48ca4bbaa5f8
push id24634
push userryanvm@gmail.com
push dateFri, 15 Mar 2013 03:01:04 +0000
treeherdermozilla-inbound@8f5b1f9f5804 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone22.0a1
first release with
nightly linux32
8f5b1f9f5804 / 22.0a1 / 20130316030854 / files
nightly linux64
8f5b1f9f5804 / 22.0a1 / 20130316030854 / files
nightly mac
8f5b1f9f5804 / 22.0a1 / 20130316030854 / files
nightly win32
8f5b1f9f5804 / 22.0a1 / 20130316030854 / files
nightly win64
8f5b1f9f5804 / 22.0a1 / 20130316030854 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound.
browser/devtools/inspector/test/browser_inspector_bug_566084_location_changed.js
browser/devtools/webconsole/AutocompletePopup.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1038,17 +1038,17 @@ pref("devtools.gcli.allowSet", false);
 pref("devtools.commands.dir", "");
 
 // Toolbox preferences
 pref("devtools.toolbox.footer.height", 250);
 pref("devtools.toolbox.sidebar.width", 500);
 pref("devtools.toolbox.host", "bottom");
 pref("devtools.toolbox.selectedTool", "webconsole");
 pref("devtools.toolbox.toolbarSpec", '["paintflashing toggle","tilt toggle","scratchpad","resize toggle"]');
-pref("devtools.toolbox.sideEnabled", false);
+pref("devtools.toolbox.sideEnabled", true);
 
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
 pref("devtools.inspector.activeSidebar", "ruleview");
 pref("devtools.inspector.markupPreview", false);
 
 // Enable the Layout View
 pref("devtools.layoutview.enabled", true);
--- a/browser/devtools/framework/Toolbox.jsm
+++ b/browser/devtools/framework/Toolbox.jsm
@@ -397,48 +397,64 @@ Toolbox.prototype = {
     }
 
     let tabs = this.doc.getElementById("toolbox-tabs");
     let deck = this.doc.getElementById("toolbox-deck");
 
     let id = toolDefinition.id;
 
     let radio = this.doc.createElement("radio");
-    radio.setAttribute("label", toolDefinition.label);
     radio.className = "toolbox-tab devtools-tab";
     radio.id = "toolbox-tab-" + id;
+    radio.setAttribute("flex", "1");
     radio.setAttribute("toolid", id);
     radio.setAttribute("tooltiptext", toolDefinition.tooltip);
-    if (toolDefinition.icon) {
-      radio.setAttribute("src", toolDefinition.icon);
-    }
 
     radio.addEventListener("command", function(id) {
       this.selectTool(id);
     }.bind(this, id));
 
+    if (toolDefinition.icon) {
+      let image = this.doc.createElement("image");
+      image.setAttribute("src", toolDefinition.icon);
+      radio.appendChild(image);
+    }
+
+    let label = this.doc.createElement("label");
+    label.setAttribute("value", toolDefinition.label)
+    label.setAttribute("crop", "end");
+    label.setAttribute("flex", "1");
+
     let vbox = this.doc.createElement("vbox");
     vbox.className = "toolbox-panel";
     vbox.id = "toolbox-panel-" + id;
 
+    radio.appendChild(label);
     tabs.appendChild(radio);
     deck.appendChild(vbox);
 
     this._addKeysToWindow();
   },
 
   /**
    * Switch to the tool with the given id
    *
    * @param {string} id
    *        The id of the tool to switch to
    */
   selectTool: function TBOX_selectTool(id) {
     let deferred = Promise.defer();
 
+    let selected = this.doc.querySelector(".devtools-tab[selected]");
+    if (selected) {
+      selected.removeAttribute("selected");
+    }
+    let tab = this.doc.getElementById("toolbox-tab-" + id);
+    tab.setAttribute("selected", "true");
+
     if (this._currentToolId == id) {
       // Return the existing panel in order to have a consistent return value.
       return Promise.resolve(this._toolPanels.get(id));
     }
 
     if (!this.isReady) {
       throw new Error("Can't select tool, wait for toolbox 'ready' event");
     }
--- a/browser/devtools/framework/toolbox.xul
+++ b/browser/devtools/framework/toolbox.xul
@@ -23,19 +23,19 @@
     <toolbar class="devtools-tabbar">
 #ifdef XP_MACOSX
       <hbox id="toolbox-controls">
         <toolbarbutton id="toolbox-close"
                        tooltiptext="&toolboxCloseButton.tooltip;"/>
         <hbox id="toolbox-dock-buttons"/>
       </hbox>
 #endif
-      <radiogroup id="toolbox-tabs" orient="horizontal">
-      </radiogroup>
-      <hbox id="toolbox-buttons" flex="1" pack="end"/>
+      <hbox id="toolbox-tabs" flex="1">
+      </hbox>
+      <hbox id="toolbox-buttons" pack="end"/>
 #ifndef XP_MACOSX
       <vbox id="toolbox-controls-separator"/>
       <hbox id="toolbox-controls">
         <hbox id="toolbox-dock-buttons"/>
         <toolbarbutton id="toolbox-close"
                        tooltiptext="&toolboxCloseButton.tooltip;"/>
       </hbox>
 #endif
--- a/browser/devtools/inspector/InspectorPanel.jsm
+++ b/browser/devtools/inspector/InspectorPanel.jsm
@@ -19,16 +19,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Selection",
   "resource:///modules/devtools/Selection.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "HTMLBreadcrumbs",
   "resource:///modules/devtools/Breadcrumbs.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Highlighter",
   "resource:///modules/devtools/Highlighter.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ToolSidebar",
   "resource:///modules/devtools/Sidebar.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SelectorSearch",
+  "resource:///modules/devtools/SelectorSearch.jsm");
 
 const LAYOUT_CHANGE_TIMER = 250;
 
 /**
  * Represents an open instance of the Inspector for a tab.
  * The inspector controls the highlighter, the breadcrumbs,
  * the markup view, and the sidebar (computed view, rule view
  * and layout view).
@@ -45,38 +47,26 @@ this.InspectorPanel = function Inspector
 
 InspectorPanel.prototype = {
   /**
    * open is effectively an asynchronous constructor
    */
   open: function InspectorPanel_open() {
     let deferred = Promise.defer();
 
-    this.preventNavigateAway = this.preventNavigateAway.bind(this);
     this.onNavigatedAway = this.onNavigatedAway.bind(this);
-    this.target.on("will-navigate", this.preventNavigateAway);
     this.target.on("navigate", this.onNavigatedAway);
 
     this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
     this.lastNodemenuItem = this.nodemenu.lastChild;
     this._setupNodeMenu = this._setupNodeMenu.bind(this);
     this._resetNodeMenu = this._resetNodeMenu.bind(this);
     this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
     this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true);
 
-    // Initialize the search related items
-    this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
-    this._lastSearched = null;
-    this._searchResults = null;
-    this._searchIndex = 0;
-    this._onHTMLSearch = this._onHTMLSearch.bind(this);
-    this._onSearchKeypress = this._onSearchKeypress.bind(this);
-    this.searchBox.addEventListener("command", this._onHTMLSearch, true);
-    this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
-
     // Create an empty selection
     this._selection = new Selection();
     this.onNewSelection = this.onNewSelection.bind(this);
     this.selection.on("new-node", this.onNewSelection);
     this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this);
     this.selection.on("before-new-node", this.onBeforeNewSelection);
     this.onDetached = this.onDetached.bind(this);
     this.selection.on("detached", this.onDetached);
@@ -148,16 +138,17 @@ InspectorPanel.prototype = {
       if (this.highlighter) {
         this.highlighter.unlock();
       }
 
       this.emit("ready");
       deferred.resolve(this);
     }.bind(this));
 
+    this.setupSearchBox();
     this.setupSidebar();
 
     return deferred.promise;
   },
 
   /**
    * Selection object (read only)
    */
@@ -191,16 +182,34 @@ InspectorPanel.prototype = {
    * decide whether to show the "are you sure you want to navigate"
    * notification.
    */
   markDirty: function InspectorPanel_markDirty() {
     this.isDirty = true;
   },
 
   /**
+   * Hooks the searchbar to show result and auto completion suggestions.
+   */
+  setupSearchBox: function InspectorPanel_setupSearchBox() {
+    // Initiate the selectors search object.
+    let setNodeFunction = function(node) {
+      this.selection.setNode(node, "selectorsearch");
+    }.bind(this);
+    if (this.searchSuggestions) {
+      this.searchSuggestions.destroy();
+      this.searchSuggestions = null;
+    }
+    this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
+    this.searchSuggestions = new SelectorSearch(this.browser.contentDocument,
+                                                this.searchBox,
+                                                setNodeFunction);
+  },
+
+  /**
    * Build the sidebar.
    */
   setupSidebar: function InspectorPanel_setupSidebar() {
     let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
     this.sidebar = new ToolSidebar(tabbox, this);
 
     let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
 
@@ -251,97 +260,27 @@ InspectorPanel.prototype = {
       if (self._destroyed) {
         return;
       }
 
       if (!self.selection.node) {
         self.selection.setNode(newWindow.document.documentElement, "navigateaway");
       }
       self._initMarkup();
+      self.setupSearchBox();
     }
 
     if (newWindow.document.readyState == "loading") {
       newWindow.addEventListener("DOMContentLoaded", onDOMReady, true);
     } else {
       onDOMReady();
     }
   },
 
   /**
-   * Show a message if the inspector is dirty.
-   */
-  preventNavigateAway: function InspectorPanel_preventNavigateAway(event, request) {
-    if (!this.isDirty) {
-      return;
-    }
-
-    request.suspend();
-
-    let notificationBox = null;
-    if (this.target.isLocalTab) {
-      let gBrowser = this.target.tab.ownerDocument.defaultView.gBrowser;
-      notificationBox = gBrowser.getNotificationBox();
-    }
-    else {
-      notificationBox = this._toolbox.getNotificationBox();
-    }
-
-    let notification = notificationBox.
-      getNotificationWithValue("inspector-page-navigation");
-
-    if (notification) {
-      notificationBox.removeNotification(notification, true);
-    }
-
-    let cancelRequest = function onCancelRequest() {
-      if (request) {
-        request.cancel(Cr.NS_BINDING_ABORTED);
-        request.resume(); // needed to allow the connection to be cancelled.
-        request = null;
-      }
-    };
-
-    let eventCallback = function onNotificationCallback(event) {
-      if (event == "removed") {
-        cancelRequest();
-      }
-    };
-
-    let buttons = [
-      {
-        id: "inspector.confirmNavigationAway.buttonLeave",
-        label: this.strings.GetStringFromName("confirmNavigationAway.buttonLeave"),
-        accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonLeaveAccesskey"),
-        callback: function onButtonLeave() {
-          if (request) {
-            request.resume();
-            request = null;
-          }
-        }.bind(this),
-      },
-      {
-        id: "inspector.confirmNavigationAway.buttonStay",
-        label: this.strings.GetStringFromName("confirmNavigationAway.buttonStay"),
-        accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonStayAccesskey"),
-        callback: cancelRequest
-      },
-    ];
-
-    let message = this.strings.GetStringFromName("confirmNavigationAway.message2");
-
-    notification = notificationBox.appendNotification(message,
-      "inspector-page-navigation", "chrome://browser/skin/Info.png",
-      notificationBox.PRIORITY_WARNING_HIGH, buttons, eventCallback);
-
-    // Make sure this not a transient notification, to avoid the automatic
-    // transient notification removal.
-    notification.persistence = -1;
-  },
-
-  /**
    * When a new node is selected.
    */
   onNewSelection: function InspectorPanel_onNewSelection() {
     this.cancelLayoutChange();
   },
 
   /**
    * When a new node is selected, before the selection has changed.
@@ -374,17 +313,16 @@ InspectorPanel.prototype = {
 
     this.cancelLayoutChange();
 
     if (this.browser) {
       this.browser.removeEventListener("resize", this.scheduleLayoutChange, true);
       this.browser = null;
     }
 
-    this.target.off("will-navigate", this.preventNavigateAway);
     this.target.off("navigate", this.onNavigatedAway);
 
     if (this.highlighter) {
       this.highlighter.off("locked", this.onLockStateChanged);
       this.highlighter.off("unlocked", this.onLockStateChanged);
       this.highlighter.destroy();
     }
 
@@ -395,107 +333,38 @@ InspectorPanel.prototype = {
     this._toolbox = null;
 
     this.sidebar.off("select", this._setDefaultSidebar);
     this.sidebar.destroy();
     this.sidebar = null;
 
     this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true);
     this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true);
-    this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
-    this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
     this.breadcrumbs.destroy();
+    this.searchSuggestions.destroy();
     this.selection.off("new-node", this.onNewSelection);
     this.selection.off("before-new-node", this.onBeforeNewSelection);
     this.selection.off("detached", this.onDetached);
     this._destroyMarkup();
     this._selection.destroy();
     this._selection = null;
     this.panelWin.inspector = null;
     this.target = null;
     this.panelDoc = null;
     this.panelWin = null;
     this.breadcrumbs = null;
+    this.searchSuggestions = null;
     this.lastNodemenuItem = null;
     this.nodemenu = null;
-    this.searchBox = null;
     this.highlighter = null;
-    this._searchResults = null;
 
     return Promise.resolve(null);
   },
 
   /**
-   * The command callback for the HTML search box. This function is
-   * automatically invoked as the user is typing.
-   */
-  _onHTMLSearch: function InspectorPanel__onHTMLSearch() {
-    let query = this.searchBox.value;
-    if (query == this._lastSearched) {
-      return;
-    }
-    this._lastSearched = query;
-    this._searchIndex = 0;
-
-    if (query.length == 0) {
-      this.searchBox.removeAttribute("filled");
-      this.searchBox.classList.remove("devtools-no-search-result");
-      return;
-    }
-
-    this.searchBox.setAttribute("filled", true);
-    this._searchResults = this.browser.contentDocument.querySelectorAll(query);
-    if (this._searchResults.length > 0) {
-      this.searchBox.classList.remove("devtools-no-search-result");
-      this.cancelLayoutChange();
-      this.selection.setNode(this._searchResults[0]);
-    } else {
-      this.searchBox.classList.add("devtools-no-search-result");
-    }
-  },
-
-  /**
-   * Search for the search box value as a query selector.
-   */
-  _onSearchKeypress: function InspectorPanel__onSearchKeypress(aEvent) {
-    let query = this.searchBox.value;
-    switch(aEvent.keyCode) {
-      case aEvent.DOM_VK_ENTER:
-      case aEvent.DOM_VK_RETURN:
-        if (query == this._lastSearched) {
-          this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
-        } else {
-          this._onHTMLSearch();
-          return;
-        }
-        break;
-
-      case aEvent.DOM_VK_UP:
-        if (--this._searchIndex < 0) {
-          this._searchIndex = this._searchResults.length - 1;
-        }
-        break;
-
-      case aEvent.DOM_VK_DOWN:
-        this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
-        break;
-
-      default:
-        return;
-    }
-
-    aEvent.preventDefault();
-    aEvent.stopPropagation();
-    this.cancelLayoutChange();
-    if (this._searchResults.length > 0) {
-      this.selection.setNode(this._searchResults[this._searchIndex]);
-    }
-  },
-
-  /**
    * Show the node menu.
    */
   showNodeMenu: function InspectorPanel_showNodeMenu(aButton, aPosition, aExtraItems) {
     if (aExtraItems) {
       for (let item of aExtraItems) {
         this.nodemenu.appendChild(item);
       }
     }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/SelectorSearch.jsm
@@ -0,0 +1,549 @@
+/* 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/. */
+
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup",
+                                  "resource:///modules/devtools/AutocompletePopup.jsm");
+this.EXPORTED_SYMBOLS = ["SelectorSearch"];
+
+// Maximum number of selector suggestions shown in the panel.
+const MAX_SUGGESTIONS = 15;
+
+/**
+ * Converts any input box on a page to a CSS selector search and suggestion box.
+ *
+ * @constructor
+ * @param nsIDOMDocument aContentDocument
+ *        The content document which inspector is attached to.
+ * @param nsiInputElement aInputNode
+ *        The input element to which the panel will be attached and from where
+ *        search input will be taken.
+ * @param Function aCallback
+ *        The method to callback when a search is available.
+ *        This method is called with the matched node as the first argument.
+ */
+this.SelectorSearch = function(aContentDocument, aInputNode, aCallback) {
+  this.doc = aContentDocument;
+  this.callback = aCallback;
+  this.searchBox = aInputNode;
+  this.panelDoc = this.searchBox.ownerDocument;
+
+  // initialize variables.
+  this._lastSearched = null;
+  this._lastValidSearch = "";
+  this._lastToLastValidSearch = null;
+  this._searchResults = null;
+  this._searchSuggestions = {};
+  this._searchIndex = 0;
+
+  // bind!
+  this._showPopup = this._showPopup.bind(this);
+  this._onHTMLSearch = this._onHTMLSearch.bind(this);
+  this._onSearchKeypress = this._onSearchKeypress.bind(this);
+  this._onListBoxKeypress = this._onListBoxKeypress.bind(this);
+
+  // Options for the AutocompletePopup.
+  let options = {
+    panelId: "inspector-searchbox-panel",
+    listBoxId: "searchbox-panel-listbox",
+    fixedWidth: true,
+    autoSelect: true,
+    position: "before_start",
+    direction: "ltr",
+    onClick: this._onListBoxKeypress,
+    onKeypress: this._onListBoxKeypress,
+  };
+  this.searchPopup = new AutocompletePopup(this.panelDoc, options);
+
+  // event listeners.
+  this.searchBox.addEventListener("command", this._onHTMLSearch, true);
+  this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
+}
+
+this.SelectorSearch.prototype = {
+
+  // The possible states of the query.
+  States: {
+    CLASS: "class",
+    ID: "id",
+    TAG: "tag",
+  },
+
+  // The current state of the query.
+  _state: null,
+
+  // The query corresponding to last state computation.
+  _lastStateCheckAt: null,
+
+  /**
+   * Computes the state of the query. State refers to whether the query
+   * currently requires a class suggestion, or a tag, or an Id suggestion.
+   * This getter will effectively compute the state by traversing the query
+   * character by character each time the query changes.
+   *
+   * @example
+   *        '#f' requires an Id suggestion, so the state is States.ID
+   *        'div > .foo' requires class suggestion, so state is States.CLASS
+   */
+  get state() {
+    if (!this.searchBox || !this.searchBox.value) {
+      return null;
+    }
+
+    let query = this.searchBox.value;
+    if (this._lastStateCheckAt == query) {
+      // If query is the same, return early.
+      return this._state;
+    }
+    this._lastStateCheckAt = query;
+
+    this._state = null;
+    let subQuery = "";
+    // Now we iterate over the query and decide the state character by character.
+    // The logic here is that while iterating, the state can go from one to
+    // another with some restrictions. Like, if the state is Class, then it can
+    // never go to Tag state without a space or '>' character; Or like, a Class
+    // state with only '.' cannot go to an Id state without any [a-zA-Z] after
+    // the '.' which means that '.#' is a selector matching a class name '#'.
+    // Similarily for '#.' which means a selctor matching an id '.'.
+    for (let i = 1; i <= query.length; i++) {
+      // Calculate the state.
+      subQuery = query.slice(0, i);
+      let [secondLastChar, lastChar] = subQuery.slice(-2);
+      switch (this._state) {
+        case null:
+          // This will happen only in the first iteration of the for loop.
+          lastChar = secondLastChar;
+        case this.States.TAG:
+          this._state = lastChar == "."
+            ? this.States.CLASS
+            : lastChar == "#"
+              ? this.States.ID
+              : this.States.TAG;
+          break;
+
+        case this.States.CLASS:
+          if (subQuery.match(/[\.]+[^\.]*$/)[0].length > 2) {
+            // Checks whether the subQuery has atleast one [a-zA-Z] after the '.'.
+            this._state = (lastChar == " " || lastChar == ">")
+            ? this.States.TAG
+            : lastChar == "#"
+              ? this.States.ID
+              : this.States.CLASS;
+          }
+          break;
+
+        case this.States.ID:
+          if (subQuery.match(/[#]+[^#]*$/)[0].length > 2) {
+            // Checks whether the subQuery has atleast one [a-zA-Z] after the '#'.
+            this._state = (lastChar == " " || lastChar == ">")
+            ? this.States.TAG
+            : lastChar == "."
+              ? this.States.CLASS
+              : this.States.ID;
+          }
+          break;
+      }
+    }
+    return this._state;
+  },
+
+  /**
+   * Removes event listeners and cleans up references.
+   */
+  destroy: function SelectorSearch_destroy() {
+    // event listeners.
+    this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
+    this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
+    this.searchPopup.destroy();
+    this.searchPopup = null;
+    this.searchBox = null;
+    this.doc = null;
+    this.panelDoc = null;
+    this._searchResults = null;
+    this._searchSuggestions = null;
+    this.callback = null;
+  },
+
+  /**
+   * The command callback for the input box. This function is automatically
+   * invoked as the user is typing if the input box type is search.
+   */
+  _onHTMLSearch: function SelectorSearch__onHTMLSearch() {
+    let query = this.searchBox.value;
+    if (query == this._lastSearched) {
+      return;
+    }
+    this._lastSearched = query;
+    this._searchIndex = 0;
+
+    if (query.length == 0) {
+      this._lastValidSearch = "";
+      this.searchBox.removeAttribute("filled");
+      this.searchBox.classList.remove("devtools-no-search-result");
+      if (this.searchPopup.isOpen) {
+        this.searchPopup.hidePopup();
+      }
+      return;
+    }
+
+    this.searchBox.setAttribute("filled", true);
+    try {
+      this._searchResults = this.doc.querySelectorAll(query);
+    }
+    catch (ex) {
+      this._searchResults = [];
+    }
+    if (this._searchResults.length > 0) {
+      this._lastValidSearch = query;
+      // Even though the selector matched atleast one node, there is still
+      // possibility of suggestions.
+      if (query.match(/[\s>+]$/)) {
+        // If the query has a space or '>' at the end, create a selector to match
+        // the children of the selector inside the search box by adding a '*'.
+        this._lastValidSearch += "*";
+      }
+      else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
+        // If the query is a partial descendant selector which does not matches
+        // any node, remove the last incomplete part and add a '*' to match
+        // everything. For ex, convert 'foo > b' to 'foo > *' .
+        let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0];
+        this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
+      }
+
+      if (!query.slice(-1).match(/[\.#\s>+]/)) {
+        // Hide the popup if we have some matching nodes and the query is not
+        // ending with [.# >] which means that the selector is not at the
+        // beginning of a new class, tag or id.
+        if (this.searchPopup.isOpen) {
+          this.searchPopup.hidePopup();
+        }
+      }
+      else {
+        this.showSuggestions();
+      }
+      this.searchBox.classList.remove("devtools-no-search-result");
+      this.callback(this._searchResults[0]);
+    }
+    else {
+      if (query.match(/[\s>+]$/)) {
+        this._lastValidSearch = query + "*";
+      }
+      else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
+        let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
+        this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
+      }
+      this.searchBox.classList.add("devtools-no-search-result");
+      this.showSuggestions();
+    }
+  },
+
+  /**
+   * Handles keypresses inside the input box.
+   */
+  _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) {
+    let query = this.searchBox.value;
+    switch(aEvent.keyCode) {
+      case aEvent.DOM_VK_ENTER:
+      case aEvent.DOM_VK_RETURN:
+        if (query == this._lastSearched) {
+          this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
+        }
+        else {
+          this._onHTMLSearch();
+          return;
+        }
+        break;
+
+      case aEvent.DOM_VK_UP:
+        if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
+          this.searchPopup.focus();
+          if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
+            this.searchPopup.selectedIndex =
+              Math.max(0, this.searchPopup.itemCount - 2);
+          }
+          else {
+            this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
+          }
+          this.searchBox.value = this.searchPopup.selectedItem.label;
+        }
+        else if (--this._searchIndex < 0) {
+          this._searchIndex = this._searchResults.length - 1;
+        }
+        break;
+
+      case aEvent.DOM_VK_DOWN:
+        if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) {
+          this.searchPopup.focus();
+          this.searchPopup.selectedIndex = 0;
+          this.searchBox.value = this.searchPopup.selectedItem.label;
+        }
+        this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
+        break;
+
+      case aEvent.DOM_VK_TAB:
+        if (this.searchPopup.isOpen &&
+            this.searchPopup.getItemAtIndex(this.searchPopup.itemCount - 1)
+                .preLabel == query) {
+          this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1;
+          this.searchBox.value = this.searchPopup.selectedItem.label;
+          this._onHTMLSearch();
+        }
+        break;
+
+      case aEvent.DOM_VK_BACK_SPACE:
+      case aEvent.DOM_VK_DELETE:
+        // need to throw away the lastValidSearch.
+        this._lastToLastValidSearch = null;
+        // This gets the most complete selector from the query. For ex.
+        // '.foo.ba' returns '.foo' , '#foo > .bar.baz' returns '#foo > .bar'
+        // '.foo +bar' returns '.foo +' and likewise.
+        this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
+                                 query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
+                                 ["",""])[1];
+        return;
+
+      default:
+        return;
+    }
+
+    aEvent.preventDefault();
+    aEvent.stopPropagation();
+    if (this._searchResults.length > 0) {
+      this.callback(this._searchResults[this._searchIndex]);
+    }
+  },
+
+  /**
+   * Handles keypress and mouse click on the suggestions richlistbox.
+   */
+  _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) {
+    switch(aEvent.keyCode || aEvent.button) {
+      case aEvent.DOM_VK_ENTER:
+      case aEvent.DOM_VK_RETURN:
+      case aEvent.DOM_VK_TAB:
+      case 0: // left mouse button
+        aEvent.stopPropagation();
+        aEvent.preventDefault();
+        this.searchBox.value = this.searchPopup.selectedItem.label;
+        this.searchBox.focus();
+        this._onHTMLSearch();
+        break;
+
+      case aEvent.DOM_VK_UP:
+        if (this.searchPopup.selectedIndex == 0) {
+          this.searchPopup.selectedIndex = -1;
+          aEvent.stopPropagation();
+          aEvent.preventDefault();
+          this.searchBox.focus();
+        }
+        else {
+          let index = this.searchPopup.selectedIndex;
+          this.searchBox.value = this.searchPopup.getItemAtIndex(index - 1).label;
+        }
+        break;
+
+      case aEvent.DOM_VK_DOWN:
+        if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) {
+          this.searchPopup.selectedIndex = -1;
+          aEvent.stopPropagation();
+          aEvent.preventDefault();
+          this.searchBox.focus();
+        }
+        else {
+          let index = this.searchPopup.selectedIndex;
+          this.searchBox.value = this.searchPopup.getItemAtIndex(index + 1).label;
+        }
+        break;
+
+      case aEvent.DOM_VK_BACK_SPACE:
+        aEvent.stopPropagation();
+        aEvent.preventDefault();
+        this.searchBox.focus();
+        if (this.searchBox.selectionStart > 0) {
+          this.searchBox.value =
+            this.searchBox.value.substring(0, this.searchBox.selectionStart - 1);
+        }
+        this._lastToLastValidSearch = null;
+        let query = this.searchBox.value;
+        this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
+                                 query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
+                                 ["",""])[1];
+        this._onHTMLSearch();
+        break;
+    }
+  },
+
+  
+  /**
+   * Populates the suggestions list and show the suggestion popup.
+   */
+  _showPopup: function SelectorSearch__showPopup(aList, aFirstPart) {
+    // Sort alphabetically in increaseing order.
+    aList = aList.sort();
+    // Sort based on count= in decreasing order.
+    aList = aList.sort(function([a1,a2], [b1,b2]) {
+      return a2 < b2;
+    });
+
+    let total = 0;
+    let query = this.searchBox.value;
+    let toLowerCase = false;
+    let items = [];
+    // In case of tagNames, change the case to small.
+    if (query.match(/.*[\.#][^\.#]{0,}$/) == null) {
+      toLowerCase = true;
+    }
+    for (let [value, count] of aList) {
+      // for cases like 'div ' or 'div >' or 'div+'
+      if (query.match(/[\s>+]$/)) {
+        value = query + value;
+      }
+      // for cases like 'div #a' or 'div .a' or 'div > d' and likewise
+      else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#]*$/)) {
+        let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+\.#]*$/)[0];
+        value = query.slice(0, -1 * lastPart.length + 1) + value;
+      }
+      // for cases like 'div.class' or '#foo.bar' and likewise
+      else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) {
+        let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s>+]*$/)[0];
+        value = query.slice(0, -1 * lastPart.length + 1) + value;
+      }
+      let item = {
+        preLabel: query,
+        label: value,
+        count: count
+      };
+      if (toLowerCase) {
+        item.label = value.toLowerCase();
+      }
+      items.unshift(item);
+      if (++total > MAX_SUGGESTIONS - 1) {
+        break;
+      }
+    }
+    if (total > 0) {
+      this.searchPopup.setItems(items);
+      this.searchPopup.openPopup(this.searchBox);
+    }
+    else {
+      this.searchPopup.hidePopup();
+    }
+  },
+
+  /**
+   * Suggests classes,ids and tags based on the user input as user types in the
+   * searchbox.
+   */
+  showSuggestions: function SelectorSearch_showSuggestions() {
+    let query = this.searchBox.value;
+    if (this._lastValidSearch != "" &&
+        this._lastToLastValidSearch != this._lastValidSearch) {
+      this._searchSuggestions = {
+        ids: new Map(),
+        classes: new Map(),
+        tags: new Map(),
+      };
+
+      let nodes = [];
+      try {
+        nodes = this.doc.querySelectorAll(this._lastValidSearch);
+      } catch (ex) {}
+      for (let node of nodes) {
+        this._searchSuggestions.ids.set(node.id, 1);
+        this._searchSuggestions.tags
+            .set(node.tagName,
+                 (this._searchSuggestions.tags.get(node.tagName) || 0) + 1);
+        for (let className of node.classList) {
+          this._searchSuggestions.classes
+            .set(className,
+                 (this._searchSuggestions.classes.get(className) || 0) + 1);
+        }
+      }
+      this._lastToLastValidSearch = this._lastValidSearch;
+    }
+    else if (this._lastToLastValidSearch != this._lastValidSearch) {
+      this._searchSuggestions = {
+        ids: new Map(),
+        classes: new Map(),
+        tags: new Map(),
+      };
+
+      if (query.length == 0) {
+        return;
+      }
+
+      let nodes = null;
+      if (this.state == this.States.CLASS) {
+        nodes = this.doc.querySelectorAll("[class]");
+        for (let node of nodes) {
+          for (let className of node.classList) {
+            this._searchSuggestions.classes
+              .set(className,
+                   (this._searchSuggestions.classes.get(className) || 0) + 1);
+          }
+        }
+      }
+      else if (this.state == this.States.ID) {
+        nodes = this.doc.querySelectorAll("[id]");
+        for (let node of nodes) {
+          this._searchSuggestions.ids.set(node.id, 1);
+        }
+      }
+      else if (this.state == this.States.TAG) {
+        nodes = this.doc.getElementsByTagName("*");
+        for (let node of nodes) {
+          this._searchSuggestions.tags
+              .set(node.tagName,
+                   (this._searchSuggestions.tags.get(node.tagName) || 0) + 1);
+        }
+      }
+      else {
+        return;
+      }
+      this._lastToLastValidSearch = this._lastValidSearch;
+    }
+
+    // Filter the suggestions based on search box value.
+    let result = [];
+    let firstPart = "";
+    if (this.state == this.States.TAG) {
+      // gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
+      // 'di' returns 'di' and likewise.
+      firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["",query])[1];
+      for (let [tag, count] of this._searchSuggestions.tags) {
+        if (tag.toLowerCase().startsWith(firstPart.toLowerCase())) {
+          result.push([tag, count]);
+        }
+      }
+    }
+    else if (this.state == this.States.CLASS) {
+      // gets the class that is being completed. For ex. '.foo.b' returns 'b'
+      firstPart = query.match(/\.([^\.]*)$/)[1];
+      for (let [className, count] of this._searchSuggestions.classes) {
+        if (className.startsWith(firstPart)) {
+          result.push(["." + className, count]);
+        }
+      }
+      firstPart = "." + firstPart;
+    }
+    else if (this.state == this.States.ID) {
+      // gets the id that is being completed. For ex. '.foo#b' returns 'b'
+      firstPart = query.match(/#([^#]*)$/)[1];
+      for (let [id, count] of this._searchSuggestions.ids) {
+        if (id.startsWith(firstPart)) {
+          result.push(["#" + id, 1]);
+        }
+      }
+      firstPart = "#" + firstPart;
+    }
+
+    this._showPopup(result, firstPart);
+  },
+};
--- a/browser/devtools/inspector/inspector.css
+++ b/browser/devtools/inspector/inspector.css
@@ -1,8 +1,28 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 #inspector-sidebar {
   min-width: 250px;
 }
+
+#searchbox-panel-listbox {
+  width: 250px;
+  max-width: 250px;
+  overflow-x: hidden;
+}
+
+#searchbox-panel-listbox > richlistitem,
+#searchbox-panel-listbox > richlistitem[selected] {
+  overflow-x: hidden;
+}
+
+#searchbox-panel-listbox > richlistitem > .initial-value {
+  max-width: 130px;
+  margin-left: 15px;
+}
+
+#searchbox-panel-listbox > richlistitem > .autocomplete-value {
+  max-width: 150px;
+}
--- a/browser/devtools/inspector/test/Makefile.in
+++ b/browser/devtools/inspector/test/Makefile.in
@@ -23,27 +23,30 @@ include $(topsrcdir)/config/rules.mk
 		browser_inspector_invalidate.js \
 		browser_inspector_menu.js \
 		browser_inspector_menu.html \
 		browser_inspector_pseudoClass_menu.js \
 		browser_inspector_destroyselection.html \
 		browser_inspector_destroyselection.js \
 		browser_inspector_bug_699308_iframe_navigation.js \
 		browser_inspector_bug_672902_keyboard_shortcuts.js \
-		browser_inspector_bug_566084_location_changed.js \
 		browser_inspector_sidebarstate.js \
 		browser_inspector_pseudoclass_lock.js \
 		browser_inspector_cmd_inspect.js \
 		browser_inspector_cmd_inspect.html \
 		browser_inspector_highlighter_autohide.js \
 		browser_inspector_changes.js \
 		browser_inspector_bug_674871.js \
 		browser_inspector_bug_817558_delete_node.js \
 		browser_inspector_bug_650804_search.js \
 		browser_inspector_bug_650804_search.html \
+		browser_inspector_bug_831693_input_suggestion.js \
+		browser_inspector_bug_831693_searchbox_panel_navigation.js \
+		browser_inspector_bug_831693_combinator_suggestions.js \
+		browser_inspector_bug_831693_search_suggestions.html \
 		browser_inspector_bug_835722_infobar_reappears.js \
 		browser_inspector_bug_840156_destroy_after_navigation.js \
 		head.js \
 		helpers.js \
 		$(NULL)
 
 libs::	$(_BROWSER_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
deleted file mode 100644
--- a/browser/devtools/inspector/test/browser_inspector_bug_566084_location_changed.js
+++ /dev/null
@@ -1,131 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-let tempScope = {};
-Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
-let TargetFactory = tempScope.TargetFactory;
-
-function test() {
-  let notificationBox, inspector;
-  let alertActive1_called = false;
-  let alertActive2_called = false;
-
-  function startLocationTests() {
-    openInspector(runInspectorTests);
-  }
-
-  function runInspectorTests(aInspector) {
-    inspector = aInspector;
-
-    let para = content.document.querySelector("p");
-    ok(para, "found the paragraph element");
-    is(para.textContent, "init", "paragraph content is correct");
-
-    inspector.markDirty();
-
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
-    let toolbox = gDevTools.getToolbox(target);
-    notificationBox = gBrowser.getNotificationBox();
-    notificationBox.addEventListener("AlertActive", alertActive1, false);
-
-    ok(toolbox, "We have access to the notificationBox");
-
-    gBrowser.selectedBrowser.addEventListener("load", onPageLoad, true);
-
-    content.location = "data:text/html,<div>location change test 1 for " +
-      "inspector</div><p>test1</p>";
-  }
-
-  function alertActive1() {
-    alertActive1_called = true;
-    notificationBox.removeEventListener("AlertActive", alertActive1, false);
-
-    let notification = notificationBox.
-      getNotificationWithValue("inspector-page-navigation");
-    ok(notification, "found the inspector-page-navigation notification");
-
-    // By closing the notification it is expected that page navigation is
-    // canceled.
-    executeSoon(function() {
-      notification.close();
-      locationTest2();
-    });
-  }
-
-  function locationTest2() {
-    // Location did not change.
-    let para = content.document.querySelector("p");
-    ok(para, "found the paragraph element, second time");
-    is(para.textContent, "init", "paragraph content is correct");
-
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
-    let inspector = gDevTools.getToolbox(target).getPanel("inspector");
-    ok(inspector, "Inspector still alive");
-
-    notificationBox.addEventListener("AlertActive", alertActive2, false);
-
-    content.location = "data:text/html,<div>location change test 2 for " +
-      "inspector</div><p>test2</p>";
-  }
-
-  function alertActive2() {
-    alertActive2_called = true;
-    notificationBox.removeEventListener("AlertActive", alertActive2, false);
-
-    let notification = notificationBox.
-      getNotificationWithValue("inspector-page-navigation");
-    ok(notification, "found the inspector-page-navigation notification");
-
-    let buttons = notification.querySelectorAll("button");
-    let buttonLeave = null;
-    for (let i = 0; i < buttons.length; i++) {
-      if (buttons[i].buttonInfo.id == "inspector.confirmNavigationAway.buttonLeave") {
-        buttonLeave = buttons[i];
-        break;
-      }
-    }
-
-    ok(buttonLeave, "the Leave page button was found");
-
-    // Accept page navigation.
-    executeSoon(function(){
-      buttonLeave.doCommand();
-    });
-  }
-
-  function onPageLoad() {
-    gBrowser.selectedBrowser.removeEventListener("load", onPageLoad, true);
-
-    isnot(content.location.href.indexOf("test2"), -1,
-          "page navigated to the correct location");
-
-    let para = content.document.querySelector("p");
-    ok(para, "found the paragraph element, third time");
-    is(para.textContent, "test2", "paragraph content is correct");
-
-    let root = content.document.documentElement;
-    is(inspector.selection.node, root, "Selection is the root of the new page.");
-
-    ok(alertActive1_called, "first notification box has been showed");
-    ok(alertActive2_called, "second notification box has been showed");
-    testEnd();
-  }
-
-
-  function testEnd() {
-    notificationBox = null;
-    gBrowser.removeCurrentTab();
-    executeSoon(finish);
-  }
-
-  waitForExplicitFinish();
-
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad() {
-    gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true);
-    waitForFocus(startLocationTests, content);
-  }, true);
-
-  content.location = "data:text/html,<div>location change tests for " +
-    "inspector.</div><p>init</p>";
-}
--- a/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
@@ -15,27 +15,27 @@ function test()
   //  is the searched text valid selector
   // ]
   let keyStates = [
     ["d", "b1", false],
     ["i", "b1", false],
     ["v", "d1", true],
     ["VK_DOWN", "d2", true],
     ["VK_ENTER", "d1", true],
-    [".", "d1", true],
+    [".", "d1", false],
     ["c", "d1", false],
     ["1", "d2", true],
     ["VK_DOWN", "d2", true],
     ["VK_BACK_SPACE", "d2", false],
     ["VK_BACK_SPACE", "d2", false],
     ["VK_BACK_SPACE", "d1", true],
     ["VK_BACK_SPACE", "d1", false],
     ["VK_BACK_SPACE", "d1", false],
     ["VK_BACK_SPACE", "d1", true],
-    [".", "d1", true],
+    [".", "d1", false],
     ["c", "d1", false],
     ["1", "d2", true],
     ["VK_DOWN", "s2", true],
     ["VK_DOWN", "p1", true],
     ["VK_UP", "s2", true],
     ["VK_UP", "d2", true],
     ["VK_UP", "p1", true],
     ["VK_BACK_SPACE", "p1", false],
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+  waitForExplicitFinish();
+
+  let inspector, searchBox, state, popup;
+
+  // The various states of the inspector: [key, suggestions array]
+  // [
+  //  what key to press,
+  //  suggestions array with count [
+  //    [suggestion1, count1], [suggestion2] ...
+  //  ] count can be left to represent 1
+  // ]
+  let keyStates = [
+    ["d", [["div", 4]]],
+    ["i", [["div", 4]]],
+    ["v", []],
+    [" ", [["div div", 2], ["div span", 2]]],
+    [">", [["div >div", 2], ["div >span", 2]]],
+    ["VK_BACK_SPACE", [["div div", 2], ["div span", 2]]],
+    ["+", [["div +span"]]],
+    ["VK_BACK_SPACE", [["div div", 2], ["div span", 2]]],
+    ["VK_BACK_SPACE", []],
+    ["VK_BACK_SPACE", [["div", 4]]],
+    ["VK_BACK_SPACE", [["div", 4]]],
+    ["VK_BACK_SPACE", []],
+    ["p", []],
+    [" ", [["p strong"]]],
+    ["+", [["p +button"], ["p +p"]]],
+    ["b", [["p +button"]]],
+    ["u", [["p +button"]]],
+    ["t", [["p +button"]]],
+    ["t", [["p +button"]]],
+    ["o", [["p +button"]]],
+    ["n", []],
+    ["+", [["p +button+p"]]],
+  ];
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(setupTest, content);
+  }, true);
+
+  content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html";
+
+  function $(id) {
+    if (id == null) return null;
+    return content.document.getElementById(id);
+  }
+
+  function setupTest()
+  {
+    openInspector(startTest);
+  }
+
+  function startTest(aInspector)
+  {
+    inspector = aInspector;
+    searchBox =
+      inspector.panelWin.document.getElementById("inspector-searchbox");
+    popup = inspector.searchSuggestions.searchPopup;
+
+    focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+      searchBox.addEventListener("command", checkState, true);
+      checkStateAndMoveOn(0);
+    });
+  }
+
+  function checkStateAndMoveOn(index) {
+    if (index == keyStates.length) {
+      finishUp();
+      return;
+    }
+
+    let [key, suggestions] = keyStates[index];
+    state = index;
+
+    info("pressing key " + key + " to get suggestions " +
+         JSON.stringify(suggestions));
+    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+  }
+
+  function checkState(event) {
+    executeSoon(function() {
+      let [key, suggestions] = keyStates[state];
+      let actualSuggestions = popup.getItems();
+      is(popup._panel.state == "open" || popup._panel.state == "showing"
+         ? actualSuggestions.length: 0, suggestions.length,
+         "There are expected number of suggestions at " + state + "th step.");
+      actualSuggestions = actualSuggestions.reverse();
+      for (let i = 0; i < suggestions.length; i++) {
+        is(suggestions[i][0], actualSuggestions[i].label,
+           "The suggestion at " + i + "th index for " + state +
+           "th step is correct.")
+        is(suggestions[i][1] || 1, actualSuggestions[i].count,
+           "The count for suggestion at " + i + "th index for " + state +
+           "th step is correct.")
+      }
+      checkStateAndMoveOn(state + 1);
+    });
+  }
+
+  function finishUp() {
+    searchBox = null;
+    popup = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+  waitForExplicitFinish();
+
+  let inspector, searchBox, state, popup;
+
+  // The various states of the inspector: [key, suggestions array]
+  // [
+  //  what key to press,
+  //  suggestions array with count [
+  //    [suggestion1, count1], [suggestion2] ...
+  //  ] count can be left to represent 1
+  // ]
+  let keyStates = [
+    ["d", [["div", 2]]],
+    ["i", [["div", 2]]],
+    ["v", []],
+    [".", [["div.c1"]]],
+    ["VK_BACK_SPACE", []],
+    ["#", [["div#d1"], ["div#d2"]]],
+    ["VK_BACK_SPACE", []],
+    ["VK_BACK_SPACE", [["div", 2]]],
+    ["VK_BACK_SPACE", [["div", 2]]],
+    ["VK_BACK_SPACE", []],
+    [".", [[".c1", 3], [".c2"]]],
+    ["c", [[".c1", 3], [".c2"]]],
+    ["2", []],
+    ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
+    ["1", []],
+    ["#", [["#d2"], ["#p1"], ["#s2"]]],
+    ["VK_BACK_SPACE", []],
+    ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
+    ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]],
+    ["VK_BACK_SPACE", []],
+    ["#", [["#b1"], ["#d1"], ["#d2"], ["#p1"], ["#p2"], ["#p3"], ["#s1"], ["#s2"]]],
+    ["p", [["#p1"], ["#p2"], ["#p3"]]],
+    ["VK_BACK_SPACE", [["#b1"], ["#d1"], ["#d2"], ["#p1"], ["#p2"], ["#p3"], ["#s1"], ["#s2"]]],
+    ["VK_BACK_SPACE", []],
+  ];
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(setupTest, content);
+  }, true);
+
+  content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_650804_search.html";
+
+  function $(id) {
+    if (id == null) return null;
+    return content.document.getElementById(id);
+  }
+
+  function setupTest()
+  {
+    openInspector(startTest);
+  }
+
+  function startTest(aInspector)
+  {
+    inspector = aInspector;
+    searchBox =
+      inspector.panelWin.document.getElementById("inspector-searchbox");
+    popup = inspector.searchSuggestions.searchPopup;
+
+    focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+      searchBox.addEventListener("command", checkState, true);
+      checkStateAndMoveOn(0);
+    });
+  }
+
+  function checkStateAndMoveOn(index) {
+    if (index == keyStates.length) {
+      finishUp();
+      return;
+    }
+
+    let [key, suggestions] = keyStates[index];
+    state = index;
+
+    info("pressing key " + key + " to get suggestions " +
+         JSON.stringify(suggestions));
+    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+  }
+
+  function checkState(event) {
+    executeSoon(function() {
+      let [key, suggestions] = keyStates[state];
+      let actualSuggestions = popup.getItems();
+      is(popup._panel.state == "open" || popup._panel.state == "showing"
+         ? actualSuggestions.length: 0, suggestions.length,
+         "There are expected number of suggestions at " + state + "th step.");
+      actualSuggestions = actualSuggestions.reverse();
+      for (let i = 0; i < suggestions.length; i++) {
+        is(suggestions[i][0], actualSuggestions[i].label,
+           "The suggestion at " + i + "th index for " + state +
+           "th step is correct.")
+        is(suggestions[i][1] || 1, actualSuggestions[i].count,
+           "The count for suggestion at " + i + "th index for " + state +
+           "th step is correct.")
+      }
+      checkStateAndMoveOn(state + 1);
+    });
+  }
+
+  function finishUp() {
+    searchBox = null;
+    popup = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Inspector Search Box Test</title>
+</head>
+<body>
+  <div id="d1">
+    <div class="l1">
+      <div id="d2" class="c1">Hello, I'm nested div</div>
+    </div>
+  </div>
+  <span id="s1">Hello, I'm a span
+    <div class="l1">
+      <span>Hi I am a nested span</span>
+      <span class="s4">Hi I am a nested classed span</span>
+    </div>
+  </span>
+  <span class="c1" id="s2">And me</span>
+
+  <p class="c1" id="p1">.someclass</p>
+  <p id="p2">#someid</p>
+  <button id="b1" disabled>button[disabled]</button>
+  <p id="p3" class="c2"><strong>p&gt;strong</strong></p>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_searchbox_panel_navigation.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test()
+{
+  waitForExplicitFinish();
+  requestLongerTimeout(2);
+
+  let inspector, searchBox, state, panel;
+  let panelOpeningStates = [0, 3, 14, 17];
+  let panelClosingStates = [2, 13, 16];
+
+  // The various states of the inspector: [key, query]
+  // [
+  //  what key to press,
+  //  what should be the text in the searchbox
+  // ]
+  let keyStates = [
+    ["d", "d"],
+    ["i", "di"],
+    ["v", "div"],
+    [".", "div."],
+    ["VK_UP", "div.c1"],
+    ["VK_DOWN", "div.l1"],
+    ["VK_DOWN", "div.l1"],
+    ["VK_BACK_SPACE", "div.l"],
+    ["VK_TAB", "div.l1"],
+    [" ", "div.l1 "],
+    ["VK_UP", "div.l1 DIV"],
+    ["VK_UP", "div.l1 DIV"],
+    [".", "div.l1 DIV."],
+    ["VK_TAB", "div.l1 DIV.c1"],
+    ["VK_BACK_SPACE", "div.l1 DIV.c"],
+    ["VK_BACK_SPACE", "div.l1 DIV."],
+    ["VK_BACK_SPACE", "div.l1 DIV"],
+    ["VK_BACK_SPACE", "div.l1 DI"],
+    ["VK_BACK_SPACE", "div.l1 D"],
+    ["VK_BACK_SPACE", "div.l1 "],
+    ["VK_UP", "div.l1 DIV"],
+    ["VK_BACK_SPACE", "div.l1 DI"],
+    ["VK_BACK_SPACE", "div.l1 D"],
+    ["VK_BACK_SPACE", "div.l1 "],
+    ["VK_UP", "div.l1 DIV"],
+    ["VK_UP", "div.l1 DIV"],
+    ["VK_TAB", "div.l1 DIV"],
+    ["VK_BACK_SPACE", "div.l1 DI"],
+    ["VK_BACK_SPACE", "div.l1 D"],
+    ["VK_BACK_SPACE", "div.l1 "],
+    ["VK_DOWN", "div.l1 DIV"],
+    ["VK_DOWN", "div.l1 SPAN"],
+    ["VK_DOWN", "div.l1 SPAN"],
+    ["VK_BACK_SPACE", "div.l1 SPA"],
+    ["VK_BACK_SPACE", "div.l1 SP"],
+    ["VK_BACK_SPACE", "div.l1 S"],
+    ["VK_BACK_SPACE", "div.l1 "],
+    ["VK_BACK_SPACE", "div.l1"],
+    ["VK_BACK_SPACE", "div.l"],
+    ["VK_BACK_SPACE", "div."],
+    ["VK_BACK_SPACE", "div"],
+    ["VK_BACK_SPACE", "di"],
+    ["VK_BACK_SPACE", "d"],
+    ["VK_BACK_SPACE", ""],
+  ];
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(setupTest, content);
+  }, true);
+
+  content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html";
+
+  function $(id) {
+    if (id == null) return null;
+    return content.document.getElementById(id);
+  }
+
+  function setupTest()
+  {
+    openInspector(startTest);
+  }
+
+  function startTest(aInspector)
+  {
+    inspector = aInspector;
+    searchBox =
+      inspector.panelWin.document.getElementById("inspector-searchbox");
+    panel = inspector.searchSuggestions.searchPopup._list;
+
+    focusSearchBoxUsingShortcut(inspector.panelWin, function() {
+      searchBox.addEventListener("keypress", checkState, true);
+      panel.addEventListener("keypress", checkState, true);
+      checkStateAndMoveOn(0);
+    });
+  }
+
+  function checkStateAndMoveOn(index) {
+    if (index == keyStates.length) {
+      finishUp();
+      return;
+    }
+
+    let [key, query] = keyStates[index];
+    state = index;
+
+    info("pressing key " + key + " to get searchbox value as " + query);
+    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+  }
+
+  function checkState(event) {
+    if (panelOpeningStates.indexOf(state) != -1 &&
+        !inspector.searchSuggestions.searchPopup.isOpen) {
+      info("Panel is not open, should wait before it shows up.");
+      panel.parentNode.addEventListener("popupshown", function retry() {
+        panel.parentNode.removeEventListener("popupshown", retry, false);
+        info("Panel is visible now");
+        executeSoon(checkState);
+      }, false);
+      return;
+    }
+    else if (panelClosingStates.indexOf(state) != -1 &&
+             panel.parentNode.state != "closed") {
+      info("Panel is open, should wait for it to close.");
+      panel.parentNode.addEventListener("popuphidden", function retry() {
+        panel.parentNode.removeEventListener("popuphidden", retry, false);
+        info("Panel is hidden now");
+        executeSoon(checkState);
+      }, false);
+      return;
+    }
+
+    // Using setTimout as the "command" event fires at delay after keypress
+    window.setTimeout(function() {
+      let [key, query] = keyStates[state];
+
+      if (searchBox.value == query) {
+        ok(true, "The suggestion at " + state + "th step on " +
+           "pressing " + key + " key is correct.");
+      }
+      else {
+        info("value is not correct, waiting longer for state " + state +
+             " with panel " + panel.parentNode.state);
+        checkState();
+        return;
+      }
+      checkStateAndMoveOn(state + 1);
+    }, 200);
+  }
+
+  function finishUp() {
+    searchBox = null;
+    panel = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
rename from browser/devtools/webconsole/AutocompletePopup.jsm
rename to browser/devtools/shared/AutocompletePopup.jsm
--- a/browser/devtools/webconsole/AutocompletePopup.jsm
+++ b/browser/devtools/shared/AutocompletePopup.jsm
@@ -2,118 +2,148 @@
 /* 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 Cu = Components.utils;
 
 // The XUL and XHTML namespace.
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const XHTML_NS = "http://www.w3.org/1999/xhtml";
-
-const HUD_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
-
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "stringBundle", function () {
-  return Services.strings.createBundle(HUD_STRINGS_URI);
-});
-
-
 this.EXPORTED_SYMBOLS = ["AutocompletePopup"];
 
 /**
  * Autocomplete popup UI implementation.
  *
  * @constructor
  * @param nsIDOMDocument aDocument
  *        The document you want the popup attached to.
+ * @param Object aOptions
+ *        An object consiting any of the following options:
+ *        - panelId {String} The id for the popup panel.
+ *        - listBoxId {String} The id for the richlistbox inside the panel.
+ *        - position {String} The position for the popup panel.
+ *        - theme {String} String related to the theme of the popup.
+ *        - autoSelect {Boolean} Boolean to allow the first entry of the popup
+ *                     panel to be automatically selected when the popup shows.
+ *        - fixedWidth {Boolean} Boolean to control dynamic width of the popup.
+ *        - direction {String} The direction of the text in the panel. rtl or ltr
+ *        - onSelect {String} The select event handler for the richlistbox
+ *        - onClick {String} The click event handler for the richlistbox.
+ *        - onKeypress {String} The keypress event handler for the richlistitems.
  */
-this.AutocompletePopup = function AutocompletePopup(aDocument)
+this.AutocompletePopup =
+function AutocompletePopup(aDocument, aOptions = {})
 {
   this._document = aDocument;
 
+  this.fixedWidth = aOptions.fixedWidth || false;
+  this.autoSelect = aOptions.autoSelect || false;
+  this.position = aOptions.position || "after_start";
+  this.direction = aOptions.direction || "ltr";
+
+  this.onSelect = aOptions.onSelect;
+  this.onClick = aOptions.onClick;
+  this.onKeypress = aOptions.onKeypress;
+
+  let id = aOptions.panelId || "devtools_autoCompletePopup";
+  let theme = aOptions.theme || "dark";
   // Reuse the existing popup elements.
-  this._panel = this._document.getElementById("webConsole_autocompletePopup");
+  this._panel = this._document.getElementById(id);
   if (!this._panel) {
     this._panel = this._document.createElementNS(XUL_NS, "panel");
-    this._panel.setAttribute("id", "webConsole_autocompletePopup");
-    this._panel.setAttribute("label",
-      stringBundle.GetStringFromName("Autocomplete.label"));
+    this._panel.setAttribute("id", id);
+    this._panel.className = "devtools-autocomplete-popup " + theme + "-theme";
+
     this._panel.setAttribute("noautofocus", "true");
-    this._panel.setAttribute("ignorekeys", "true");
     this._panel.setAttribute("level", "top");
+    if (!aOptions.onKeypress) {
+      this._panel.setAttribute("ignorekeys", "true");
+    }
 
     let mainPopupSet = this._document.getElementById("mainPopupSet");
     if (mainPopupSet) {
       mainPopupSet.appendChild(this._panel);
     }
     else {
       this._document.documentElement.appendChild(this._panel);
     }
+    this._list = null;
+  }
+  else {
+    this._list = this._panel.firstChild;
+  }
 
+  if (!this._list) {
     this._list = this._document.createElementNS(XUL_NS, "richlistbox");
-    this._list.flex = 1;
     this._panel.appendChild(this._list);
 
     // Open and hide the panel, so we initialize the API of the richlistbox.
-    this._panel.width = 1;
-    this._panel.height = 1;
-    this._panel.openPopup(null, "overlap", 0, 0, false, false);
+    this._panel.openPopup(null, this.popup, 0, 0);
     this._panel.hidePopup();
-    this._panel.width = "";
-    this._panel.height = "";
+  }
+
+  this._list.flex = 1;
+  this._list.setAttribute("seltype", "single");
+
+  if (aOptions.listBoxId) {
+    this._list.setAttribute("id", aOptions.listBoxId);
   }
-  else {
-    this._list = this._panel.firstChild;
+  this._list.className = "devtools-autocomplete-listbox " + theme + "-theme";
+
+  if (this.onSelect) {
+    this._list.addEventListener("select", this.onSelect, false);
+  }
+
+  if (this.onClick) {
+    this._list.addEventListener("click", this.onClick, false);
+  }
+
+  if (this.onKeypress) {
+    this._list.addEventListener("keypress", this.onKeypress, false);
   }
 }
 
 AutocompletePopup.prototype = {
   _document: null,
   _panel: null,
   _list: null,
 
+  // Event handlers.
+  onSelect: null,
+  onClick: null,
+  onKeypress: null,
+
   /**
    * Open the autocomplete popup panel.
    *
    * @param nsIDOMNode aAnchor
    *        Optional node to anchor the panel to.
    */
   openPopup: function AP_openPopup(aAnchor)
   {
-    this._panel.openPopup(aAnchor, "after_start", 0, 0, false, false);
-
-    if (this.onSelect) {
-      this._list.addEventListener("select", this.onSelect, false);
-    }
+    this._panel.openPopup(aAnchor, this.position, 0, 0);
 
-    if (this.onClick) {
-      this._list.addEventListener("click", this.onClick, false);
+    if (this.autoSelect) {
+      this.selectFirstItem();
     }
-
-    this._updateSize();
+    if (!this.fixedWidth) {
+      this._updateSize();
+    }
   },
 
   /**
    * Hide the autocomplete popup panel.
    */
   hidePopup: function AP_hidePopup()
   {
     this._panel.hidePopup();
-
-    if (this.onSelect) {
-      this._list.removeEventListener("select", this.onSelect, false);
-    }
-
-    if (this.onClick) {
-      this._list.removeEventListener("click", this.onClick, false);
-    }
   },
 
   /**
    * Check if the autocomplete popup is open.
    */
   get isOpen() {
     return this._panel.state == "open";
   },
@@ -126,24 +156,48 @@ AutocompletePopup.prototype = {
    */
   destroy: function AP_destroy()
   {
     if (this.isOpen) {
       this.hidePopup();
     }
     this.clearItems();
 
+    if (this.onSelect) {
+      this._list.removeEventListener("select", this.onSelect, false);
+    }
+
+    if (this.onClick) {
+      this._list.removeEventListener("click", this.onClick, false);
+    }
+
+    if (this.onKeypress) {
+      this._list.removeEventListener("keypress", this.onKeypress, false);
+    }
+
     this._document = null;
     this._list = null;
     this._panel = null;
   },
 
   /**
    * Get the autocomplete items array.
    *
+   * @param Number aIndex The index of the item what is wanted.
+   *
+   * @return The autocomplete item at index aIndex.
+   */
+  getItemAtIndex: function AP_getItemAtIndex(aIndex)
+  {
+    return this._list.getItemAtIndex(aIndex)._autocompleteItem;
+  },
+
+  /**
+   * Get the autocomplete items array.
+   *
    * @return array
    *         The array of autocomplete items.
    */
   getItems: function AP_getItems()
   {
     let items = [];
 
     Array.forEach(this._list.childNodes, function(aItem) {
@@ -161,56 +215,88 @@ AutocompletePopup.prototype = {
    */
   setItems: function AP_setItems(aItems)
   {
     this.clearItems();
     aItems.forEach(this.appendItem, this);
 
     // Make sure that the new content is properly fitted by the XUL richlistbox.
     if (this.isOpen) {
-      // We need the timeout to allow the content to reflow. Attempting to
-      // update the richlistbox size too early does not work.
-      this._document.defaultView.setTimeout(this._updateSize.bind(this), 1);
+      if (this.autoSelect) {
+        this.selectFirstItem();
+      }
+      if (!this.fixedWidth) {
+        this._updateSize();
+      }
+    }
+  },
+
+  /**
+   * Selects the first item of the richlistbox. Note that first item here is the
+   * item closes to the input element, which means that 0th index if position is
+   * below, and last index if position is above.
+   */
+  selectFirstItem: function AP_selectFirstItem()
+  {
+    if (this.position.contains("before")) {
+      this.selectedIndex = this.itemCount - 1;
+    }
+    else {
+      this.selectedIndex = 0;
     }
   },
 
   /**
    * Update the panel size to fit the content.
    *
    * @private
    */
   _updateSize: function AP__updateSize()
   {
-    if (!this._panel) {
-      return;
-    }
-    this._list.width = this._panel.clientWidth +
-                       this._scrollbarWidth;
+    // We need the timeout to allow the content to reflow. Attempting to
+    // update the richlistbox size too early does not work.
+    this._document.defaultView.setTimeout(function() {
+      if (!this._panel) {
+        return;
+      }
+      this._list.width = this._panel.clientWidth +
+                         this._scrollbarWidth;
+      // Height change is required, otherwise the panel is drawn at an offset
+      // the first time.
+      this._list.height = this._panel.clientHeight;
+      // This brings the panel back at right position.
+      this._list.top = 0;
+      // Changing panel height might make the selected item out of view, so
+      // bring it back to view.
+      this._list.ensureIndexIsVisible(this._list.selectedIndex);
+    }.bind(this), 5);
   },
 
   /**
    * Clear all the items from the autocomplete list.
    */
   clearItems: function AP_clearItems()
   {
     // Reset the selectedIndex to -1 before clearing the list
     this.selectedIndex = -1;
 
     while (this._list.hasChildNodes()) {
       this._list.removeChild(this._list.firstChild);
     }
 
-    // Reset the panel and list dimensions. New dimensions are calculated when a
-    // new set of items is added to the autocomplete popup.
-    this._list.width = "";
-    this._list.height = "";
-    this._panel.width = "";
-    this._panel.height = "";
-    this._panel.top = "";
-    this._panel.left = "";
+    if (!this.fixedWidth) {
+      // Reset the panel and list dimensions. New dimensions are calculated when
+      // a new set of items is added to the autocomplete popup.
+      this._list.width = "";
+      this._list.height = "";
+      this._panel.width = "";
+      this._panel.height = "";
+      this._panel.top = "";
+      this._panel.left = "";
+    }
   },
 
   /**
    * Getter for the index of the selected item.
    *
    * @type number
    */
   get selectedIndex() {
@@ -220,17 +306,17 @@ AutocompletePopup.prototype = {
   /**
    * Setter for the selected index.
    *
    * @param number aIndex
    *        The number (index) of the item you want to select in the list.
    */
   set selectedIndex(aIndex) {
     this._list.selectedIndex = aIndex;
-    if (this._list.ensureIndexIsVisible) {
+    if (this.isOpen && this._list.ensureIndexIsVisible) {
       this._list.ensureIndexIsVisible(this._list.selectedIndex);
     }
   },
 
   /**
    * Getter for the selected item.
    * @type object
    */
@@ -242,33 +328,61 @@ AutocompletePopup.prototype = {
   /**
    * Setter for the selected item.
    *
    * @param object aItem
    *        The object you want selected in the list.
    */
   set selectedItem(aItem) {
     this._list.selectedItem = this._findListItem(aItem);
-    this._list.ensureIndexIsVisible(this._list.selectedIndex);
+    if (this.isOpen) {
+      this._list.ensureIndexIsVisible(this._list.selectedIndex);
+    }
   },
 
   /**
    * Append an item into the autocomplete list.
    *
    * @param object aItem
-   *        The item you want appended to the list. The object must have a
-   *        "label" property which is used as the displayed value.
+   *        The item you want appended to the list.
+   *        The item object can have the following properties:
+   *        - label {String} Property which is used as the displayed value.
+   *        - preLabel {String} [Optional] The String that will be displayed
+   *                   before the label indicating that this is the already
+   *                   present text in the input box, and label is the text
+   *                   that will be auto completed. When this property is
+   *                   present, |preLabel.length| starting characters will be
+   *                   removed from label.
+   *        - count {Number} [Optional] The number to represent the count of
+   *                autocompleted label.
    */
   appendItem: function AP_appendItem(aItem)
   {
-    let description = this._document.createElementNS(XUL_NS, "description");
-    description.textContent = aItem.label;
-
     let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
-    listItem.appendChild(description);
+    if (this.direction) {
+      listItem.setAttribute("dir", this.direction);
+    }
+    let label = this._document.createElementNS(XUL_NS, "label");
+    label.setAttribute("value", aItem.label);
+    label.setAttribute("class", "autocomplete-value");
+    if (aItem.preLabel) {
+      let preDesc = this._document.createElementNS(XUL_NS, "label");
+      preDesc.setAttribute("value", aItem.preLabel);
+      preDesc.setAttribute("class", "initial-value");
+      listItem.appendChild(preDesc);
+      label.setAttribute("value", aItem.label.slice(aItem.preLabel.length));
+    }
+    listItem.appendChild(label);
+    if (aItem.count && aItem.count > 1) {
+      let countDesc = this._document.createElementNS(XUL_NS, "label");
+      countDesc.setAttribute("value", aItem.count);
+      countDesc.setAttribute("flex", "1");
+      countDesc.setAttribute("class", "autocomplete-count");
+      listItem.appendChild(countDesc);
+    }
     listItem._autocompleteItem = aItem;
 
     this._list.appendChild(listItem);
   },
 
   /**
    * Find the richlistitem element that belongs to an item.
    *
@@ -347,16 +461,24 @@ AutocompletePopup.prototype = {
     else {
       this.selectedIndex = this.itemCount - 1;
     }
 
     return this.selectedItem;
   },
 
   /**
+   * Focuses the richlistbox.
+   */
+  focus: function AP_focus()
+  {
+    this._list.focus();
+  },
+
+  /**
    * Determine the scrollbar width in the current document.
    *
    * @private
    */
   get _scrollbarWidth()
   {
     if (this.__scrollbarWidth) {
       return this.__scrollbarWidth;
--- a/browser/devtools/webconsole/Makefile.in
+++ b/browser/devtools/webconsole/Makefile.in
@@ -8,13 +8,12 @@ srcdir		= @srcdir@
 VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
 		HUDService.jsm \
 		PropertyPanel.jsm \
 		NetworkPanel.jsm \
-		AutocompletePopup.jsm \
 		WebConsolePanel.jsm \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js
@@ -36,17 +36,17 @@ function consoleOpened(aHud) {
     ok(popup.isOpen, "popup is open");
 
     // 4 values, and the following properties:
     // __defineGetter__  __defineSetter__ __lookupGetter__ __lookupSetter__
     // hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString
     // toSource unwatch valueOf watch constructor.
     is(popup.itemCount, 18, "popup.itemCount is correct");
 
-    let sameItems = popup.getItems().map(function(e) {return e.label;});
+    let sameItems = popup.getItems().reverse().map(function(e) {return e.label;});
     ok(sameItems.every(function(prop, index) {
       return [
         "__defineGetter__",
         "__defineSetter__",
         "__lookupGetter__",
         "__lookupSetter__",
         "constructor",
         "hasOwnProperty",
@@ -59,39 +59,41 @@ function consoleOpened(aHud) {
         "toLocaleString",
         "toSource",
         "toString",
         "unwatch",
         "valueOf",
         "watch",
       ][index] === prop}), "getItems returns the items we expect");
 
-    is(popup.selectedIndex, -1, "no index is selected");
+    is(popup.selectedIndex, 17,
+       "Index of the first item from bottom is selected.");
+    EventUtils.synthesizeKey("VK_DOWN", {});
     EventUtils.synthesizeKey("VK_DOWN", {});
 
     let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
 
     is(popup.selectedIndex, 0, "index 0 is selected");
-    is(popup.selectedItem.label, "__defineGetter__", "__defineGetter__ is selected");
-    is(completeNode.value, prefix + "__defineGetter__",
-        "completeNode.value holds __defineGetter__");
+    is(popup.selectedItem.label, "watch", "watch is selected");
+    is(completeNode.value, prefix + "watch",
+        "completeNode.value holds watch");
 
     EventUtils.synthesizeKey("VK_DOWN", {});
 
     is(popup.selectedIndex, 1, "index 1 is selected");
-    is(popup.selectedItem.label, "__defineSetter__", "__defineSetter__ is selected");
-    is(completeNode.value, prefix + "__defineSetter__",
-        "completeNode.value holds __defineSetter__");
+    is(popup.selectedItem.label, "valueOf", "valueOf is selected");
+    is(completeNode.value, prefix + "valueOf",
+        "completeNode.value holds valueOf");
 
     EventUtils.synthesizeKey("VK_UP", {});
 
     is(popup.selectedIndex, 0, "index 0 is selected");
-    is(popup.selectedItem.label, "__defineGetter__", "__defineGetter__ is selected");
-    is(completeNode.value, prefix + "__defineGetter__",
-        "completeNode.value holds __defineGetter__");
+    is(popup.selectedItem.label, "watch", "watch is selected");
+    is(completeNode.value, prefix + "watch",
+        "completeNode.value holds watch");
 
     popup._panel.addEventListener("popuphidden", autocompletePopupHidden, false);
 
     EventUtils.synthesizeKey("VK_TAB", {});
   }, false);
 
   jsterm.setInputValue("window.foobarBug585991");
   EventUtils.synthesizeKey(".", {});
@@ -103,37 +105,38 @@ function autocompletePopupHidden()
   let popup = jsterm.autocompletePopup;
   let completeNode = jsterm.completeNode;
   let inputNode = jsterm.inputNode;
 
   popup._panel.removeEventListener("popuphidden", autocompletePopupHidden, false);
 
   ok(!popup.isOpen, "popup is not open");
 
-  is(inputNode.value, "window.foobarBug585991.__defineGetter__",
+  is(inputNode.value, "window.foobarBug585991.watch",
      "completion was successful after VK_TAB");
 
   ok(!completeNode.value, "completeNode is empty");
 
   popup._panel.addEventListener("popupshown", function onShown() {
     popup._panel.removeEventListener("popupshown", onShown, false);
 
     ok(popup.isOpen, "popup is open");
 
     is(popup.itemCount, 18, "popup.itemCount is correct");
 
-    is(popup.selectedIndex, -1, "no index is selected");
+    is(popup.selectedIndex, 17, "First index from bottom is selected");
+    EventUtils.synthesizeKey("VK_DOWN", {});
     EventUtils.synthesizeKey("VK_DOWN", {});
 
     let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
 
     is(popup.selectedIndex, 0, "index 0 is selected");
-    is(popup.selectedItem.label, "__defineGetter__", "__defineGetter__ is selected");
-    is(completeNode.value, prefix + "__defineGetter__",
-        "completeNode.value holds __defineGetter__");
+    is(popup.selectedItem.label, "watch", "watch is selected");
+    is(completeNode.value, prefix + "watch",
+        "completeNode.value holds watch");
 
     popup._panel.addEventListener("popuphidden", function onHidden() {
       popup._panel.removeEventListener("popuphidden", onHidden, false);
 
       ok(!popup.isOpen, "popup is not open after VK_ESCAPE");
 
       is(inputNode.value, "window.foobarBug585991.",
          "completion was cancelled");
@@ -163,39 +166,40 @@ function testReturnKey()
 
   popup._panel.addEventListener("popupshown", function onShown() {
     popup._panel.removeEventListener("popupshown", onShown, false);
 
     ok(popup.isOpen, "popup is open");
 
     is(popup.itemCount, 18, "popup.itemCount is correct");
 
-    is(popup.selectedIndex, -1, "no index is selected");
+    is(popup.selectedIndex, 17, "First index from bottom is selected");
+    EventUtils.synthesizeKey("VK_DOWN", {});
     EventUtils.synthesizeKey("VK_DOWN", {});
 
     let prefix = jsterm.inputNode.value.replace(/[\S]/g, " ");
 
     is(popup.selectedIndex, 0, "index 0 is selected");
-    is(popup.selectedItem.label, "__defineGetter__", "__defineGetter__ is selected");
-    is(completeNode.value, prefix + "__defineGetter__",
-        "completeNode.value holds __defineGetter__");
+    is(popup.selectedItem.label, "watch", "watch is selected");
+    is(completeNode.value, prefix + "watch",
+        "completeNode.value holds watch");
 
     EventUtils.synthesizeKey("VK_DOWN", {});
 
     is(popup.selectedIndex, 1, "index 1 is selected");
-    is(popup.selectedItem.label, "__defineSetter__", "__defineSetter__ is selected");
-    is(completeNode.value, prefix + "__defineSetter__",
-        "completeNode.value holds __defineSetter__");
+    is(popup.selectedItem.label, "valueOf", "valueOf is selected");
+    is(completeNode.value, prefix + "valueOf",
+        "completeNode.value holds valueOf");
 
     popup._panel.addEventListener("popuphidden", function onHidden() {
       popup._panel.removeEventListener("popuphidden", onHidden, false);
 
       ok(!popup.isOpen, "popup is not open after VK_RETURN");
 
-      is(inputNode.value, "window.foobarBug585991.__defineSetter__",
+      is(inputNode.value, "window.foobarBug585991.valueOf",
          "completion was successful after VK_RETURN");
 
       ok(!completeNode.value, "completeNode is empty");
 
       dontShowArrayNumbers();
     }, false);
 
     EventUtils.synthesizeKey("VK_RETURN", {});
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
@@ -35,18 +35,19 @@ function consoleOpened(HUD) {
 
     is(popup.itemCount, items.length, "items added");
 
     let sameItems = popup.getItems();
     is(sameItems.every(function(aItem, aIndex) {
       return aItem === items[aIndex];
     }), true, "getItems returns back the same items");
 
-    is(popup.selectedIndex, -1, "no index is selected");
-    ok(!popup.selectedItem, "no item is selected");
+    is(popup.selectedIndex, 2,
+       "Index of the first item from bottom is selected.");
+    is(popup.selectedItem, items[2], "First item from bottom is selected");
 
     popup.selectedIndex = 1;
 
     is(popup.selectedIndex, 1, "index 1 is selected");
     is(popup.selectedItem, items[1], "item1 is selected");
 
     popup.selectedItem = items[2];
 
--- a/browser/devtools/webconsole/test/browser_webconsole_completion.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_completion.js
@@ -61,31 +61,31 @@ function testCompletion(hud) {
 
   // Test typing 'document.getElem'.
   input.value = "document.getElem";
   input.setSelectionRange(16, 16);
   jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
   yield;
 
   is(input.value, "document.getElem", "'document.getElem' completion");
-  is(jsterm.completeNode.value, "                entById", "'document.getElem' completion");
+  is(jsterm.completeNode.value, "", "'document.getElem' completion");
 
   // Test pressing tab another time.
   jsterm.complete(jsterm.COMPLETE_FORWARD, testNext);
   yield;
 
   is(input.value, "document.getElem", "'document.getElem' completion");
-  is(jsterm.completeNode.value, "                entsByClassName", "'document.getElem' another tab completion");
+  is(jsterm.completeNode.value, "                entsByTagNameNS", "'document.getElem' another tab completion");
 
   // Test pressing shift_tab.
   jsterm.complete(jsterm.COMPLETE_BACKWARD, testNext);
   yield;
 
   is(input.value, "document.getElem", "'document.getElem' untab completion");
-  is(jsterm.completeNode.value, "                entById", "'document.getElem' completion");
+  is(jsterm.completeNode.value, "", "'document.getElem' completion");
 
   jsterm.clearOutput();
 
   input.value = "docu";
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
   yield;
 
   is(jsterm.completeNode.value, "    ment", "'docu' completion");
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -24,17 +24,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 XPCOMUtils.defineLazyModuleGetter(this, "PropertyTreeView",
                                   "resource:///modules/PropertyPanel.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetworkPanel",
                                   "resource:///modules/NetworkPanel.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup",
-                                  "resource:///modules/AutocompletePopup.jsm");
+                                  "resource:///modules/devtools/AutocompletePopup.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
                                   "resource://gre/modules/devtools/WebConsoleUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
@@ -2726,19 +2726,28 @@ JSTerm.prototype = {
   COMPLETE_HINT_ONLY: 2,
 
   /**
    * Initialize the JSTerminal UI.
    */
   init: function JST_init()
   {
     let chromeDocument = this.hud.owner.chromeDocument;
-    this.autocompletePopup = new AutocompletePopup(chromeDocument);
-    this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this);
-    this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this);
+    let autocompleteOptions = {
+      onSelect: this.onAutocompleteSelect.bind(this),
+      onClick: this.acceptProposedCompletion.bind(this),
+      panelId: "webConsole_autocompletePopup",
+      listBoxId: "webConsole_autocompletePopupListBox",
+      position: "before_start",
+      theme: "light",
+      direction: "ltr",
+      autoSelect: true
+    };
+    this.autocompletePopup = new AutocompletePopup(chromeDocument,
+                                                   autocompleteOptions);
 
     let doc = this.hud.document;
     this.completeNode = doc.querySelector(".jsterm-complete-node");
     this.inputNode = doc.querySelector(".jsterm-input-node");
     this.inputNode.addEventListener("keypress", this._keyPress, false);
     this.inputNode.addEventListener("input", this._inputEventHandler, false);
     this.inputNode.addEventListener("keyup", this._inputEventHandler, false);
 
@@ -3474,32 +3483,33 @@ JSTerm.prototype = {
     let inputNode = this.inputNode;
     let inputValue = inputNode.value;
     if (this.lastCompletion.value == inputValue ||
         aRequestId != this.lastCompletion.requestId) {
       return;
     }
 
     let matches = aMessage.matches;
+    let lastPart = aMessage.matchProp;
     if (!matches.length) {
       this.clearCompletion();
       return;
     }
 
-    let items = matches.map(function(aMatch) {
-      return { label: aMatch };
+    let items = matches.reverse().map(function(aMatch) {
+      return { preLabel: lastPart, label: aMatch };
     });
 
     let popup = this.autocompletePopup;
     popup.setItems(items);
 
     let completionType = this.lastCompletion.completionType;
     this.lastCompletion = {
       value: inputValue,
-      matchProp: aMessage.matchProp,
+      matchProp: lastPart,
     };
 
     if (items.length > 1 && !popup.isOpen) {
       popup.openPopup(inputNode);
     }
     else if (items.length < 2 && popup.isOpen) {
       popup.hidePopup();
     }
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -105,20 +105,16 @@ webConsoleMixedContentWarning=Mixed Cont
 # JavaScript is being entered, to indicate how to jump into scratchpad mode
 scratchpad.linkText=Shift+RETURN - Open in Scratchpad
 
 # LOCALIZATION NOTE (gcliterm.instanceLabel): The console displays
 # objects using their type (from the constructor function) in this descriptive
 # string
 gcliterm.instanceLabel=Instance of %S
 
-# LOCALIZATION NOTE (Autocomplete.label):
-# The autocomplete popup panel label/title.
-Autocomplete.label=Autocomplete popup
-
 # LOCALIZATION NOTE (stacktrace.anonymousFunction):
 # This string is used to display JavaScript functions that have no given name -
 # they are said to be anonymous. See stacktrace.outputMessage.
 stacktrace.anonymousFunction=<anonymous>
 
 # LOCALIZATION NOTE (stacktrace.outputMessage):
 # This string is used in the Web Console output to identify a web developer call
 # to console.trace(). The stack trace of JavaScript function calls is displayed.
--- a/browser/themes/linux/devtools/common.css
+++ b/browser/themes/linux/devtools/common.css
@@ -214,16 +214,20 @@
   width: 3px;
   background-color: transparent;
   -moz-margin-end: -3px;
   position: relative;
 }
 
 /* In-tools sidebar */
 
+.devtools-toolbox-side-iframe {
+  min-width: 465px;
+}
+
 .devtools-sidebar-tabs {
   -moz-appearance: none;
   margin: 0;
 }
 
 .devtools-sidebar-tabs > tabpanels {
   -moz-appearance: none;
   padding: 0;
@@ -355,8 +359,10 @@
 
 .devtools-theme-attrname {
   color: hsl(208,56%,40%); /* blue */
 }
 
 .devtools-theme-attrvalue {
   color: hsl(24,85%,39%); /* orange */
 }
+
+%include ../../shared/devtools/common.inc.css
--- a/browser/themes/linux/devtools/toolbox.css
+++ b/browser/themes/linux/devtools/toolbox.css
@@ -145,48 +145,48 @@
 }
 
 #toolbox-tabs {
   margin: 0;
 }
 
 .devtools-tab {
   -moz-appearance: none;
-  min-width: 88px;
+  width: 47px;
+  min-width: 47px;
   min-height: 32px;
+  max-width: 137px;
   color: #b6babf;
   margin: 0;
   padding: 0;
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1));
   background-size: 1px 100%;
   background-repeat: no-repeat;
   background-position: left, right;
   border-right: 1px solid hsla(206,37%,4%,.45);
 }
 
-.devtools-tab > .radio-label-center-box > .radio-label-box {
-  -moz-appearance: none;
+.devtools-tab > image {
   border: none;
-  padding: 0 16px;
-}
-
-.devtools-tab > .radio-label-center-box >.radio-label-box > .radio-icon {
   -moz-margin-end: 6px;
+  -moz-margin-start: 16px;
   opacity: 0.6;
 }
 
-.devtools-tab:hover > .radio-label-center-box > .radio-label-box >
-.radio-icon {
+.devtools-tab > label {
+  white-space: nowrap;
+}
+
+.devtools-tab:hover > image {
   opacity: 0.8;
 }
 
-.devtools-tab:active > .radio-label-center-box > .radio-label-box > .radio-icon,
-.devtools-tab[selected=true] > .radio-label-center-box > .radio-label-box >
-.radio-icon {
+.devtools-tab:active > image,
+.devtools-tab[selected=true] > label {
   opacity: 1;
 }
 
 .devtools-tab:hover {
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(206,37%,4%,.1), hsla(206,37%,4%,.2));
   background-size: 1px 100%,
--- a/browser/themes/osx/devtools/common.css
+++ b/browser/themes/osx/devtools/common.css
@@ -228,16 +228,20 @@
   min-width: 0;
   width: 3px;
   -moz-margin-end: -3px;
   position: relative;
 }
 
 /* In-tools sidebar */
 
+.devtools-toolbox-side-iframe {
+  min-width: 465px;
+}
+
 .devtools-sidebar-tabs {
   -moz-appearance: none;
   margin: 0;
 }
 
 .devtools-sidebar-tabs > tabpanels {
   padding: 0;
 }
@@ -374,8 +378,10 @@
 
 .devtools-theme-attrname {
   color: hsl(208,56%,40%); /* blue */
 }
 
 .devtools-theme-attrvalue {
   color: hsl(24,85%,39%); /* orange */
 }
+
+%include ../../shared/devtools/common.inc.css
--- a/browser/themes/osx/devtools/toolbox.css
+++ b/browser/themes/osx/devtools/toolbox.css
@@ -134,44 +134,47 @@
 
 #toolbox-tabs {
   margin: 0;
   border-left: 1px solid hsla(206,37%,4%,.45);
 }
 
 .devtools-tab {
   -moz-appearance: none;
-  min-width: 88px;
+  width: 47px;
+  min-width: 47px;
   min-height: 32px;
+  max-width: 137px;
   color: #b6babf;
   margin: 0;
   padding: 0;
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1));
   background-size: 1px 100%;
   background-repeat: no-repeat;
   background-position: left, right;
   border-right: 1px solid hsla(206,37%,4%,.45);
 }
 
-.devtools-tab > .radio-label-box {
-  padding: 0 16px;
-}
-
-.devtools-tab > .radio-label-box > .radio-icon {
+.devtools-tab > image {
   -moz-margin-end: 6px;
+  -moz-margin-start: 16px;
   opacity: 0.6;
 }
 
-.devtools-tab:hover > .radio-label-box > .radio-icon {
+.devtools-tab > label {
+  white-space: nowrap;
+}
+
+.devtools-tab:hover > image {
   opacity: 0.8;
 }
 
-.devtools-tab:active > .radio-label-box > .radio-icon,
-.devtools-tab[selected=true] > .radio-label-box > .radio-icon {
+.devtools-tab:active > image,
+.devtools-tab[selected=true] > image {
   opacity: 1;
 }
 
 .devtools-tab:hover {
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(206,37%,4%,.1), hsla(206,37%,4%,.2));
   background-size: 1px 100%,
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devtools/common.inc.css
@@ -0,0 +1,85 @@
+%if 0
+/* 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/. */
+%endif
+
+/* Autocomplete Popup */
+/* Dark and light theme */
+
+.devtools-autocomplete-popup {
+  -moz-appearance: none !important;
+  border: 1px solid hsl(210,11%,10%);
+  box-shadow: 0 1px 0 hsla(209,29%,72%,.25) inset;
+  background-color: transparent;
+  background-image: linear-gradient(to bottom, hsla(209,18%,18%,0.9), hsl(210,11%,16%));
+  border-radius: 3px;
+%ifdef XP_LINUX
+  max-height: 32rem;
+%else
+  max-height: 40rem;
+%endif
+}
+
+.devtools-autocomplete-listbox {
+  -moz-appearance: none !important;
+  background-color: transparent;
+  border-width: 0px !important;
+}
+
+.devtools-autocomplete-listbox > richlistitem,
+.devtools-autocomplete-listbox > richlistitem[selected] {
+  width: 100%;
+  background-color: transparent;
+  border-radius: 4px;
+}
+
+.devtools-autocomplete-listbox.dark-theme > richlistitem[selected],
+.devtools-autocomplete-listbox.dark-theme > richlistitem:hover {
+  background-color: rgba(0,0,0,0.5);
+}
+
+.devtools-autocomplete-listbox.dark-theme > richlistitem[selected] > .autocomplete-value,
+.devtools-autocomplete-listbox:focus.dark-theme > richlistitem[selected] > .initial-value {
+  color: hsl(208,100%,60%);
+}
+
+.devtools-autocomplete-listbox.dark-theme > richlistitem[selected] > label {
+  color: #eee;
+}
+
+.devtools-autocomplete-listbox.dark-theme > richlistitem > label {
+  color: #ccc;
+}
+
+.devtools-autocomplete-listbox > richlistitem > .initial-value,
+.devtools-autocomplete-listbox > richlistitem > .autocomplete-value {
+  margin: 0;
+  padding: 1px 0;
+}
+
+.devtools-autocomplete-listbox > richlistitem > .autocomplete-count {
+  text-align: right;
+}
+
+/* Rest of the light theme */
+
+.devtools-autocomplete-popup.light-theme {
+  border: 1px solid hsl(210,24%,90%);
+  box-shadow: 0 1px 0 hsla(209,29%,90%,.25) inset;
+  background-image: linear-gradient(to bottom, hsla(209,18%,100%,0.9), hsl(210,24%,95%));
+}
+
+.devtools-autocomplete-listbox.light-theme > richlistitem[selected],
+.devtools-autocomplete-listbox.light-theme > richlistitem:hover {
+  background-color: rgba(128,128,128,0.3);
+}
+
+.devtools-autocomplete-listbox.light-theme > richlistitem[selected] > .autocomplete-value,
+.devtools-autocomplete-listbox:focus.light-theme > richlistitem[selected] > .initial-value {
+  color: #222;
+}
+
+.devtools-autocomplete-listbox.light-theme > richlistitem > label {
+  color: #666;
+}
--- a/browser/themes/windows/devtools/common.css
+++ b/browser/themes/windows/devtools/common.css
@@ -234,16 +234,20 @@
   width: 3px;
   background-color: transparent;
   -moz-margin-end: -3px;
   position: relative;
 }
 
 /* In-tools sidebar */
 
+.devtools-toolbox-side-iframe {
+  min-width: 465px;
+}
+
 .devtools-sidebar-tabs {
   -moz-appearance: none;
   margin: 0;
 }
 
 .devtools-sidebar-tabs > tabpanels {
   -moz-appearance: none;
   padding: 0;
@@ -383,8 +387,10 @@
 
 .devtools-theme-attrname {
   color: hsl(208,56%,40%); /* blue */
 }
 
 .devtools-theme-attrvalue {
   color: hsl(24,85%,39%); /* orange */
 }
+
+%include ../../shared/devtools/common.inc.css
--- a/browser/themes/windows/devtools/toolbox.css
+++ b/browser/themes/windows/devtools/toolbox.css
@@ -146,46 +146,44 @@
 }
 
 #toolbox-tabs {
   margin: 0;
 }
 
 .devtools-tab {
   -moz-appearance: none;
-  min-width: 88px;
+  width: 47px;
+  min-width: 47px;
   min-height: 32px;
+  max-width: 137px;
   color: #b6babf;
   margin: 0;
   padding: 0;
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1));
   background-size: 1px 100%;
   background-repeat: no-repeat;
   background-position: left, right;
   border-top: 1px solid #060a0d;
   border-right: 1px solid hsla(206,37%,4%,.45);
 }
 
-.devtools-tab > .radio-label-box {
-  border: none;
-  padding: 0 16px;
-}
-
-.devtools-tab > .radio-label-box > .radio-icon {
+.devtools-tab > image {
   -moz-margin-end: 6px;
+  -moz-margin-start: 16px;
   opacity: 0.6;
 }
 
-.devtools-tab:hover > .radio-label-box > .radio-icon {
+.devtools-tab:hover > image {
   opacity: 0.8;
 }
 
-.devtools-tab:active > .radio-label-box > .radio-icon,
-.devtools-tab[selected=true] > .radio-label-box > .radio-icon {
+.devtools-tab:active > image,
+.devtools-tab[selected=true] > image {
   opacity: 1;
 }
 
 .devtools-tab:hover {
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(206,37%,4%,.1), hsla(206,37%,4%,.2));
   background-size: 1px 100%,
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_utils_exceptionStr.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_exceptionStr_non_exceptions() {
+  do_check_eq(CommonUtils.exceptionStr(null), "null");
+  do_check_eq(CommonUtils.exceptionStr(false), "false");
+  do_check_eq(CommonUtils.exceptionStr(undefined), "undefined");
+  do_check_eq(CommonUtils.exceptionStr(12), "12 No traceback available");
+
+  run_next_test();
+});
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -8,16 +8,17 @@ firefox-appdir = browser
 
 [test_utils_atob.js]
 [test_utils_convert_string.js]
 [test_utils_dateprefs.js]
 [test_utils_deepCopy.js]
 [test_utils_encodeBase32.js]
 [test_utils_encodeBase64URL.js]
 [test_utils_ensureMillisecondsTimestamp.js]
+[test_utils_exceptionStr.js]
 [test_utils_json.js]
 [test_utils_makeURI.js]
 [test_utils_namedTimer.js]
 [test_utils_stackTrace.js]
 [test_utils_utf8.js]
 [test_utils_uuid.js]
 
 [test_async_chain.js]
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -8,16 +8,19 @@ this.EXPORTED_SYMBOLS = ["CommonUtils"];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm")
 Cu.import("resource://services-common/log4moz.js");
 
 this.CommonUtils = {
   exceptionStr: function exceptionStr(e) {
+    if (!e) {
+      return "" + e;
+    }
     let message = e.message ? e.message : e;
     return message + " " + CommonUtils.stackTrace(e);
   },
 
   stackTrace: function stackTrace(e) {
     // Wrapped nsIException
     if (e.location) {
       let frame = e.location;
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -23,16 +23,18 @@ Cu.import("resource://services-common/pr
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
+                                  "resource://gre/modules/UpdateChannel.jsm");
 
 // Oldest year to allow in date preferences. This module was implemented in
 // 2012 and no dates older than that should be encountered.
 const OLDEST_ALLOWED_YEAR = 2012;
 
 const DAYS_IN_PAYLOAD = 180;
 
 const DEFAULT_DATABASE_NAME = "healthreport.sqlite";
@@ -606,18 +608,19 @@ AbstractHealthReporter.prototype = Objec
     return deferred.promise;
   },
 
   _getJSONPayload: function (now, asObject=false) {
     let pingDateString = this._formatDate(now);
     this._log.info("Producing JSON payload for " + pingDateString);
 
     let o = {
-      version: 1,
+      version: 2,
       thisPingDate: pingDateString,
+      geckoAppInfo: this.obtainAppInfo(this._log),
       data: {last: {}, days: {}},
     };
 
     let outputDataDays = o.data.days;
 
     // Guard here in case we don't track this (e.g., on Android).
     let lastPingDate = this.lastPingDate;
     if (lastPingDate && lastPingDate.getTime() > 0) {
@@ -779,16 +782,62 @@ AbstractHealthReporter.prototype = Objec
         return Promise.reject(error);
       }
     );
   },
 
   _now: function _now() {
     return new Date();
   },
+
+  // These are stolen from AppInfoProvider.
+  appInfoVersion: 1,
+  appInfoFields: {
+    // From nsIXULAppInfo.
+    vendor: "vendor",
+    name: "name",
+    id: "ID",
+    version: "version",
+    appBuildID: "appBuildID",
+    platformVersion: "platformVersion",
+    platformBuildID: "platformBuildID",
+
+    // From nsIXULRuntime.
+    os: "OS",
+    xpcomabi: "XPCOMABI",
+  },
+
+  /**
+   * Statically return a bundle of app info data, a subset of that produced by
+   * AppInfoProvider._populateConstants. This allows us to more usefully handle
+   * payloads that, due to error, contain no data.
+   *
+   * Returns a very sparse object if Services.appinfo is unavailable.
+   */
+  obtainAppInfo: function () {
+    let out = {"_v": this.appInfoVersion};
+    try {
+      let ai = Services.appinfo;
+      for (let [k, v] in Iterator(this.appInfoFields)) {
+        out[k] = ai[v];
+      }
+    } catch (ex) {
+      this._log.warn("Could not obtain Services.appinfo: " +
+                     CommonUtils.exceptionStr(ex));
+    }
+
+    try {
+      out["updateChannel"] = UpdateChannel.get();
+    } catch (ex) {
+      this._log.warn("Could not obtain update channel: " +
+                     CommonUtils.exceptionStr(ex));
+    }
+
+    return out;
+  },
 });
 
 /**
  * HealthReporter and its abstract superclass coordinate collection and
  * submission of health report metrics.
  *
  * This is the main type for Firefox Health Report on desktop. It glues all the
  * lower-level components (such as collection and submission) together.
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -246,17 +246,17 @@ add_task(function test_json_payload_simp
   let reporter = yield getReporter("json_payload_simple");
 
   try {
     let now = new Date();
     let payload = yield reporter.getJSONPayload();
     do_check_eq(typeof payload, "string");
     let original = JSON.parse(payload);
 
-    do_check_eq(original.version, 1);
+    do_check_eq(original.version, 2);
     do_check_eq(original.thisPingDate, reporter._formatDate(now));
     do_check_eq(Object.keys(original.data.last).length, 0);
     do_check_eq(Object.keys(original.data.days).length, 0);
 
     reporter.lastPingDate = new Date(now.getTime() - 24 * 60 * 60 * 1000 - 10);
 
     original = JSON.parse(yield reporter.getJSONPayload());
     do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate));
@@ -580,26 +580,51 @@ add_task(function test_error_message_scr
 
     reporter._recordError("Foo " + uri.spec);
     do_check_eq(reporter._errors[0], "Foo <AppDataURI>");
   } finally {
     reporter._shutdown();
   }
 });
 
+add_task(function test_basic_appinfo() {
+  function verify(d) {
+    do_check_eq(d["_v"], 1);
+    do_check_eq(d._v, 1);
+    do_check_eq(d.vendor, "Mozilla");
+    do_check_eq(d.name, "xpcshell");
+    do_check_eq(d.id, "xpcshell@tests.mozilla.org");
+    do_check_eq(d.version, "1");
+    do_check_eq(d.appBuildID, "20121107");
+    do_check_eq(d.platformVersion, "p-ver");
+    do_check_eq(d.platformBuildID, "20121106");
+    do_check_eq(d.os, "XPCShell");
+    do_check_eq(d.xpcomabi, "noarch-spidermonkey");
+    do_check_true("updateChannel" in d);
+  }
+  let reporter = yield getReporter("basic_appinfo");
+  try {
+    verify(reporter.obtainAppInfo());
+    let payload = yield reporter.collectAndObtainJSONPayload(true);
+    do_check_eq(payload["version"], 2);
+    verify(payload["geckoAppInfo"]);
+  } finally {
+    reporter._shutdown();
+  }
+});
+
 // Ensure collection occurs if upload is disabled.
 add_task(function test_collect_when_upload_disabled() {
   let reporter = getJustReporter("collect_when_upload_disabled");
   reporter._policy.recordHealthReportUploadEnabled(false, "testing-collect");
   do_check_false(reporter._policy.healthReportUploadEnabled);
 
   let name = "healthreport-testing-collect_when_upload_disabled-healthreport-lastDailyCollection";
   let pref = "app.update.lastUpdateTime." + name;
   do_check_false(Services.prefs.prefHasUserValue(pref));
-
   try {
     yield reporter.onInit();
     do_check_true(Services.prefs.prefHasUserValue(pref));
 
     // We would ideally ensure the timer fires and does the right thing.
     // However, testing the update timer manager is quite involved.
   } finally {
     reporter._shutdown();