Bug 1294502 - Use the same AutoCompletePopup logic for e10s and non-e10s. r?MattN draft
authorMike Conley <mconley@mozilla.com>
Thu, 18 Aug 2016 10:50:58 -0400
changeset 402663 fbeff28e2aabc59a8848d3dc0df9a11441623428
parent 402662 bd718bff1bbc06461e00c85008ac6a04132ba142
child 403496 035d2f16891a73323497a0c7db0b8243d65387d7
child 403500 f7c77421898e1d55d2471c35faba3c66bbadb9ac
push id26721
push usermconley@mozilla.com
push dateThu, 18 Aug 2016 15:51:49 +0000
reviewersMattN
bugs1294502
milestone51.0a1
Bug 1294502 - Use the same AutoCompletePopup logic for e10s and non-e10s. r?MattN MozReview-Commit-ID: B0QQLEn3yS
browser/components/nsBrowserGlue.js
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/satchel/AutoCompleteE10S.jsm
toolkit/components/satchel/AutoCompletePopup.jsm
toolkit/components/satchel/FormHistoryStartup.js
toolkit/components/satchel/moz.build
toolkit/components/search/tests/xpcshell/test_searchSuggest.js
toolkit/content/browser-content.js
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -19,16 +19,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 // lazy module getters
 [
   ["AboutHome", "resource:///modules/AboutHome.jsm"],
   ["AboutNewTab", "resource:///modules/AboutNewTab.jsm"],
   ["AddonManager", "resource://gre/modules/AddonManager.jsm"],
   ["AddonWatcher", "resource://gre/modules/AddonWatcher.jsm"],
   ["AsyncShutdown", "resource://gre/modules/AsyncShutdown.jsm"],
+  ["AutoCompletePopup", "resource://gre/modules/AutoCompletePopup.jsm"],
   ["BookmarkHTMLUtils", "resource://gre/modules/BookmarkHTMLUtils.jsm"],
   ["BookmarkJSONUtils", "resource://gre/modules/BookmarkJSONUtils.jsm"],
   ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"],
   ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"],
   ["CaptivePortalWatcher", "resource:///modules/CaptivePortalWatcher.jsm"],
   ["ContentClick", "resource:///modules/ContentClick.jsm"],
   ["ContentPrefServiceParent", "resource://gre/modules/ContentPrefServiceParent.jsm"],
   ["ContentSearch", "resource:///modules/ContentSearch.jsm"],
@@ -77,17 +78,16 @@ if (AppConstants.MOZ_CRASHREPORTER) {
 XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
   return Services.strings.createBundle('chrome://branding/locale/brand.properties');
 });
 
 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
   return Services.strings.createBundle('chrome://browser/locale/browser.properties');
 });
 
-
 // Seconds of idle before trying to create a bookmarks backup.
 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60;
 // Minimum interval between backups.  We try to not create more than one backup
 // per interval.
 const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
 // Maximum interval between backups.  If the last backup is older than these
 // days we will try to create a new one more aggressively.
 const BOOKMARKS_BACKUP_MAX_INTERVAL_DAYS = 3;
