Bug 1149346 - First word in selector-search also matches classes and ids; r=harth
authorPatrick Brosset <pbrosset@mozilla.com>
Wed, 15 Apr 2015 08:15:20 -0700
changeset 239337 fd1fddc74fe82cdb2dde47400243a9403aadf37b
parent 239336 848604d33facee4e37d89e44d72f47c165f7c97b
child 239338 955b413d5bb3d58acf1a283e34f637626c040c11
push id28589
push userryanvm@gmail.com
push dateWed, 15 Apr 2015 19:13:10 +0000
treeherdermozilla-central@24ccca4707eb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth
bugs1149346
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1149346 - First word in selector-search also matches classes and ids; r=harth
browser/devtools/inspector/selector-search.js
browser/devtools/inspector/test/browser_inspector_search-02.js
browser/devtools/inspector/test/browser_inspector_search-03.js
browser/devtools/inspector/test/browser_inspector_search-04.js
browser/devtools/inspector/test/browser_inspector_search-suggests-ids-and-classes.js
browser/devtools/sourceeditor/css-autocompleter.js
browser/devtools/sourceeditor/test/css_autocompletion_tests.json
toolkit/devtools/server/actors/inspector.js
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -420,48 +420,57 @@ SelectorSearch.prototype = {
         break;
     }
     this.emit("processing-done");
   },
 
   /**
    * Populates the suggestions list and show the suggestion popup.
    */
