Bug 1294502 - Use the same AutoCompletePopup logic for e10s and non-e10s. r=MattN
authorMike Conley <mconley@mozilla.com>
Thu, 18 Aug 2016 10:50:58 -0400
changeset 309956 f2ea401ab10ce254c22d7ec6ec715b55fbb73998
parent 309955 2557ea45b8e53cc883a971e3a3736ab92bdbc738
child 309957 7a3a65ff58653cfa96f39c2ab4e458111602b92c
push id30575
push userryanvm@gmail.com
push dateFri, 19 Aug 2016 13:46:06 +0000
treeherdermozilla-central@3da4d64410c0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1294502
milestone51.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 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();