@@ -1050,16 +1050,18 @@ BrowserGlue.prototype = {
     this._checkForOldBuildUpdates();
 
     if (!AppConstants.RELEASE_BUILD) {
       this.checkForPendingCrashReports();
     }
 
     CaptivePortalWatcher.init();
 
+    AutoCompletePopup.init();
+
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
   },
 
   /**
    * Application shutdown handler.
    */
   _onQuitApplicationGranted: function () {
@@ -1075,22 +1077,21 @@ BrowserGlue.prototype = {
       appStartup.trackStartupCrashEnd();
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     BrowserUsageTelemetry.uninit();
     SelfSupportBackend.uninit();
     NewTabMessages.uninit();
-
     CaptivePortalWatcher.uninit();
-
     AboutNewTab.uninit();
     webrtcUI.uninit();
     FormValidationHandler.uninit();
+    AutoCompletePopup.uninit();
     if (AppConstants.NIGHTLY_BUILD) {
       AddonWatcher.uninit();
     }
   },
 
   _initServiceDiscovery: function () {
     if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
       return;
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -8,18 +8,18 @@ const { classes: Cc, interfaces: Ci, res
 
 Cu.importGlobalProperties(["URL"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "UserAutoCompleteResult",
                                   "resource://gre/modules/LoginManagerContent.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "AutoCompleteE10S",
-                                  "resource://gre/modules/AutoCompleteE10S.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AutoCompletePopup",
+                                  "resource://gre/modules/AutoCompletePopup.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
                                   "resource://gre/modules/DeferredTask.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginDoorhangers",
                                   "resource://gre/modules/LoginDoorhangers.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
@@ -96,17 +96,17 @@ var LoginManagerParent = {
 
       case "RemoteLogins:autoCompleteLogins": {
         this.doAutocompleteSearch(data, msg.target);
         break;
       }
 
       case "RemoteLogins:removeLogin": {
         let login = LoginHelper.vanillaObjectToLogin(data.login);
-        AutoCompleteE10S.removeLogin(login);
+        AutoCompletePopup.removeLogin(login);
         break;
       }
     }
 
     return undefined;
   },
 
   /**
@@ -227,17 +227,16 @@ var LoginManagerParent = {
     });
   }),
 
   doAutocompleteSearch: function({ formOrigin, actionOrigin,
                                    searchString, previousResult,
                                    rect, requestId, remote }, target) {
     // Note: previousResult is a regular object, not an
     // nsIAutoCompleteResult.
-    var result;
 
     let searchStringLower = searchString.toLowerCase();
     let logins;
     if (previousResult &&
         searchStringLower.startsWith(previousResult.searchString.toLowerCase())) {
       log("Using previous autocomplete result");
 
       // We have a list of results for a shorter search string, so just
@@ -268,18 +267,18 @@ var LoginManagerParent = {
     });
 
     // XXX In the E10S case, we're responsible for showing our own
     // autocomplete popup here because the autocomplete protocol hasn't
     // been e10s-ized yet. In the non-e10s case, our caller is responsible
     // for showing the autocomplete popup (via the regular
     // nsAutoCompleteController).
     if (remote) {
-      result = new UserAutoCompleteResult(searchString, matchingLogins);
-      AutoCompleteE10S.showPopupWithResults(target.ownerDocument.defaultView, rect, result);
+      let results = new UserAutoCompleteResult(searchString, matchingLogins);
+      AutoCompletePopup.showPopupWithResults({ browser: target.ownerDocument.defaultView, rect, results });
     }
 
     // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
     // doesn't support structured cloning.
     var jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
     target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
       requestId: requestId,
       logins: jsLogins,
rename from toolkit/components/satchel/AutoCompleteE10S.jsm
rename to toolkit/components/satchel/AutoCompletePopup.jsm
--- a/toolkit/components/satchel/AutoCompleteE10S.jsm
+++ b/toolkit/components/satchel/AutoCompletePopup.jsm
@@ -3,302 +3,267 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
-this.EXPORTED_SYMBOLS = [ "AutoCompleteE10S" ];
+this.EXPORTED_SYMBOLS = [ "AutoCompletePopup" ];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
 
 // nsITreeView implementation that feeds the autocomplete popup
 // with the search data.
-var AutoCompleteE10SView = {
+var AutoCompleteTreeView = {
   // nsISupports
   QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView,
                                          Ci.nsIAutoCompleteController]),
 
   // Private variables
   treeBox: null,
-  treeData: [],
-  properties: [],
+  results: [],
 
   // nsITreeView
   selection: null,
 
-  get rowCount()                     { return this.treeData.length; },
+  get rowCount()                     { return this.results.length; },
   setTree: function(treeBox)         { this.treeBox = treeBox; },
-  getCellText: function(idx, column) { return this.treeData[idx] },
+  getCellText: function(idx, column) { return this.results[idx].value },
   isContainer: function(idx)         { return false; },
   getCellValue: function(idx, column) { return false },
   isContainerOpen: function(idx)     { return false; },
   isContainerEmpty: function(idx)    { return false; },
   isSeparator: function(idx)         { return false; },
   isSorted: function()               { return false; },
   isEditable: function(idx, column)  { return false; },
   canDrop: function(idx, orientation, dt) { return false; },
   getLevel: function(idx)            { return 0; },
   getParentIndex: function(idx)      { return -1; },
-  hasNextSibling: function(idx, after) { return idx < this.treeData.length - 1 },
+  hasNextSibling: function(idx, after) { return idx < this.results.length - 1 },
   toggleOpenState: function(idx)     { },
-  getCellProperties: function(idx, column) { return this.properties[idx] || ""; },
+  getCellProperties: function(idx, column) { return this.results[idx].style || ""; },
   getRowProperties: function(idx)    { return ""; },
   getImageSrc: function(idx, column) { return null; },
   getProgressMode : function(idx, column) { },
   cycleHeader: function(column) { },
   cycleCell: function(idx, column) { },
   selectionChanged: function() { },
   performAction: function(action) { },
   performActionOnCell: function(action, index, column) { },
   getColumnProperties: function(column) { return ""; },
 
   // nsIAutoCompleteController
   get matchCount() {
     return this.rowCount;
   },
 
   handleEnter: function(aIsPopupSelection) {
-    AutoCompleteE10S.handleEnter(aIsPopupSelection);
+    AutoCompletePopup.handleEnter(aIsPopupSelection);
   },
 
   stopSearch: function() {},
 
   // Internal JS-only API
   clearResults: function() {
-    this.treeData = [];
-    this.properties = [];
+    this.results = [];
   },
 
-  addResult: function(text, properties) {
-    this.treeData.push(text);
-    this.properties.push(properties);
+  setResults: function(results) {
+    this.results = results;
   },
 };
 
-this.AutoCompleteE10S = {
+this.AutoCompletePopup = {
+  MESSAGES: [
+    "FormAutoComplete:SelectBy",
+    "FormAutoComplete:GetSelectedIndex",
+    "FormAutoComplete:SetSelectedIndex",
+    "FormAutoComplete:MaybeOpenPopup",
+    "FormAutoComplete:ClosePopup",
+    "FormAutoComplete:Disconnect",
+    "FormAutoComplete:RemoveEntry",
+    "FormAutoComplete:Invalidate",
+  ],
+
   init: function() {
-    let messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
-                         getService(Ci.nsIMessageListenerManager);
-    messageManager.addMessageListener("FormAutoComplete:SelectBy", this);
-    messageManager.addMessageListener("FormAutoComplete:GetSelectedIndex", this);
-    messageManager.addMessageListener("FormAutoComplete:MaybeOpenPopup", this);
-    messageManager.addMessageListener("FormAutoComplete:ClosePopup", this);
-    messageManager.addMessageListener("FormAutoComplete:Disconnect", this);
-    messageManager.addMessageListener("FormAutoComplete:RemoveEntry", this);
+    for (let msg of this.MESSAGES) {
+      Services.mm.addMessageListener(msg, this);
+    }
   },
 
-  _initPopup: function(browserWindow, rect, direction) {
-    if (this._popupCache) {
-      this._popupCache.browserWindow.removeEventListener("unload", this);
+  uninit: function() {
+    for (let msg of this.MESSAGES) {
+      Services.mm.removeMessageListener(msg, this);
     }
-    browserWindow.addEventListener("unload", this);
-
-    this._popupCache = { browserWindow, rect, direction };
-
-    this.browser = browserWindow.gBrowser.selectedBrowser;
-    this.popup = this.browser.autoCompletePopup;
-    this.popup.hidden = false;
-    // don't allow the popup to become overly narrow
-    this.popup.setAttribute("width", Math.max(100, rect.width));
-    this.popup.style.direction = direction;
-
-    this.x = rect.left;
-    this.y = rect.top;
-    this.width = rect.width;
-    this.height = rect.height;
   },
 
   handleEvent: function(evt) {
-    if (evt.type === "unload") {
-      this._uninitPopup();
+    if (evt.type === "popuphidden") {
+      this.openedPopup = null;
+      this.weakBrowser = null;
+      evt.target.removeEventListener("popuphidden", this);
     }
   },
 
-  _uninitPopup: function() {
-    this._popupCache = null;
-    this.browser = null;
-    this.popup = null;
-  },
-
-  _showPopup: function(results) {
-    AutoCompleteE10SView.clearResults();
+  // Along with being called internally by the receiveMessage handler,
+  // this function is also called directly by the login manager, which
+  // uses a single message to fill in the autocomplete results. See
+  // "RemoteLogins:autoCompleteLogins".
+  showPopupWithResults: function({ browser, rect, dir, results }) {
+    if (!results.length || this.openedPopup) {
+      // We shouldn't ever be showing an empty popup, and if we
+      // already have a popup open, the old one needs to close before
+      // we consider opening a new one.
+      return;
+    }
 
-    let resultsArray = [];
-    let count = results.matchCount;
-    for (let i = 0; i < count; i++) {
-      // The actual result for each match in the results object is the value.
-      // We return that in order to submit the correct value. However, we have
-      // to make sure we display the label corresponding to it in the popup.
-      resultsArray.push(results.getValueAt(i));
-      AutoCompleteE10SView.addResult(results.getLabelAt(i),
-                                     results.getStyleAt(i));
+    let window = browser.ownerDocument.defaultView;
+    let tabbrowser = window.gBrowser;
+    if (Services.focus.activeWindow != window ||
+        tabbrowser.selectedBrowser != browser) {
+      // We were sent a message from a window or tab that went into the
+      // background, so we'll ignore it for now.
+      return;
     }
 
-    this.popup.view = AutoCompleteE10SView;
+    this.weakBrowser = Cu.getWeakReference(browser);
+    this.openedPopup = browser.autoCompletePopup;
+    this.openedPopup.hidden = false;
+    // don't allow the popup to become overly narrow
+    this.openedPopup.setAttribute("width", Math.max(100, rect.width));
+    this.openedPopup.style.direction = dir;
 
-    this.popup.selectedIndex = -1;
-    this.popup.invalidate();
+    AutoCompleteTreeView.setResults(results);
+    this.openedPopup.view = AutoCompleteTreeView;
+    this.openedPopup.selectedIndex = -1;
+    this.openedPopup.invalidate();
 
-    if (count > 0) {
+    if (results.length) {
       // Reset fields that were set from the last time the search popup was open
-      this.popup.mInput = null;
-      this.popup.showCommentColumn = false;
-      this.popup.showImageColumn = false;
-      this.popup.openPopupAtScreenRect("after_start", this.x, this.y, this.width, this.height, false, false);
+      this.openedPopup.mInput = null;
+      this.openedPopup.showCommentColumn = false;
+      this.openedPopup.showImageColumn = false;
+      this.openedPopup.openPopupAtScreenRect("after_start", rect.left, rect.top,
+                                             rect.width, rect.height, false,
+                                             false);
+      this.openedPopup.addEventListener("popuphidden", this);
     } else {
-      this.popup.closePopup();
+      this.closePopup();
+    }
+  },
+
+  invalidate(results) {
+    if (!this.openedPopup) {
+      return;
     }
 
-    this._resultCache = results;
-    return resultsArray;
+    if (!results.length) {
+      this.closePopup();
+    } else {
+      AutoCompleteTreeView.setResults(results);
+      this.openedPopup.invalidate();
+    }
   },
 
-  // This function is used by the login manager, which uses a single message
-  // to fill in the autocomplete results. See
-  // "RemoteLogins:autoCompleteLogins".
-  showPopupWithResults: function(browserWindow, rect, results) {
-    this._initPopup(browserWindow, rect);
-    this._showPopup(results);
+  closePopup() {
+    if (this.openedPopup) {
+      this.openedPopup.closePopup();
+    }
+    AutoCompleteTreeView.clearResults();
   },
 
   removeLogin(login) {
     Services.logins.removeLogin(login);
 
     // It's possible to race and have the deleted login no longer be in our
     // resultCache's logins, so we remove it from the database above and only
     // deal with our resultCache below.
     let idx = this._resultCache.logins.findIndex(cur => {
       return login.guid === cur.QueryInterface(Ci.nsILoginMetaInfo).guid
     });
     if (idx !== -1) {
       this.removeEntry(idx, false);
     }
   },
 
-  removeEntry(index, updateDB = true) {
-    this._resultCache.removeValueAt(index, updateDB);
-
-    let selectedIndex = this.popup.selectedIndex;
-    this.showPopupWithResults(this._popupCache.browserWindow,
-                              this._popupCache.rect,
-                              this._resultCache);
-
-    // If we removed the last result, bump the selected index back once.
-    if (selectedIndex >= this._resultCache.matchCount)
-      selectedIndex--;
-    this.popup.selectedIndex = selectedIndex;
-  },
-
-  // This function is called in response to AutoComplete requests from the
-  // child (received via the message manager, see
-  // "FormHistory:AutoCompleteSearchAsync").
-  search: function(message) {
-    let browserWindow = message.target.ownerDocument.defaultView;
-    let rect = message.data;
-    let direction = message.data.direction;
-
-    this._initPopup(browserWindow, rect, direction);
-
-    // NB: We use .wrappedJSObject here in order to pass our mock DOM object
-    // without being rejected by XPConnect (which attempts to enforce that DOM
-    // objects are implemented in C++.
-    let formAutoComplete = Cc["@mozilla.org/satchel/form-autocomplete;1"]
-                             .getService(Ci.nsIFormAutoComplete).wrappedJSObject;
-
-    let values, labels;
-    if (message.data.datalistResult) {
-      // Create a full FormAutoCompleteResult from the mock one that we pass
-      // over IPC.
-      message.data.datalistResult =
-        new FormAutoCompleteResult(message.data.untrimmedSearchString,
-                                   Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
-                                   0, "", message.data.datalistResult.values,
-                                   message.data.datalistResult.labels,
-                                   [], null);
-    } else {
-      message.data.datalistResult = null;
+  receiveMessage: function(message) {
+    if (!message.target.autoCompletePopup) {
+      // Returning false to pacify ESLint, but this return value is
+      // ignored by the messaging infrastructure.
+      return false;
     }
 
-    let previousResult = null;
-    let previousSearchString = message.data.previousSearchString;
-    let searchString = message.data.untrimmedSearchString.toLowerCase();
-    if (previousSearchString && previousSearchString.length > 1 &&
-        searchString.includes(previousSearchString)) {
-      previousResult = this._resultCache;
-    }
-    formAutoComplete.autoCompleteSearchAsync(message.data.inputName,
-                                             message.data.untrimmedSearchString,
-                                             message.data.mockField,
-                                             previousResult,
-                                             message.data.datalistResult,
-                                             { onSearchCompletion:
-                                               this.onSearchComplete.bind(this) });
-  },
-
-  // The second half of search, this fills in the popup and returns the
-  // results to the child.
-  onSearchComplete: function(results) {
-    let resultsArray = this._showPopup(results);
+    switch (message.name) {
+      case "FormAutoComplete:SelectBy": {
+        this.openedPopup.selectBy(message.data.reverse, message.data.page);
+        break;
+      }
 
-    this.browser.messageManager.sendAsyncMessage(
-      "FormAutoComplete:AutoCompleteSearchAsyncResult",
-      {results: resultsArray}
-    );
-  },
-
-  receiveMessage: function(message) {
-    switch (message.name) {
-      case "FormAutoComplete:SelectBy":
-        this.popup.selectBy(message.data.reverse, message.data.page);
-        break;
-
-      case "FormAutoComplete:GetSelectedIndex":
-        return this.popup.selectedIndex;
+      case "FormAutoComplete:GetSelectedIndex": {
+        if (this.openedPopup) {
+          return this.openedPopup.selectedIndex;
+        }
+        // If the popup was closed, then the selection
+        // has not changed.
+        return -1;
+      }
 
-      case "FormAutoComplete:RemoveEntry":
-        this.removeEntry(message.data.index);
-        break;
-
-      case "FormAutoComplete:MaybeOpenPopup":
-        if (AutoCompleteE10SView.treeData.length > 0 &&
-            !this.popup.popupOpen) {
-          // This happens when one of the arrow keys is pressed after a search
-          // has already been completed. nsAutoCompleteController tries to
-          // re-use its own cache of the results without re-doing the search.
-          // Detect that and show the popup here.
-          this.showPopupWithResults(this._popupCache.browserWindow,
-                                    this._popupCache.rect,
-                                    this._resultCache);
+      case "FormAutoComplete:SetSelectedIndex": {
+        let { index } = message.data;
+        if (this.openedPopup) {
+          this.openedPopup.selectedIndex = index;
         }
         break;
+      }
 
-      case "FormAutoComplete:ClosePopup":
-        this.popup.closePopup();
+      case "FormAutoComplete:MaybeOpenPopup": {
+        let { results, rect, dir } = message.data;
+        this.showPopupWithResults({ browser: message.target, rect, dir,
+                                    results });
         break;
+      }
 
-      case "FormAutoComplete:Disconnect":
+      case "FormAutoComplete:Invalidate": {
+        let { results } = message.data;
+        this.invalidate(results);
+        break;
+      }
+
+      case "FormAutoComplete:ClosePopup": {
+        this.closePopup();
+        break;
+      }
+
+      case "FormAutoComplete:Disconnect": {
         // The controller stopped controlling the current input, so clear
         // any cached data.  This is necessary cause otherwise we'd clear data
         // only when starting a new search, but the next input could not support
         // autocomplete and it would end up inheriting the existing data.
-        AutoCompleteE10SView.clearResults();
+        AutoCompleteTreeView.clearResults();
         break;
+      }
     }
-    return undefined;
+    // Returning false to pacify ESLint, but this return value is
+    // ignored by the messaging infrastructure.
+    return false;
   },
 
+  /**
+   * Despite its name, handleEnter is what is called when the
+   * user clicks on one of the items in the popup.
+   */
   handleEnter: function(aIsPopupSelection) {
-    this.browser.messageManager.sendAsyncMessage(
-      "FormAutoComplete:HandleEnter",
-      { selectedIndex: this.popup.selectedIndex,
-        isPopupSelection: aIsPopupSelection }
-    );
+    let browser = this.weakBrowser ? this.weakBrowser.get()
+                                   : null;
+    if (browser && this.openedPopup) {
+      browser.messageManager.sendAsyncMessage(
+        "FormAutoComplete:HandleEnter",
+        { selectedIndex: this.openedPopup.selectedIndex,
+          isPopupSelection: aIsPopupSelection }
+      );
+    }
   },
 
   stopSearch: function() {}
 }
-
-this.AutoCompleteE10S.init();
--- a/toolkit/components/satchel/FormHistoryStartup.js
+++ b/toolkit/components/satchel/FormHistoryStartup.js
@@ -6,19 +6,16 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "AutoCompleteE10S",
-                                  "resource://gre/modules/AutoCompleteE10S.jsm");
-
 function FormHistoryStartup() { }
 
 FormHistoryStartup.prototype = {
   classID: Components.ID("{3A0012EB-007F-4BB8-AA81-A07385F77A25}"),
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference,
--- a/toolkit/components/satchel/moz.build
+++ b/toolkit/components/satchel/moz.build
@@ -29,16 +29,16 @@ EXTRA_COMPONENTS += [
     'FormHistoryStartup.js',
     'nsFormAutoComplete.js',
     'nsFormHistory.js',
     'nsInputListAutoComplete.js',
     'satchel.manifest',
 ]
 
 EXTRA_JS_MODULES += [
-    'AutoCompleteE10S.jsm',
+    'AutoCompletePopup.jsm',
     'FormHistory.jsm',
     'nsFormAutoCompleteResult.jsm',
 ]
 
 FINAL_LIBRARY = 'xul'
 
 JAR_MANIFESTS += ['jar.mn']
--- a/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
+++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
@@ -6,16 +6,23 @@
  */
 
 "use strict";
 
 Cu.import("resource://gre/modules/FormHistory.jsm");
 Cu.import("resource://gre/modules/SearchSuggestionController.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 
+// We must make sure the FormHistoryStartup component is
+// initialized in order for it to respond to FormHistory
+// requests from nsFormAutoComplete.js.
+var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+                         getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+
 var httpServer = new HttpServer();
 var getEngine, postEngine, unresolvableEngine;
 
 function run_test() {
   Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
 
   removeMetadata();
   updateAppInfo();
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -8,16 +8,18 @@ var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cr = Components.results;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
   "resource://gre/modules/ReaderMode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+  "resource://gre/modules/BrowserUtils.jsm");
 
 var global = this;
 
 
 // Lazily load the finder code
 addMessageListener("Finder:Initialize", function () {
   let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {});
   new RemoteFinderListener(global);
@@ -767,17 +769,16 @@ var FindBar = {
     }
   },
 
   /**
    * Returns whether FAYT can be used for the given event in
    * the current content state.
    */
   _canAndShouldFastFind() {
-    let {BrowserUtils} = Cu.import("resource://gre/modules/BrowserUtils.jsm", {});
     let should = false;
     let can = BrowserUtils.canFastFind(content);
     if (can) {
       //XXXgijs: why all these shenanigans? Why not use the event's target?
       let focusedWindow = {};
       let elt = Services.focus.getFocusedElementForWindow(content, true, focusedWindow);
       let win = focusedWindow.value;
       should = BrowserUtils.shouldFastFind(elt, win);
@@ -1425,50 +1426,86 @@ let AutoCompletePopup = {
 
       controller.detachFromBrowser(docShell);
       this._connected = false;
     }
   },
 
   get input () { return this._input; },
   get overrideValue () { return null; },
-  set selectedIndex (index) { },
+  set selectedIndex (index) {
+    sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
+  },
   get selectedIndex () {
     // selectedIndex getter must be synchronous because we need the
     // correct value when the controller is in controller::HandleEnter.
     // We can't easily just let the parent inform us the new value every
     // time it changes because not every action that can change the
     // selectedIndex is trivial to catch (e.g. moving the mouse over the
     // list).
     return sendSyncMessage("FormAutoComplete:GetSelectedIndex", {});
   },
   get popupOpen () {
     return this._popupOpen;
   },
 
   openAutocompletePopup: function (input, element) {
-    if (!this._popupOpen) {
-      // The search itself normally opens the popup itself, but in some cases,
-      // nsAutoCompleteController tries to use cached results so notify our
-      // popup to reuse the last results.
-      sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", {});
+    if (this._popupOpen || !input) {
+      return;
     }
+
+    let rect = BrowserUtils.getElementBoundingScreenRect(element);
+    let window = element.ownerDocument.defaultView;
+    let dir = window.getComputedStyle(element).direction;
+    let results = this.getResultsFromController(input);
+
+    sendAsyncMessage("FormAutoComplete:MaybeOpenPopup",
+                     { results, rect, dir });
     this._input = input;
     this._popupOpen = true;
   },
 
   closePopup: function () {
     this._popupOpen = false;
     sendAsyncMessage("FormAutoComplete:ClosePopup", {});
   },
 
   invalidate: function () {
+    if (this._popupOpen) {
+      let results = this.getResultsFromController(this._input);
+      sendAsyncMessage("FormAutoComplete:Invalidate", { results });
+    }
   },
 
   selectBy: function(reverse, page) {
     this._index = sendSyncMessage("FormAutoComplete:SelectBy", {
       reverse: reverse,
       page: page
     });
+  },
+
+  getResultsFromController(inputField) {
+    let results = [];
+
+    if (!inputField) {
+      return results;
+    }
+
+    let controller = inputField.controller;
+    if (!(controller instanceof Ci.nsIAutoCompleteController)) {
+      return results;
+    }
+
+    for (let i = 0; i < controller.matchCount; ++i) {
+      let result = {};
+      result.value = controller.getValueAt(i);
+      result.label = controller.getLabelAt(i);
+      result.comment = controller.getCommentAt(i);
+      result.style = controller.getStyleAt(i);
+      result.image = controller.getImageAt(i);
+      results.push(result);
+    }
+
+    return results;
   }
 }
 
 AutoCompletePopup.init();