Bug 879523 - Remote the SelectorSearch. r=paul
authorDave Camp <dcamp@mozilla.com>
Mon, 10 Jun 2013 21:18:46 -0700
changeset 151610 40fcf0ac7aba8f606f139c24103b0138c3ea988f
parent 151609 de691ff8c67791447de2e1d1cea6de941fa0544c
child 151611 541487a04a9c065060401d98cbbcd4e7e8042943
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaul
bugs879523
milestone25.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 879523 - Remote the SelectorSearch. r=paul
browser/devtools/inspector/inspector-panel.js
browser/devtools/inspector/selector-search.js
browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js
browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js
toolkit/devtools/server/actors/inspector.js
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -219,28 +219,29 @@ InspectorPanel.prototype = {
    */
   setupSearchBox: function InspectorPanel_setupSearchBox() {
     let searchDoc;
     if (this.target.isLocalTab) {
       searchDoc = this.browser.contentDocument;
     } else if (this.target.window) {
       searchDoc = this.target.window.document;
     } else {
-      return;
+      searchDoc = null;
     }
     // Initiate the selectors search object.
-    let setNodeFunction = function(node) {
-      this.selection.setNode(node, "selectorsearch");
+    let setNodeFunction = function(eventName, node) {
+      this.selection.setNodeFront(node, "selectorsearch");
     }.bind(this);
     if (this.searchSuggestions) {
       this.searchSuggestions.destroy();
       this.searchSuggestions = null;
     }
     this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
-    this.searchSuggestions = new SelectorSearch(searchDoc, this.searchBox, setNodeFunction);
+    this.searchSuggestions = new SelectorSearch(this, searchDoc, this.searchBox);
+    this.searchSuggestions.on("node-selected", setNodeFunction);
   },
 
   /**
    * Build the sidebar.
    */
   setupSidebar: function InspectorPanel_setupSidebar() {
     let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
     this.sidebar = new ToolSidebar(tabbox, this, "inspector");
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -1,39 +1,42 @@
 /* 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} = require("chrome");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("sdk/core/promise");
 
 loader.lazyGetter(this, "AutocompletePopup", () => {
   return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
 });
 
 // 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 InspectorPanel aInspector
+ *        The InspectorPanel whose `walker` attribute should be used for
+ *        document traversal.
  * @param nsIDOMDocument aContentDocument
- *        The content document which inspector is attached to.
+ *        The content document which inspector is attached to, or null if
+ *        a remote document.
  * @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.
  */
-function SelectorSearch(aContentDocument, aInputNode, aCallback) {
+function SelectorSearch(aInspector, aContentDocument, aInputNode) {
+  this.inspector = aInspector;
   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;
@@ -57,22 +60,30 @@ function SelectorSearch(aContentDocument
     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);
+
+  // For testing, we need to be able to wait for the most recent node request
+  // to finish.  Tests can watch this promise for that.
+  this._lastQuery = promise.resolve(null);
+
+  EventEmitter.decorate(this);
 }
 
 exports.SelectorSearch = SelectorSearch;
 
 SelectorSearch.prototype = {
 
+  get walker() this.inspector.walker,
+
   // The possible states of the query.
   States: {
     CLASS: "class",
     ID: "id",
     TAG: "tag",
   },
 
   // The current state of the query.
@@ -163,101 +174,122 @@ SelectorSearch.prototype = {
     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;
+    EventEmitter.decorate(this);
+  },
+
+  _selectResult: function(index) {
+    return this._searchResults.item(index).then(node => {
+      this.emit("node-selected", node);
+    });
   },
 
   /**
    * 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._searchResults = null;
     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) + "*";
+    let queryList = null;
+
+    this._lastQuery = this.walker.querySelectorAll(this.walker.rootNode, query).then(list => {
+      return list;
+    }, (err) => {
+      // Failures are ok here, just use a null item list;
+      return null;
+    }).then(queryList => {
+      // Value has changed since we started this request, we're done.
+      if (query != this.searchBox.value) {
+        if (queryList) {
+          queryList.release();
+        }
+        return promise.reject(null);
       }
 
-      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();
+      this._searchResults = queryList;
+      if (this._searchResults && 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");
+
+        return this._selectResult(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();
       }
-      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();
-    }
+      return undefined;
+    });
   },
 
   /**
    * 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) {
+        if (query == this._lastSearched && this._searchResults) {
           this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
         }
         else {
           this._onHTMLSearch();
           return;
         }
         break;
 
@@ -310,18 +342,18 @@ SelectorSearch.prototype = {
         return;
 
       default:
         return;
     }
 
     aEvent.preventDefault();
     aEvent.stopPropagation();
-    if (this._searchResults.length > 0) {
-      this.callback(this._searchResults[this._searchIndex]);
+    if (this._searchResults && this._searchResults.length > 0) {
+      this._lastQuery = this._selectResult(this._searchIndex);
     }
   },
 
   /**
    * Handles keypress and mouse click on the suggestions richlistbox.
    */
   _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) {
     switch(aEvent.keyCode || aEvent.button) {
@@ -437,16 +469,19 @@ SelectorSearch.prototype = {
     }
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
   showSuggestions: function SelectorSearch_showSuggestions() {
+    if (!this.walker.isLocal()) {
+      return;
+    }
     let query = this.searchBox.value;
     if (this._lastValidSearch != "" &&
         this._lastToLastValidSearch != this._lastValidSearch) {
       this._searchSuggestions = {
         ids: new Map(),
         classes: new Map(),
         tags: new Map(),
       };
--- a/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js
@@ -63,16 +63,17 @@ function test()
   {
     openInspector(startTest);
   }
 
   function startTest(aInspector)
   {
     inspector = aInspector;
     inspector.selection.setNode($("b1"));
+
     searchBox =
       inspector.panelWin.document.getElementById("inspector-searchbox");
 
     focusSearchBoxUsingShortcut(inspector.panelWin, function() {
       searchBox.addEventListener("command", checkState, true);
       searchBox.addEventListener("keypress", checkState, true);
       checkStateAndMoveOn(0);
     });
@@ -90,25 +91,28 @@ function test()
     info("pressing key " + key + " to get id " + id);
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
   }
 
   function checkState(event) {
     if (event.type == "keypress" && keypressStates.indexOf(state) == -1) {
       return;
     }
-    executeSoon(function() {
-      let [key, id, isValid] = keyStates[state];
-      info(inspector.selection.node.id + " is selected with text " +
-           inspector.searchBox.value);
-      is(inspector.selection.node, $(id),
-         "Correct node is selected for state " + state);
-      is(!searchBox.classList.contains("devtools-no-search-result"), isValid,
-         "Correct searchbox result state for state " + state);
-      checkStateAndMoveOn(state + 1);
+
+    inspector.searchSuggestions._lastQuery.then(() => {
+      executeSoon(() => {
+        let [key, id, isValid] = keyStates[state];
+        info(inspector.selection.node.id + " is selected with text " +
+             inspector.searchBox.value);
+        is(inspector.selection.node, $(id),
+           "Correct node is selected for state " + state);
+        is(!searchBox.classList.contains("devtools-no-search-result"), isValid,
+           "Correct searchbox result state for state " + state);
+        checkStateAndMoveOn(state + 1);
+      });
     });
   }
 
   function finishUp() {
     searchBox = null;
     gBrowser.removeCurrentTab();
     finish();
   }
--- a/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js
@@ -55,16 +55,17 @@ function test()
   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);
     });
@@ -80,17 +81,17 @@ function test()
     state = index;
 
     info("pressing key " + key + " to get suggestions " +
          JSON.stringify(suggestions));
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
   }
 
   function checkState(event) {
-    executeSoon(function() {
+    inspector.searchSuggestions._lastQuery.then(() => {
       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,
--- a/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js
@@ -82,17 +82,17 @@ function test()
     state = index;
 
     info("pressing key " + key + " to get suggestions " +
          JSON.stringify(suggestions));
     EventUtils.synthesizeKey(key, {}, inspector.panelWin);
   }
 
   function checkState(event) {
-    executeSoon(function() {
+    inspector.searchSuggestions._lastQuery.then(() => {
       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,
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -2038,17 +2038,17 @@ function nodeDocument(node) {
  * Similar to a TreeWalker, except will dig in to iframes and it doesn't
  * implement the good methods like previousNode and nextNode.
  *
  * See TreeWalker documentation for explanations of the methods.
  */
 function DocumentWalker(aNode, aShow, aFilter, aExpandEntityReferences)
 {
   let doc = nodeDocument(aNode);
-  this.walker = doc.createTreeWalker(nodeDocument(aNode),
+  this.walker = doc.createTreeWalker(doc,
     aShow, aFilter, aExpandEntityReferences);
   this.walker.currentNode = aNode;
   this.filter = aFilter;
 }
 
 DocumentWalker.prototype = {
   get node() this.walker.node,
   get whatToShow() this.walker.whatToShow,