-  _showPopup: function(aList, aFirstPart) {
+  _showPopup: function(aList, aFirstPart, aState) {
     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 (let [value, count, state] 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) {
+
+      // In case of tagNames, change te case to small
+      if (value.match(/.*[\.#][^\.#]{0,}$/) == null) {
         item.label = value.toLowerCase();
       }
+
+      // In case the query's state is tag and the item's state is id or class
+      // adjust the preLabel
+      if (aState === this.States.TAG && state === this.States.CLASS) {
+        item.preLabel = "." + item.preLabel;
+      }
+      if (aState === this.States.TAG && state === this.States.ID) {
+        item.preLabel = "#" + item.preLabel;
+      }
+
       items.unshift(item);
       if (++total > MAX_SUGGESTIONS - 1) {
         break;
       }
     }
     if (total > 0) {
       this.searchPopup.setItems(items);
       this.searchPopup.openPopup(this.searchBox);
@@ -472,48 +481,53 @@ SelectorSearch.prototype = {
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
   showSuggestions: function() {
     let query = this.searchBox.value;
+    let state = this.state;
     let firstPart = "";
-    if (this.state == this.States.TAG) {
+
+    if (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];
       query = query.slice(0, query.length - firstPart.length);
     }
-    else if (this.state == this.States.CLASS) {
+    else if (state == this.States.CLASS) {
       // gets the class that is being completed. For ex. '.foo.b' returns 'b'
       firstPart = query.match(/\.([^\.]*)$/)[1];
       query = query.slice(0, query.length - firstPart.length - 1);
     }
-    else if (this.state == this.States.ID) {
+    else if (state == this.States.ID) {
       // gets the id that is being completed. For ex. '.foo#b' returns 'b'
       firstPart = query.match(/#([^#]*)$/)[1];
       query = query.slice(0, query.length - firstPart.length - 1);
     }
     // TODO: implement some caching so that over the wire request is not made
     // everytime.
     if (/[\s+>~]$/.test(query)) {
       query += "*";
     }
+
     this._currentSuggesting = query;
-    return this.walker.getSuggestionsForQuery(query, firstPart, this.state).then(result => {
+    return this.walker.getSuggestionsForQuery(query, firstPart, state).then(result => {
       if (this._currentSuggesting != result.query) {
         // This means that this response is for a previous request and the user
         // as since typed something extra leading to a new request.
         return;
       }
       this._lastToLastValidSearch = this._lastValidSearch;
-      if (this.state == this.States.CLASS) {
+
+      if (state == this.States.CLASS) {
         firstPart = "." + firstPart;
       }
-      else if (this.state == this.States.ID) {
+      else if (state == this.States.ID) {
         firstPart = "#" + firstPart;
       }
-      this._showPopup(result.suggestions, firstPart);
+
+      this._showPopup(result.suggestions, firstPart, state);
     });
   }
 };
--- a/browser/devtools/inspector/test/browser_inspector_search-02.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-02.js
@@ -10,17 +10,21 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 const TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 4}]
+    suggestions: [
+      {label: "div", count: 4},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 4}]
   },
   {
     key: "v",
     suggestions: []
@@ -62,17 +66,21 @@ const TEST_DATA = [
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 4}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 4}]
+    suggestions: [
+      {label: "div", count: 4},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: "p",
     suggestions: []
@@ -130,28 +138,29 @@ add_task(function* () {
 
     let command = once(searchBox, "command");
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
     yield command;
 
     info("Waiting for search query to complete");
     yield inspector.searchSuggestions._lastQuery;
 
-    info("Query completed. Performing checks for input '" + searchBox.value + "'");
+    info("Query completed. Performing checks for input '" + searchBox.value +
+      "' - key pressed: " + key);
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
--- a/browser/devtools/inspector/test/browser_inspector_search-03.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-03.js
@@ -10,17 +10,21 @@ const TEST_URL = TEST_URL_ROOT + "doc_in
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 let TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 2}]
+    suggestions: [
+      {label: "div", count: 2},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 2}]
   },
   {
     key: "v",
     suggestions: []
@@ -45,17 +49,21 @@ let TEST_DATA = [
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 2}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 2}]
+    suggestions: [
+      {label: "div", count: 2},
+      {label: "#d1", count: 1},
+      {label: "#d2", count: 1}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [
@@ -174,21 +182,21 @@ add_task(function* () {
 
     info("Query completed. Performing checks for input '" + searchBox.value + "'");
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
--- a/browser/devtools/inspector/test/browser_inspector_search-04.js
+++ b/browser/devtools/inspector/test/browser_inspector_search-04.js
@@ -13,33 +13,41 @@ const TEST_URL = "data:text/html;charset
 
 // An array of (key, suggestions) pairs where key is a key to press and
 // suggestions is an array of suggestions that should be shown in the popup.
 // Suggestion is an object with label of the entry and optional count
 // (defaults to 1)
 let TEST_DATA = [
   {
     key: "d",
-    suggestions: [{label: "div", count: 5}]
+    suggestions: [
+      {label: "div", count: 5},
+      {label: "#d1", count: 2},
+      {label: "#d2", count: 2}
+    ]
   },
   {
     key: "i",
     suggestions: [{label: "div", count: 5}]
   },
   {
     key: "v",
     suggestions: []
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: [{label: "div", count: 5}]
   },
   {
     key: "VK_BACK_SPACE",
-    suggestions: [{label: "div", count: 5}]
+    suggestions: [
+      {label: "div", count: 5},
+      {label: "#d1", count: 2},
+      {label: "#d2", count: 2}
+    ]
   },
   {
     key: "VK_BACK_SPACE",
     suggestions: []
   },
   {
     key: ".",
     suggestions: [
@@ -85,21 +93,21 @@ add_task(function* () {
 
     info("Query completed. Performing checks for input '" + searchBox.value + "'");
     let actualSuggestions = popup.getItems().reverse();
 
     is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
        "There are expected number of suggestions.");
 
     for (let i = 0; i < suggestions.length; i++) {
-      is(suggestions[i].label, actualSuggestions[i].label,
+      is(actualSuggestions[i].label, suggestions[i].label,
          "The suggestion at " + i + "th index is correct.");
-      is(suggestions[i].count || 1, actualSuggestions[i].count,
+      is(actualSuggestions[i].count, suggestions[i].count || 1,
          "The count for suggestion at " + i + "th index is correct.");
     }
   }
 });
 
 function formatSuggestions(suggestions) {
   return "[" + suggestions
-                .map(s => "'" + s.label + "' (" + s.count || 1 + ")")
+                .map(s => "'" + s.label + "' (" + (s.count || 1) + ")")
                 .join(", ") + "]";
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_search-suggests-ids-and-classes.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the selector-search input proposes ids and classes even when . and
+// # is missing, but that this only occurs when the query is one word (no
+// selector combination)
+
+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 = [
+    ["s", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["p", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["a", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["n", []],
+    [" ", [["span div", 1]]],
+    ["d", [["span div", 1]]], // mixed tag/class/id suggestions only work for the first word
+    ["VK_BACK_SPACE", [["span div", 1]]],
+    ["VK_BACK_SPACE", []],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", [["span", 1], [".span", 1], ["#span", 1]]],
+    ["VK_BACK_SPACE", []],
+    // Test that mixed tags, classes and ids are grouped by types, sorted by
+    // count and alphabetical order
+    ["b", [
+      ["button", 3],
+      ["body", 1],
+      [".bc", 3],
+      [".ba", 1],
+      [".bb", 1],
+      ["#ba", 1],
+      ["#bb", 1],
+      ["#bc", 1]
+    ]],
+  ];
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    waitForFocus(setupTest, content);
+  }, true);
+
+  content.location = "data:text/html," +
+                     "<span class='span' id='span'>" +
+                     "  <div class='div' id='div'></div>" +
+                     "</span>" +
+                     "<button class='ba bc' id='bc'></button>" +
+                     "<button class='bb bc' id='bb'></button>" +
+                     "<button class='bc' id='ba'></button>";
+
+  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) {
+    inspector.searchSuggestions._lastQuery.then(() => {
+      let [key, suggestions] = keyStates[state];
+      let actualSuggestions = popup.getItems();
+      is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
+         "There are expected number of suggestions at " + state + "th step.");
+      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();
+  }
+}
--- a/browser/devtools/sourceeditor/css-autocompleter.js
+++ b/browser/devtools/sourceeditor/css-autocompleter.js
@@ -752,49 +752,64 @@ CSSCompleter.prototype = {
   */
   prepareSelectorResults: function(result) {
     if (this._currentQuery != result.query)
       return [];
 
     result = result.suggestions;
     let query = this.selector;
     let completion = [];
-    for (let value of result) {
+    for (let [value, count, state] of result) {
       switch(this.selectorState) {
         case SELECTOR_STATES.id:
         case SELECTOR_STATES.class:
         case SELECTOR_STATES.pseudo:
           if (/^[.:#]$/.test(this.completing)) {
-            value[0] = query.slice(0, query.length - this.completing.length) +
-                       value[0];
+            value = query.slice(0, query.length - this.completing.length) +
+                       value;
           } else {
-            value[0] = query.slice(0, query.length - this.completing.length - 1) +
-                       value[0];
+            value = query.slice(0, query.length - this.completing.length - 1) +
+                       value;
           }
           break;
 
         case SELECTOR_STATES.tag:
-          value[0] = query.slice(0, query.length - this.completing.length) +
-                     value[0];
+          value = query.slice(0, query.length - this.completing.length) +
+                     value;
           break;
 
         case SELECTOR_STATES.null:
-          value[0] = query + value[0];
+          value = query + value;
           break;
 
         default:
-         value[0] = query.slice(0, query.length - this.completing.length) +
-                    value[0];
+         value = query.slice(0, query.length - this.completing.length) +
+                    value;
       }
-      completion.push({
-        label: value[0],
+
+      let item = {
+        label: value,
         preLabel: query,
-        text: value[0],
-        score: value[1]
-      });
+        text: value,
+        score: count
+      };
+
+      // In case the query's state is tag and the item's state is id or class
+      // adjust the preLabel
+      if (this.selectorState === SELECTOR_STATES.tag &&
+          state === SELECTOR_STATES.class) {
+        item.preLabel = "." + item.preLabel;
+      }
+      if (this.selectorState === SELECTOR_STATES.tag &&
+          state === SELECTOR_STATES.id) {
+        item.preLabel = "#" + item.preLabel;
+      }
+
+      completion.push(item);
+
       if (completion.length > this.maxEntries - 1)
         break;
     }
     return completion;
   },
 
   /**
    * Returns CSS property name suggestions based on the input.
--- a/browser/devtools/sourceeditor/test/css_autocompletion_tests.json
+++ b/browser/devtools/sourceeditor/test/css_autocompletion_tests.json
@@ -16,21 +16,21 @@
              '-moz-animation-play-state', '-moz-animation-timing-function',
              '-moz-appearance']],
   [[12, 20], ['none', 'number-input']],
   [[12, 22], ['none']],
   [[17, 22], ['hsl', 'hsla']],
   [[21,  9], ["-moz-calc", "auto", "calc", "inherit", "initial","unset"]],
   [[22,  5], ['color', 'color-interpolation', 'color-interpolation-filters']],
   [[25, 26], ['.devtools-toolbarbutton > tab',
-              '.devtools-toolbarbutton > .toolbarbutton-menubutton-button',
-              '.devtools-toolbarbutton > hbox']],
+              '.devtools-toolbarbutton > hbox',
+              '.devtools-toolbarbutton > .toolbarbutton-menubutton-button']],
   [[25, 31], ['.devtools-toolbarbutton > hbox.toolbarbutton-menubutton-button']],
   [[29, 20], ['.devtools-menulist:after', '.devtools-menulist:active']],
   [[30, 10], ['#devtools-anotherone', '#devtools-itjustgoeson', '#devtools-menu',
               '#devtools-okstopitnow', '#devtools-toolbarbutton', '#devtools-yetagain']],
   [[39, 39], ['.devtools-toolbarbutton:not([label]) > tab']],
   [[43, 51], ['.devtools-toolbarbutton:not([checked=true]):hover:after',
               '.devtools-toolbarbutton:not([checked=true]):hover:active']],
   [[58, 36], ['!important;']],
-  [[73, 42], [':last-child', ':lang(', ':last-of-type', ':link']],
+  [[73, 42], [':lang(', ':last-of-type', ':link', ':last-child']],
   [[77, 25], ['.visible']],
 ]
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -1010,17 +1010,17 @@ var NodeListActor = exports.NodeListActo
   marshallPool: function() {
     return this.walker;
   },
 
   // Returns the JSON representation of this object over the wire.
   form: function() {
     return {
       actor: this.actorID,
-      length: this.nodeList.length
+      length: this.nodeList ? this.nodeList.length : 0
     }
   },
 
   /**
    * Get a single node from the node list.
    */
   item: method(function(index) {
     return this.walker.attachElement(this.nodeList[index]);
@@ -1863,34 +1863,34 @@ var WalkerActor = protocol.ActorClass({
         sugs.classes.delete("");
         // Editing the style editor may make the stylesheet have errors and
         // thus the page's elements' styles start changing with a transition.
         // That transition comes from the `moz-styleeditor-transitioning` class.
         sugs.classes.delete("moz-styleeditor-transitioning");
         sugs.classes.delete(HIDDEN_CLASS);
         for (let [className, count] of sugs.classes) {
           if (className.startsWith(completing)) {
-            result.push(["." + className, count]);
+            result.push(["." + className, count, selectorState]);
           }
         }
         break;
 
       case "id":
         if (!query) {
           nodes = this._multiFrameQuerySelectorAll("[id]");
         }
         else {
           nodes = this._multiFrameQuerySelectorAll(query);
         }
         for (let node of nodes) {
           sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
         }
         for (let [id, count] of sugs.ids) {
           if (id.startsWith(completing)) {
-            result.push(["#" + id, count]);
+            result.push(["#" + id, count, selectorState]);
           }
         }
         break;
 
       case "tag":
         if (!query) {
           nodes = this._multiFrameQuerySelectorAll("*");
         }
@@ -1898,19 +1898,30 @@ var WalkerActor = protocol.ActorClass({
           nodes = this._multiFrameQuerySelectorAll(query);
         }
         for (let node of nodes) {
           let tag = node.tagName.toLowerCase();
           sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
         }
         for (let [tag, count] of sugs.tags) {
           if ((new RegExp("^" + completing + ".*", "i")).test(tag)) {
-            result.push([tag, count]);
+            result.push([tag, count, selectorState]);
           }
         }
+
+        // For state 'tag' (no preceding # or .) and when there's no query (i.e.
+        // only one word) then search for the matching classes and ids
+        if (!query) {
+          result = [
+            ...result,
+            ...this.getSuggestionsForQuery(null, completing, "class").suggestions,
+            ...this.getSuggestionsForQuery(null, completing, "id").suggestions
+          ];
+        }
+
         break;
 
       case "null":
         nodes = this._multiFrameQuerySelectorAll(query);
         for (let node of nodes) {
           sugs.ids.set(node.id, (sugs.ids.get(node.id)|0) + 1);
           let tag = node.tagName.toLowerCase();
           sugs.tags.set(tag, (sugs.tags.get(tag)|0) + 1);
@@ -1930,21 +1941,48 @@ var WalkerActor = protocol.ActorClass({
         // That transition comes from the `moz-styleeditor-transitioning` class.
         sugs.classes.delete("moz-styleeditor-transitioning");
         sugs.classes.delete(HIDDEN_CLASS);
         for (let [className, count] of sugs.classes) {
           className && result.push(["." + className, count]);
         }
     }
 
-    // Sort alphabetically in increaseing order.
-    result = result.sort();
-    // Sort based on count in decreasing order.
-    result = result.sort(function(a, b) {
-      return b[1] - a[1];
+    // Sort by count (desc) and name (asc)
+    result = result.sort((a, b) => {
+      // Computed a sortable string with first the inverted count, then the name
+      let sortA = (10000-a[1]) + a[0];
+      let sortB = (10000-b[1]) + b[0];
+
+      // Prefixing ids, classes and tags, to group results
+      let firstA = a[0].substring(0, 1);
+      let firstB = b[0].substring(0, 1);
+
+      if (firstA === "#") {
+        sortA = "2" + sortA;
+      }
+      else if (firstA === ".") {
+        sortA = "1" + sortA;
+      }
+      else {
+        sortA = "0" + sortA;
+      }
+
+      if (firstB === "#") {
+        sortB = "2" + sortB;
+      }
+      else if (firstB === ".") {
+        sortB = "1" + sortB;
+      }
+      else {
+        sortB = "0" + sortB;
+      }
+
+      // String compare
+      return sortA.localeCompare(sortB);
     });
 
     result.slice(0, 25);
 
     return {
       query: query,
       suggestions: result
     };