Bug 1573836, make autocomplete component fission compatible, r=mak,MattN
authorNeil Deakin <neil@mozilla.com>
Wed, 09 Oct 2019 19:02:57 +0000
changeset 497019 25ba54160c0aee63142e87bf06e67b92c89f566a
parent 497018 6b9df1cffc79c4a0ee61f5c3f8677722aa4193a0
child 497020 8dfd8ae933128a7d9c8028c9294e8e0076c1d0a5
push id36674
push userccoroiu@mozilla.com
push dateThu, 10 Oct 2019 09:26:37 +0000
treeherdermozilla-central@f20fa8068ec2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, MattN
bugs1573836
milestone71.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 1573836, make autocomplete component fission compatible, r=mak,MattN Differential Revision: https://phabricator.services.mozilla.com/D47093
browser/base/content/test/performance/browser_startup_content.js
browser/components/BrowserGlue.jsm
browser/components/tests/unit/test_distribution.js
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/api.js
browser/extensions/formautofill/content/FormAutofillFrameScript.js
browser/extensions/formautofill/content/customElements.js
testing/specialpowers/content/SpecialPowersChild.jsm
toolkit/actors/AutoCompleteChild.jsm
toolkit/actors/AutoCompleteParent.jsm
toolkit/actors/moz.build
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/satchel/AutoCompletePopup.jsm
toolkit/components/satchel/moz.build
toolkit/components/satchel/nsFormFillController.cpp
toolkit/components/satchel/nsFormFillController.h
toolkit/components/satchel/nsIFormFillController.idl
toolkit/components/satchel/test/browser/browser_close_tab.js
toolkit/components/satchel/test/test_datalist_shadow_dom.html
toolkit/content/browser-content.js
toolkit/content/tests/chrome/file_autocomplete_with_composition.js
toolkit/modules/ActorManagerParent.jsm
toolkit/modules/AutoCompletePopupContent.jsm
toolkit/modules/moz.build
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -40,16 +40,17 @@ const whitelist = {
     "resource://gre/modules/sessionstore/SessionHistory.jsm",
 
     // Browser front-end
     "resource:///actors/AboutReaderChild.jsm",
     "resource:///actors/BrowserTabChild.jsm",
     "resource:///modules/ContentMetaHandler.jsm",
     "resource:///actors/LinkHandlerChild.jsm",
     "resource:///actors/SearchTelemetryChild.jsm",
+    "resource://gre/actors/AutoCompleteChild.jsm",
     "resource://gre/modules/ActorChild.jsm",
     "resource://gre/modules/ActorManagerChild.jsm",
     "resource://gre/modules/E10SUtils.jsm",
     "resource://gre/modules/Readerable.jsm",
 
     // Telemetry
     "resource://gre/modules/TelemetryController.jsm", // bug 1470339
     "resource://gre/modules/TelemetryUtils.jsm", // bug 1470339
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -465,17 +465,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
     "resource:///modules/aboutpages/AboutNetErrorHandler.jsm",
   AboutPrivateBrowsingHandler:
     "resource:///modules/aboutpages/AboutPrivateBrowsingHandler.jsm",
   AboutProtectionsHandler:
     "resource:///modules/aboutpages/AboutProtectionsHandler.jsm",
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm",
   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
-  AutoCompletePopup: "resource://gre/modules/AutoCompletePopup.jsm",
   Blocklist: "resource://gre/modules/Blocklist.jsm",
   BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.jsm",
   BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.jsm",
   BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
   ContextualIdentityService:
     "resource://gre/modules/ContextualIdentityService.jsm",
   Corroborate: "resource://gre/modules/Corroborate.jsm",
@@ -1613,17 +1612,16 @@ BrowserGlue.prototype = {
         if (removalSuccessful && uninstalledValue == "True") {
           this._resetProfileNotification("uninstall");
         }
       }
     }
 
     this._checkForOldBuildUpdates();
 
-    AutoCompletePopup.init();
     // Check if Sync is configured
     if (Services.prefs.prefHasUserValue("services.sync.username")) {
       WeaveService.init();
     }
 
     PageThumbs.init();
 
     NewTabUtils.init();
@@ -1874,17 +1872,16 @@ BrowserGlue.prototype = {
       this.pingCentre.uninit();
     }
 
     PageThumbs.uninit();
     NewTabUtils.uninit();
     AboutNetErrorHandler.uninit();
     AboutPrivateBrowsingHandler.uninit();
     AboutProtectionsHandler.uninit();
-    AutoCompletePopup.uninit();
 
     Normandy.uninit();
     RFPHelper.uninit();
   },
 
   // Set up a listener to enable/disable the screenshots extension
   // based on its preference.
   _monitorScreenshotsPref() {
--- a/browser/components/tests/unit/test_distribution.js
+++ b/browser/components/tests/unit/test_distribution.js
@@ -34,17 +34,20 @@ AddonTestUtils.createAppInfo(
 add_task(async function setup() {
   await AddonTestUtils.promiseStartupManager();
 });
 
 // This test causes BrowserGlue to start but not fully initialise, when the
 // AddonManager shuts down BrowserGlue will then try to uninit which will
 // cause AutoComplete.jsm to throw an error.
 // TODO: Fix in https://bugzilla.mozilla.org/show_bug.cgi?id=1543112.
-PromiseTestUtils.whitelistRejectionsGlobally(/Component returned failure code/);
+PromiseTestUtils.whitelistRejectionsGlobally(/A request was aborted/);
+PromiseTestUtils.whitelistRejectionsGlobally(
+  /The operation failed for reasons unrelated/
+);
 
 const TOPICDATA_DISTRIBUTION_CUSTOMIZATION = "force-distribution-customization";
 const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
 
 /**
  * Copy the engine-distribution.xml engine to a fake distribution
  * created in the profile, and registered with the directory service.
  * Create an empty en-US directory to make sure it isn't used.
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -345,34 +345,27 @@ let ProfileAutocomplete = {
           break;
         }
         this._fillFromAutocompleteRow(FormAutofillContent.activeInput);
         break;
       }
     }
   },
 
-  _frameMMFromWindow(contentWindow) {
-    return contentWindow.docShell.messageManager;
+  getActorFromWindow(contentWindow) {
+    return contentWindow.getWindowGlobalChild().getActor("AutoComplete");
   },
 
   _getSelectedIndex(contentWindow) {
-    let mm = this._frameMMFromWindow(contentWindow);
-    let selectedIndexResult = mm.sendSyncMessage(
-      "FormAutoComplete:GetSelectedIndex",
-      {}
-    );
-    if (
-      selectedIndexResult.length != 1 ||
-      !Number.isInteger(selectedIndexResult[0])
-    ) {
+    let actor = this.getActorFromWindow(contentWindow);
+    if (!actor) {
       throw new Error("Invalid autocomplete selectedIndex");
     }
 
-    return selectedIndexResult[0];
+    return actor.selectedIndex;
   },
 
   _fillFromAutocompleteRow(focusedInput) {
     this.debug("_fillFromAutocompleteRow:", focusedInput);
     let formDetails = FormAutofillContent.activeFormDetails;
     if (!formDetails) {
       // The observer notification is for a different frame.
       return;
--- a/browser/extensions/formautofill/api.js
+++ b/browser/extensions/formautofill/api.js
@@ -18,16 +18,21 @@ ChromeUtils.defineModuleGetter(
   "FormAutofill",
   "resource://formautofill/FormAutofill.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "formAutofillParent",
   "resource://formautofill/FormAutofillParent.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "AutoCompleteParent",
+  "resource://gre/actors/AutoCompleteParent.jsm"
+);
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "resProto",
   "@mozilla.org/network/protocol;1?name=resource",
   "nsISubstitutingProtocolHandler"
 );
 
@@ -45,18 +50,17 @@ function insertStyleSheet(domWindow, url
 
   if (CACHED_STYLESHEETS.has(domWindow)) {
     CACHED_STYLESHEETS.get(domWindow).push(styleSheet);
   } else {
     CACHED_STYLESHEETS.set(domWindow, [styleSheet]);
   }
 }
 
-function onMaybeOpenPopup(evt) {
-  let domWindow = evt.target.ownerGlobal;
+function onMaybeOpenPopup(domWindow) {
   if (CACHED_STYLESHEETS.has(domWindow)) {
     // This window already has autofill stylesheets.
     return;
   }
 
   insertStyleSheet(domWindow, "chrome://formautofill/content/formautofill.css");
   insertStyleSheet(
     domWindow,
@@ -159,20 +163,17 @@ this.formautofill = class extends Extens
       );
     } else {
       Services.prefs.clearUserPref(
         "services.sync.engine.creditcards.available"
       );
     }
 
     // Listen for the autocomplete popup message to lazily append our stylesheet related to the popup.
-    Services.mm.addMessageListener(
-      "FormAutoComplete:MaybeOpenPopup",
-      onMaybeOpenPopup
-    );
+    AutoCompleteParent.addPopupStateListener(onMaybeOpenPopup);
 
     formAutofillParent.init().catch(Cu.reportError);
     Services.mm.loadFrameScript(
       "chrome://formautofill/content/FormAutofillFrameScript.js",
       true,
       true
     );
   }
@@ -188,20 +189,17 @@ this.formautofill = class extends Extens
     this.chromeHandle = null;
 
     if (this.autofillManifest) {
       Components.manager.removeBootstrappedManifestLocation(
         this.autofillManifest
       );
     }
 
-    Services.mm.removeMessageListener(
-      "FormAutoComplete:MaybeOpenPopup",
-      onMaybeOpenPopup
-    );
+    AutoCompleteParent.removePopupStateListener(onMaybeOpenPopup);
 
     for (let win of Services.wm.getEnumerator("navigator:browser")) {
       let cachedStyleSheets = CACHED_STYLESHEETS.get(win);
 
       if (!cachedStyleSheets) {
         continue;
       }
 
--- a/browser/extensions/formautofill/content/FormAutofillFrameScript.js
+++ b/browser/extensions/formautofill/content/FormAutofillFrameScript.js
@@ -26,26 +26,63 @@ ChromeUtils.defineModuleGetter(
   "FormAutofillContent",
   "resource://formautofill/FormAutofillContent.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "FormAutofillUtils",
   "resource://formautofill/FormAutofillUtils.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "AutoCompleteChild",
+  "resource://gre/actors/AutoCompleteChild.jsm"
+);
 
 /**
  * Handles content's interactions for the frame.
  */
 var FormAutofillFrameScript = {
   _nextHandleElement: null,
   _alreadyDOMContentLoaded: false,
   _hasDOMContentLoadedHandler: false,
   _hasPendingTask: false,
 
+  popupStateListener(messageName, data, target) {
+    if (!content || !FormAutofill.isAutofillEnabled) {
+      return;
+    }
+
+    const doc = target.document;
+    const { chromeEventHandler } = doc.ownerGlobal.docShell;
+
+    switch (messageName) {
+      case "FormAutoComplete:PopupClosed": {
+        FormAutofillContent.onPopupClosed(data.selectedRowStyle);
+        Services.tm.dispatchToMainThread(() => {
+          chromeEventHandler.removeEventListener(
+            "keydown",
+            FormAutofillContent._onKeyDown,
+            true
+          );
+        });
+
+        break;
+      }
+      case "FormAutoComplete:PopupOpened": {
+        chromeEventHandler.addEventListener(
+          "keydown",
+          FormAutofillContent._onKeyDown,
+          true
+        );
+        break;
+      }
+    }
+  },
+
   _doIdentifyAutofillFields() {
     if (this._hasPendingTask) {
       return;
     }
     this._hasPendingTask = true;
 
     setTimeout(() => {
       FormAutofillContent.identifyAutofillFields(this._nextHandleElement);
@@ -56,36 +93,46 @@ var FormAutofillFrameScript = {
       sendAsyncMessage("FormAutofill:FieldsIdentified");
       FormAutofillContent.updateActiveInput();
     });
   },
 
   init() {
     addEventListener("focusin", this);
     addEventListener("DOMFormBeforeSubmit", this);
+    addEventListener("unload", this, { once: true });
     addMessageListener("FormAutofill:PreviewProfile", this);
     addMessageListener("FormAutofill:ClearForm", this);
-    addMessageListener("FormAutoComplete:PopupClosed", this);
-    addMessageListener("FormAutoComplete:PopupOpened", this);
+
+    AutoCompleteChild.addPopupStateListener(this.popupStateListener);
   },
 
   handleEvent(evt) {
-    if (!evt.isTrusted || !FormAutofill.isAutofillEnabled) {
+    if (!evt.isTrusted) {
       return;
     }
 
     switch (evt.type) {
       case "focusin": {
-        this.onFocusIn(evt);
+        if (FormAutofill.isAutofillEnabled) {
+          this.onFocusIn(evt);
+        }
         break;
       }
       case "DOMFormBeforeSubmit": {
-        this.onDOMFormBeforeSubmit(evt);
+        if (FormAutofill.isAutofillEnabled) {
+          this.onDOMFormBeforeSubmit(evt);
+        }
         break;
       }
+      case "unload": {
+        AutoCompleteChild.removePopupStateListener(this.popupStateListener);
+        break;
+      }
+
       default: {
         throw new Error("Unexpected event type");
       }
     }
   },
 
   onFocusIn(evt) {
     FormAutofillContent.updateActiveInput();
@@ -130,44 +177,23 @@ var FormAutofillFrameScript = {
   },
 
   receiveMessage(message) {
     if (!FormAutofill.isAutofillEnabled) {
       return;
     }
 
     const doc = content.document;
-    const { chromeEventHandler } = doc.ownerGlobal.docShell;
 
     switch (message.name) {
       case "FormAutofill:PreviewProfile": {
         FormAutofillContent.previewProfile(doc);
         break;
       }
       case "FormAutofill:ClearForm": {
         FormAutofillContent.clearForm();
         break;
       }
-      case "FormAutoComplete:PopupClosed": {
-        FormAutofillContent.onPopupClosed(message.data.selectedRowStyle);
-        Services.tm.dispatchToMainThread(() => {
-          chromeEventHandler.removeEventListener(
-            "keydown",
-            FormAutofillContent._onKeyDown,
-            true
-          );
-        });
-
-        break;
-      }
-      case "FormAutoComplete:PopupOpened": {
-        chromeEventHandler.addEventListener(
-          "keydown",
-          FormAutofillContent._onKeyDown,
-          true
-        );
-        break;
-      }
     }
   },
 };
 
 FormAutofillFrameScript.init();
--- a/browser/extensions/formautofill/content/customElements.js
+++ b/browser/extensions/formautofill/content/customElements.js
@@ -9,16 +9,35 @@
 "use strict";
 
 // Wrap in a block to prevent leaking to window scope.
 (() => {
   const { Services } = ChromeUtils.import(
     "resource://gre/modules/Services.jsm"
   );
 
+  function sendMessageToBrowser(msgName, data) {
+    let { AutoCompleteParent } = ChromeUtils.import(
+      "resource://gre/actors/AutoCompleteParent.jsm"
+    );
+
+    let browser = AutoCompleteParent.getCurrentBrowser();
+    if (!browser) {
+      return;
+    }
+
+    if (browser.messageManager) {
+      browser.messageManager.sendAsyncMessage(msgName, data);
+    } else {
+      Cu.reportError(
+        `customElements.js: No messageManager for message "${msgName}"`
+      );
+    }
+  }
+
   class MozAutocompleteProfileListitemBase extends MozElements.MozRichlistitem {
     constructor() {
       super();
 
       /**
        * For form autofill, we want to unify the selection no matter by
        * keyboard navigation or mouseover in order not to confuse user which
        * profile preview is being shown. This field is set to true to indicate
@@ -106,20 +125,17 @@
 
     set selected(val) {
       if (val) {
         this.setAttribute("selected", "true");
       } else {
         this.removeAttribute("selected");
       }
 
-      let { AutoCompletePopup } = ChromeUtils.import(
-        "resource://gre/modules/AutoCompletePopup.jsm"
-      );
-      AutoCompletePopup.sendMessageToBrowser("FormAutofill:PreviewProfile");
+      sendMessageToBrowser("FormAutofill:PreviewProfile");
 
       return val;
     }
 
     get selected() {
       return this.getAttribute("selected") == "true";
     }
 
@@ -345,20 +361,17 @@
     constructor() {
       super();
 
       this.addEventListener("click", event => {
         if (event.button != 0) {
           return;
         }
 
-        let { AutoCompletePopup } = ChromeUtils.import(
-          "resource://gre/modules/AutoCompletePopup.jsm"
-        );
-        AutoCompletePopup.sendMessageToBrowser("FormAutofill:ClearForm");
+        sendMessageToBrowser("FormAutofill:ClearForm");
       });
     }
 
     connectedCallback() {
       if (this.delayConnectedCallback()) {
         return;
       }
 
--- a/testing/specialpowers/content/SpecialPowersChild.jsm
+++ b/testing/specialpowers/content/SpecialPowersChild.jsm
@@ -1246,23 +1246,23 @@ class SpecialPowersChild extends JSWindo
     return WrapPrivileged.wrap(tmp.FormHistory);
   }
   getFormFillController(window) {
     return Cc["@mozilla.org/satchel/form-fill-controller;1"].getService(
       Ci.nsIFormFillController
     );
   }
   attachFormFillControllerTo(window) {
-    this.getFormFillController().attachPopupElementToBrowser(
-      window.docShell,
+    this.getFormFillController().attachPopupElementToDocument(
+      window.document,
       this._getAutoCompletePopup(window)
     );
   }
   detachFormFillControllerFrom(window) {
-    this.getFormFillController().detachFromBrowser(window.docShell);
+    this.getFormFillController().detachFromDocument(window.document);
   }
   isBackButtonEnabled(window) {
     return !this._getTopChromeWindow(window)
       .document.getElementById("Browser:Back")
       .hasAttribute("disabled");
   }
   // XXX end of problematic APIs
 
rename from toolkit/modules/AutoCompletePopupContent.jsm
rename to toolkit/actors/AutoCompleteChild.jsm
--- a/toolkit/modules/AutoCompletePopupContent.jsm
+++ b/toolkit/actors/AutoCompleteChild.jsm
@@ -1,131 +1,239 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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/. */
 
-var EXPORTED_SYMBOLS = ["AutoCompletePopup"];
+var EXPORTED_SYMBOLS = ["AutoCompleteChild"];
 
 /* eslint no-unused-vars: ["error", {args: "none"}] */
 
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
 ChromeUtils.defineModuleGetter(
   this,
   "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm"
 );
 
-const MESSAGES = [
-  "FormAutoComplete:HandleEnter",
-  "FormAutoComplete:PopupClosed",
-  "FormAutoComplete:PopupOpened",
-  "FormAutoComplete:RequestFocus",
-];
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "formFill",
+  "@mozilla.org/satchel/form-fill-controller;1",
+  "nsIFormFillController"
+);
+
+let autoCompleteListeners = new Set();
 
 class AutoCompletePopup {
-  constructor(mm) {
-    this.mm = mm;
+  constructor(actor) {
+    this.actor = actor;
+  }
 
-    for (let messageName of MESSAGES) {
-      mm.addMessageListener(messageName, this);
-    }
+  get input() {
+    return this.actor.input;
+  }
+  get overrideValue() {
+    return null;
+  }
+  set selectedIndex(index) {
+    return (this.actor.selectedIndex = index);
+  }
+  get selectedIndex() {
+    return this.actor.selectedIndex;
+  }
+  get popupOpen() {
+    return this.actor.popupOpen;
+  }
+  openAutocompletePopup(input, element) {
+    return this.actor.openAutocompletePopup(input, element);
+  }
+  closePopup() {
+    this.actor.closePopup();
+  }
+  invalidate() {
+    this.actor.invalidate();
+  }
+  selectBy(reverse, page) {
+    this.actor.selectBy(reverse, page);
+  }
+}
+
+AutoCompletePopup.prototype.QueryInterface = ChromeUtils.generateQI([
+  Ci.nsIAutoCompletePopup,
+]);
+
+class AutoCompleteChild extends JSWindowActorChild {
+  constructor() {
+    super();
 
     this._input = null;
     this._popupOpen = false;
+    this._attached = false;
+  }
+
+  static addPopupStateListener(listener) {
+    autoCompleteListeners.add(listener);
+  }
+
+  static removePopupStateListener(listener) {
+    autoCompleteListeners.delete(listener);
+  }
+
+  willDestroy() {
+    if (this._attached) {
+      formFill.detachFromDocument(this.document);
+    }
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "DOMContentLoaded":
+      case "pageshow":
+      case "focus":
+        if (!this._attached && this.document.location != "about:blank") {
+          this._attached = true;
+          formFill.attachToDocument(this.document, new AutoCompletePopup(this));
+        }
+
+        if (event.type == "focus") {
+          formFill.handleFormEvent(event);
+        }
+
+        break;
+      case "pagehide":
+      case "unload":
+        if (this._attached) {
+          formFill.detachFromDocument(this.document);
+          this._attached = false;
+        }
+      // fall through
+      default:
+        formFill.handleFormEvent(event);
+        break;
+    }
   }
 
   receiveMessage(message) {
     switch (message.name) {
       case "FormAutoComplete:HandleEnter": {
         this.selectedIndex = message.data.selectedIndex;
 
         let controller = Cc[
           "@mozilla.org/autocomplete/controller;1"
         ].getService(Ci.nsIAutoCompleteController);
         controller.handleEnter(message.data.isPopupSelection);
         break;
       }
 
       case "FormAutoComplete:PopupClosed": {
         this._popupOpen = false;
+        this.notifyListeners(message.name, message.data);
         break;
       }
 
       case "FormAutoComplete:PopupOpened": {
         this._popupOpen = true;
+        this.notifyListeners(message.name, message.data);
         break;
       }
 
-      case "FormAutoComplete:RequestFocus": {
+      case "FormAutoComplete:Focus": {
+        // XXX See bug 1582722
+        // Before bug 1573836, the messages here didn't match
+        // ("FormAutoComplete:Focus" versus "FormAutoComplete:RequestFocus")
+        // so this was never called. However this._input is actually a
+        // nsIAutoCompleteInput, which doesn't have a focus() method, so it
+        // wouldn't have worked anyway. So for now, I have just disabled this.
+        /*
         if (this._input) {
           this._input.focus();
         }
+        */
         break;
       }
     }
   }
 
+  notifyListeners(messageName, data) {
+    for (let listener of autoCompleteListeners) {
+      try {
+        listener(messageName, data, this.contentWindow);
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+    }
+  }
+
   get input() {
     return this._input;
   }
-  get overrideValue() {
-    return null;
+
+  set selectedIndex(index) {
+    this.sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
   }
-  set selectedIndex(index) {
-    this.mm.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 this.mm.sendSyncMessage("FormAutoComplete:GetSelectedIndex", {});
+    return Services.cpmm.sendSyncMessage("FormAutoComplete:GetSelectedIndex", {
+      browsingContext: this.browsingContext,
+    });
   }
+
   get popupOpen() {
     return this._popupOpen;
   }
 
   openAutocompletePopup(input, element) {
     if (this._popupOpen || !input) {
       return;
     }
 
     let rect = BrowserUtils.getElementBoundingScreenRect(element);
     let window = element.ownerGlobal;
     let dir = window.getComputedStyle(element).direction;
     let results = this.getResultsFromController(input);
 
-    this.mm.sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", {
+    this.sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", {
       results,
       rect,
       dir,
     });
+
     this._input = input;
   }
 
   closePopup() {
     // We set this here instead of just waiting for the
     // PopupClosed message to do it so that we don't end
     // up in a state where the content thinks that a popup
     // is open when it isn't (or soon won't be).
     this._popupOpen = false;
-    this.mm.sendAsyncMessage("FormAutoComplete:ClosePopup", {});
+    this.sendAsyncMessage("FormAutoComplete:ClosePopup", {});
   }
 
   invalidate() {
     if (this._popupOpen) {
       let results = this.getResultsFromController(this._input);
-      this.mm.sendAsyncMessage("FormAutoComplete:Invalidate", { results });
+      this.sendAsyncMessage("FormAutoComplete:Invalidate", { results });
     }
   }
 
   selectBy(reverse, page) {
-    this._index = this.mm.sendSyncMessage("FormAutoComplete:SelectBy", {
+    Services.cpmm.sendSyncMessage("FormAutoComplete:SelectBy", {
+      browsingContext: this.browsingContext,
       reverse,
       page,
     });
   }
 
   getResultsFromController(inputField) {
     let results = [];
 
rename from toolkit/components/satchel/AutoCompletePopup.jsm
rename to toolkit/actors/AutoCompleteParent.jsm
--- a/toolkit/components/satchel/AutoCompletePopup.jsm
+++ b/toolkit/actors/AutoCompleteParent.jsm
@@ -1,32 +1,80 @@
 /* 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";
 
-var EXPORTED_SYMBOLS = ["AutoCompletePopup"];
+var EXPORTED_SYMBOLS = ["AutoCompleteParent"];
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
-// AutoCompleteResultView is an abstraction around a list of results
-// we got back up from browser-content.js. It implements enough of
-// nsIAutoCompleteController and nsIAutoCompleteInput to make the
-// richlistbox popup work.
+// Stores the browser and actor that has the active popup, used by formfill
+let currentBrowserWeakRef = null;
+let currentActor = null;
+
+let autoCompleteListeners = new Set();
+
+function compareContext(message) {
+  if (
+    !currentActor ||
+    (currentActor.browsingContext != message.data.browsingContext &&
+      currentActor.browsingContext.top != message.data.browsingContext)
+  ) {
+    return false;
+  }
+
+  return true;
+}
+
+// These are two synchronous messages sent by the child.
+// The browsingContext within the message data is either the one that has
+// the active autocomplete popup or the top-level of the one that has
+// the active autocomplete popup.
+Services.ppmm.addMessageListener(
+  "FormAutoComplete:GetSelectedIndex",
+  message => {
+    if (compareContext(message)) {
+      let actor = currentActor;
+      if (actor && actor.openedPopup) {
+        return actor.openedPopup.selectedIndex;
+      }
+    }
+
+    return -1;
+  }
+);
+
+Services.ppmm.addMessageListener("FormAutoComplete:SelectBy", message => {
+  if (compareContext(message)) {
+    let actor = currentActor;
+    if (actor && actor.openedPopup) {
+      actor.openedPopup.selectBy(message.data.reverse, message.data.page);
+    }
+  }
+});
+
+// AutoCompleteResultView is an abstraction around a list of results.
+// It implements enough of nsIAutoCompleteController and
+// nsIAutoCompleteInput to make the richlistbox popup work. Since only
+// one autocomplete popup should be open at a time, this is a singleton.
 var AutoCompleteResultView = {
   // nsISupports
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsIAutoCompleteController,
     Ci.nsIAutoCompleteInput,
   ]),
 
   // Private variables
   results: [],
 
+  // The AutoCompleteParent currently showing results or null otherwise.
+  currentActor: null,
+
   // nsIAutoCompleteController
   get matchCount() {
     return this.results.length;
   },
 
   getValueAt(index) {
     return this.results[index].value;
   },
@@ -52,144 +100,135 @@ var AutoCompleteResultView = {
     return this.results[index].style;
   },
 
   getImageAt(index) {
     return this.results[index].image;
   },
 
   handleEnter(aIsPopupSelection) {
-    AutoCompletePopup.handleEnter(aIsPopupSelection);
+    if (this.currentActor) {
+      this.currentActor.handleEnter(aIsPopupSelection);
+    }
   },
 
   stopSearch() {},
 
   searchString: "",
 
   // nsIAutoCompleteInput
   get controller() {
     return this;
   },
 
   get popup() {
     return null;
   },
 
   _focus() {
-    AutoCompletePopup.requestFocus();
+    if (this.currentActor) {
+      this.currentActor.requestFocus();
+    }
   },
 
   // Internal JS-only API
   clearResults() {
+    this.currentActor = null;
     this.results = [];
   },
 
-  setResults(results) {
+  setResults(actor, results) {
+    this.currentActor = actor;
     this.results = results;
   },
 };
 
-this.AutoCompletePopup = {
-  MESSAGES: [
-    "FormAutoComplete:SelectBy",
-    "FormAutoComplete:GetSelectedIndex",
-    "FormAutoComplete:SetSelectedIndex",
-    "FormAutoComplete:MaybeOpenPopup",
-    "FormAutoComplete:ClosePopup",
-    "FormAutoComplete:Disconnect",
-    "FormAutoComplete:RemoveEntry",
-    "FormAutoComplete:Invalidate",
-  ],
-
-  init() {
-    for (let msg of this.MESSAGES) {
-      Services.mm.addMessageListener(msg, this);
+class AutoCompleteParent extends JSWindowActorParent {
+  willDestroy() {
+    if (this.openedPopup) {
+      this.openedPopup.closePopup();
     }
-    Services.obs.addObserver(this, "message-manager-disconnect");
-  },
+  }
 
-  uninit() {
-    for (let msg of this.MESSAGES) {
-      Services.mm.removeMessageListener(msg, this);
-    }
-    Services.obs.removeObserver(this, "message-manager-disconnect");
-  },
+  static getCurrentBrowser() {
+    return currentBrowserWeakRef ? currentBrowserWeakRef.get() : null;
+  }
 
-  observe(subject, topic, data) {
-    switch (topic) {
-      case "message-manager-disconnect": {
-        if (this.openedPopup) {
-          this.openedPopup.closePopup();
-        }
-        break;
-      }
-    }
-  },
+  static addPopupStateListener(listener) {
+    autoCompleteListeners.add(listener);
+  }
+
+  static removePopupStateListener(listener) {
+    autoCompleteListeners.delete(listener);
+  }
 
   handleEvent(evt) {
     switch (evt.type) {
       case "popupshowing": {
-        this.sendMessageToBrowser("FormAutoComplete:PopupOpened");
+        this.sendAsyncMessage("FormAutoComplete:PopupOpened", {});
         break;
       }
 
       case "popuphidden": {
         let selectedIndex = this.openedPopup.selectedIndex;
         let selectedRowStyle =
           selectedIndex != -1
             ? AutoCompleteResultView.getStyleAt(selectedIndex)
             : "";
-        this.sendMessageToBrowser("FormAutoComplete:PopupClosed", {
+        this.sendAsyncMessage("FormAutoComplete:PopupClosed", {
           selectedRowStyle,
         });
         AutoCompleteResultView.clearResults();
         // adjustHeight clears the height from the popup so that
         // we don't have a big shrink effect if we closed with a
         // large list, and then open on a small one.
         this.openedPopup.adjustHeight();
         this.openedPopup = null;
-        this.weakBrowser = null;
+        currentBrowserWeakRef = null;
+        currentActor = null;
         evt.target.removeEventListener("popuphidden", this);
         evt.target.removeEventListener("popupshowing", this);
         break;
       }
     }
-  },
+  }
 
-  showPopupWithResults({ browser, rect, dir, results }) {
+  showPopupWithResults({ 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 browser = this.browsingContext.top.embedderElement;
     let window = browser.ownerGlobal;
     // Also check window top in case this is a sidebar.
     if (
       Services.focus.activeWindow !== window.top &&
       Services.focus.focusedWindow.top !== window.top
     ) {
       // We were sent a message from a window or tab that went into the
       // background, so we'll ignore it for now.
       return;
     }
 
     // Non-empty result styles
     let resultStyles = new Set(results.map(r => r.style).filter(r => !!r));
-    this.weakBrowser = Cu.getWeakReference(browser);
+    currentBrowserWeakRef = Cu.getWeakReference(browser);
+    currentActor = this;
     this.openedPopup = browser.autoCompletePopup;
     // the layout varies according to different result type
     this.openedPopup.setAttribute("resultstyles", [...resultStyles].join(" "));
     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;
 
-    AutoCompleteResultView.setResults(results);
+    AutoCompleteResultView.setResults(this, results);
     this.openedPopup.view = AutoCompleteResultView;
     this.openedPopup.selectedIndex = -1;
 
     if (results.length) {
       // Reset fields that were set from the last time the search popup was open
       this.openedPopup.mInput = AutoCompleteResultView;
       // Temporarily increase the maxRows as we don't want to show
       // the scrollbar in login or form autofill popups.
@@ -211,84 +250,71 @@ this.AutoCompletePopup = {
         rect.height,
         false,
         false
       );
       this.openedPopup.invalidate();
     } else {
       this.closePopup();
     }
-  },
+  }
 
   invalidate(results) {
     if (!this.openedPopup) {
       return;
     }
 
     if (!results.length) {
       this.closePopup();
     } else {
-      AutoCompleteResultView.setResults(results);
+      AutoCompleteResultView.setResults(this, results);
       this.openedPopup.invalidate();
     }
-  },
+  }
 
   closePopup() {
     if (this.openedPopup) {
       // Note that hidePopup() closes the popup immediately,
       // so popuphiding or popuphidden events will be fired
       // and handled during this call.
       this.openedPopup.hidePopup();
     }
-  },
-
-  removeLogin(login) {
-    Services.logins.removeLogin(login);
-  },
+  }
 
   receiveMessage(message) {
-    if (!message.target.autoCompletePopup) {
+    let browser = this.browsingContext.top.embedderElement;
+    if (!browser || !browser.autoCompletePopup) {
+      // If there is no browser or popup, just make sure that the popup has been closed.
+      if (this.openedPopup) {
+        this.openedPopup.closePopup();
+      }
+
       // Returning false to pacify ESLint, but this return value is
       // ignored by the messaging infrastructure.
       return false;
     }
 
     switch (message.name) {
-      case "FormAutoComplete:SelectBy": {
-        if (this.openedPopup) {
-          this.openedPopup.selectBy(message.data.reverse, message.data.page);
-        }
-        break;
-      }
-
-      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:SetSelectedIndex": {
         let { index } = message.data;
         if (this.openedPopup) {
           this.openedPopup.selectedIndex = index;
         }
         break;
       }
 
       case "FormAutoComplete:MaybeOpenPopup": {
         let { results, rect, dir } = message.data;
         this.showPopupWithResults({
-          browser: message.target,
           rect,
           dir,
           results,
         });
+
+        this.notifyListeners();
         break;
       }
 
       case "FormAutoComplete:Invalidate": {
         let { results } = message.data;
         this.invalidate(results);
         break;
       }
@@ -305,63 +331,53 @@ this.AutoCompletePopup = {
         // autocomplete and it would end up inheriting the existing data.
         AutoCompleteResultView.clearResults();
         break;
       }
     }
     // Returning false to pacify ESLint, but this return value is
     // ignored by the messaging infrastructure.
     return false;
-  },
+  }
+
+  notifyListeners() {
+    let window = this.browsingContext.top.embedderElement.ownerGlobal;
+    for (let listener of autoCompleteListeners) {
+      try {
+        listener(window);
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+    }
+  }
 
   /**
    * Despite its name, this handleEnter is only called when the user clicks on
    * one of the items in the popup since the popup is rendered in the parent process.
    * The real controller's handleEnter is called directly in the content process
    * for other methods of completing a selection (e.g. using the tab or enter
    * keys) since the field with focus is in that process.
    * @param {boolean} aIsPopupSelection
    */
   handleEnter(aIsPopupSelection) {
     if (this.openedPopup) {
-      this.sendMessageToBrowser("FormAutoComplete:HandleEnter", {
+      this.sendAsyncMessage("FormAutoComplete:HandleEnter", {
         selectedIndex: this.openedPopup.selectedIndex,
         isPopupSelection: aIsPopupSelection,
       });
     }
-  },
+  }
+
+  stopSearch() {}
 
   /**
-   * If a browser exists that AutoCompletePopup knows about,
-   * sends it a message. Otherwise, this is a no-op.
-   *
-   * @param {string} msgName
-   *        The name of the message to send.
-   * @param {object} data
-   *        The optional data to send with the message.
-   */
-  sendMessageToBrowser(msgName, data) {
-    let browser = this.weakBrowser ? this.weakBrowser.get() : null;
-    if (!browser) {
-      return;
-    }
-
-    if (browser.messageManager) {
-      browser.messageManager.sendAsyncMessage(msgName, data);
-    } else {
-      Cu.reportError(
-        `AutoCompletePopup: No messageManager for message "${msgName}"`
-      );
-    }
-  },
-
-  stopSearch() {},
-
-  /**
-   * Sends a message to the browser requesting that the input
-   * that the AutoCompletePopup is open for be focused.
+   * Sends a message to the browser that is requesting the input
+   * that the open popup should be focused.
    */
   requestFocus() {
+    // Bug 1582722 - See the response in AutoCompleteChild.jsm for why this disabled.
+    /*
     if (this.openedPopup) {
-      this.sendMessageToBrowser("FormAutoComplete:Focus");
+      this.sendAsyncMessage("FormAutoComplete:Focus");
     }
-  },
-};
+    */
+  }
+}
--- a/toolkit/actors/moz.build
+++ b/toolkit/actors/moz.build
@@ -16,16 +16,18 @@ with Files('KeyPressEventModelCheckerChi
 TESTING_JS_MODULES += [
     'TestChild.jsm',
     'TestParent.jsm',
 ]
 
 FINAL_TARGET_FILES.actors += [
     'AudioPlaybackChild.jsm',
     'AudioPlaybackParent.jsm',
+    'AutoCompleteChild.jsm',
+    'AutoCompleteParent.jsm',
     'AutoplayChild.jsm',
     'AutoplayParent.jsm',
     'BrowserElementChild.jsm',
     'BrowserElementParent.jsm',
     'ControllersChild.jsm',
     'DateTimePickerChild.jsm',
     'DateTimePickerParent.jsm',
     'ExtFindChild.jsm',
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -61,16 +61,21 @@ ChromeUtils.defineModuleGetter(
   "InsecurePasswordUtils",
   "resource://gre/modules/InsecurePasswordUtils.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "ContentDOMReference",
   "resource://gre/modules/ContentDOMReference.jsm"
 );
+ChromeUtils.defineModuleGetter(
+  this,
+  "AutoCompleteChild",
+  "resource://gre/actors/AutoCompleteChild.jsm"
+);
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "gNetUtil",
   "@mozilla.org/network/util;1",
   "nsINetUtil"
 );
 
@@ -257,16 +262,86 @@ const observer = {
       }
     }
   },
 };
 
 // Add this observer once for the process.
 Services.obs.addObserver(observer, "autocomplete-did-enter-text");
 
+let gAutoCompleteListener = {
+  // Input element on which enter keydown event was fired.
+  keyDownEnterForInput: null,
+
+  added: false,
+
+  init() {
+    if (!this.added) {
+      AutoCompleteChild.addPopupStateListener((...args) => {
+        this.popupStateListener(...args);
+      });
+      this.added = true;
+    }
+  },
+
+  popupStateListener(messageName, data, target) {
+    switch (messageName) {
+      case "FormAutoComplete:PopupOpened": {
+        let { chromeEventHandler } = target.docShell;
+        chromeEventHandler.addEventListener("keydown", this, true);
+        break;
+      }
+
+      case "FormAutoComplete:PopupClosed": {
+        this.onPopupClosed(
+          data.selectedRowStyle,
+          target.docShell.messageManager
+        );
+        let { chromeEventHandler } = target.docShell;
+        chromeEventHandler.removeEventListener("keydown", this, true);
+        break;
+      }
+    }
+  },
+
+  handleEvent(event) {
+    if (event.type != "keydown") {
+      return;
+    }
+
+    let focusedElement = LoginManagerContent._formFillService.focusedInput;
+    if (
+      event.keyCode != event.DOM_VK_RETURN ||
+      focusedElement != event.target
+    ) {
+      this.keyDownEnterForInput = null;
+      return;
+    }
+    this.keyDownEnterForInput = focusedElement;
+  },
+
+  onPopupClosed(selectedRowStyle, mm) {
+    let focusedElement = LoginManagerContent._formFillService.focusedInput;
+    let eventTarget = this.keyDownEnterForInput;
+    if (
+      !eventTarget ||
+      eventTarget !== focusedElement ||
+      selectedRowStyle != "loginsFooter"
+    ) {
+      this.keyDownEnterForInput = null;
+      return;
+    }
+    let hostname = eventTarget.ownerDocument.documentURIObject.host;
+    mm.sendAsyncMessage("PasswordManager:OpenPreferences", {
+      hostname,
+      entryPoint: "autocomplete",
+    });
+  },
+};
+
 // This object maps to the "child" process (even in the single-process case).
 this.LoginManagerContent = {
   __formFillService: null, // FormFillController, for username autocompleting
   get _formFillService() {
     if (!this.__formFillService) {
       this.__formFillService = Cc[
         "@mozilla.org/satchel/form-fill-controller;1"
       ].getService(Ci.nsIFormFillController);
@@ -309,19 +384,16 @@ this.LoginManagerContent = {
   _onVisibleTasksByDocument: new WeakMap(),
 
   // Map from form login requests to information about that request.
   _requests: new Map(),
 
   // Number of outstanding requests to each manager.
   _managers: new Map(),
 
-  // Input element on which enter keydown event was fired.
-  _keyDownEnterForInput: null,
-
   _takeRequest(msg) {
     let data = msg.data;
     let request = this._requests.get(data.requestId);
 
     this._requests.delete(data.requestId);
 
     let count = this._managers.get(msg.target);
     if (--count === 0) {
@@ -395,46 +467,16 @@ this.LoginManagerContent = {
       dismissed,
     });
     log(
       "_compareAndUpdatePreviouslySentValues: values not equivalent, returning false"
     );
     return false;
   },
 
-  _onKeyDown(event) {
-    let focusedElement = LoginManagerContent._formFillService.focusedInput;
-    if (
-      event.keyCode != event.DOM_VK_RETURN ||
-      focusedElement != event.target
-    ) {
-      this._keyDownEnterForInput = null;
-      return;
-    }
-    LoginManagerContent._keyDownEnterForInput = focusedElement;
-  },
-
-  _onPopupClosed(selectedRowStyle, mm) {
-    let focusedElement = LoginManagerContent._formFillService.focusedInput;
-    let eventTarget = LoginManagerContent._keyDownEnterForInput;
-    if (
-      !eventTarget ||
-      eventTarget !== focusedElement ||
-      selectedRowStyle != "loginsFooter"
-    ) {
-      this._keyDownEnterForInput = null;
-      return;
-    }
-    let hostname = eventTarget.ownerDocument.documentURIObject.host;
-    mm.sendAsyncMessage("PasswordManager:OpenPreferences", {
-      hostname,
-      entryPoint: "autocomplete",
-    });
-  },
-
   receiveMessage(msg, topWindow) {
     if (msg.name == "PasswordManager:fillForm") {
       this.fillForm({
         topDocument: topWindow.document,
         loginFormOrigin: msg.data.loginFormOrigin,
         loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
         recipes: msg.data.recipes,
         inputElementIdentifier: msg.data.inputElementIdentifier,
@@ -490,33 +532,16 @@ this.LoginManagerContent = {
         );
         if (inputElement) {
           this._generatedPasswordFilledOrEdited(inputElement);
         } else {
           log("Could not resolve inputElementIdentifier to a living element.");
         }
         break;
       }
-
-      case "FormAutoComplete:PopupOpened": {
-        let { chromeEventHandler } = msg.target.docShell;
-        chromeEventHandler.addEventListener("keydown", this._onKeyDown, true);
-        break;
-      }
-
-      case "FormAutoComplete:PopupClosed": {
-        this._onPopupClosed(msg.data.selectedRowStyle, msg.target);
-        let { chromeEventHandler } = msg.target.docShell;
-        chromeEventHandler.removeEventListener(
-          "keydown",
-          this._onKeyDown,
-          true
-        );
-        break;
-      }
     }
   },
 
   /**
    * Get relevant logins and recipes from the parent
    *
    * @param {HTMLFormElement} form - form to get login data for
    * @param {Object} options
@@ -575,18 +600,17 @@ this.LoginManagerContent = {
       actionOrigin,
       searchString: aSearchString,
       previousResult,
       isSecure: InsecurePasswordUtils.isFormSecure(form),
       isPasswordField: aElement.type == "password",
     };
 
     if (LoginHelper.showAutoCompleteFooter) {
-      messageManager.addMessageListener("FormAutoComplete:PopupOpened", this);
-      messageManager.addMessageListener("FormAutoComplete:PopupClosed", this);
+      gAutoCompleteListener.init();
     }
 
     return this._sendRequest(
       messageManager,
       requestData,
       "PasswordManager:autoCompleteLogins",
       messageData
     );
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -14,21 +14,16 @@ const LoginInfo = new Components.Constru
   Ci.nsILoginInfo,
   "init"
 );
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
 
 ChromeUtils.defineModuleGetter(
   this,
-  "AutoCompletePopup",
-  "resource://gre/modules/AutoCompletePopup.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
   "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "LoginHelper",
   "resource://gre/modules/LoginHelper.jsm"
 );
@@ -181,17 +176,17 @@ this.LoginManagerParent = {
 
       case "PasswordManager:autoCompleteLogins": {
         this.doAutocompleteSearch(data, msg.target);
         break;
       }
 
       case "PasswordManager:removeLogin": {
         let login = LoginHelper.vanillaObjectToLogin(data.login);
-        AutoCompletePopup.removeLogin(login);
+        Services.logins.removeLogin(login);
         break;
       }
 
       case "PasswordManager:OpenPreferences": {
         LoginHelper.openPasswordManager(msg.target.ownerGlobal, {
           filterString: msg.data.hostname,
           entryPoint: msg.data.entryPoint,
         });
--- a/toolkit/components/satchel/moz.build
+++ b/toolkit/components/satchel/moz.build
@@ -23,17 +23,16 @@ SOURCES += [
     'nsFormFillController.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '../build',
 ]
 
 EXTRA_JS_MODULES += [
-    'AutoCompletePopup.jsm',
     'FormAutoComplete.jsm',
     'FormHistory.jsm',
     'FormHistoryStartup.jsm',
     'InputListAutoComplete.jsm',
     'nsFormAutoCompleteResult.jsm',
 ]
 
 XPCOM_MANIFESTS += [
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -62,25 +62,24 @@ static nsIFormAutoComplete* GetFormAutoC
       ClearOnShutdown(&sInstance);
       sInitialized = true;
     }
   }
   return sInstance;
 }
 
 NS_IMPL_CYCLE_COLLECTION(nsFormFillController, mController, mLoginManagerAC,
-                         mLoginReputationService, mFocusedPopup, mDocShells,
+                         mLoginReputationService, mFocusedPopup,
                          mPopups, mLastListener, mLastFormAutoComplete)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFormFillController)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFormFillController)
   NS_INTERFACE_MAP_ENTRY(nsIFormFillController)
   NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteInput)
   NS_INTERFACE_MAP_ENTRY(nsIAutoCompleteSearch)
-  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
   NS_INTERFACE_MAP_ENTRY(nsIFormAutoCompleteObserver)
   NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFormFillController)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFormFillController)
 
 nsFormFillController::nsFormFillController()
@@ -109,23 +108,16 @@ nsFormFillController::~nsFormFillControl
     mListNode->RemoveMutationObserver(this);
     mListNode = nullptr;
   }
   if (mFocusedInput) {
     MaybeRemoveMutationObserver(mFocusedInput);
     mFocusedInput = nullptr;
   }
   RemoveForDocument(nullptr);
-
-  // Remove ourselves as a focus listener from all cached docShells
-  uint32_t count = mDocShells.Length();
-  for (uint32_t i = 0; i < count; ++i) {
-    nsCOMPtr<nsPIDOMWindowOuter> window = GetWindowForDocShell(mDocShells[i]);
-    RemoveWindowListeners(window);
-  }
 }
 
 ////////////////////////////////////////////////////////////////////////
 //// nsIMutationObserver
 //
 
 MOZ_CAN_RUN_SCRIPT_BOUNDARY
 void nsFormFillController::AttributeChanged(mozilla::dom::Element* aElement,
@@ -212,66 +204,51 @@ void nsFormFillController::MaybeRemoveMu
     aNode->RemoveMutationObserver(this);
   }
 }
 
 ////////////////////////////////////////////////////////////////////////
 //// nsIFormFillController
 
 NS_IMETHODIMP
-nsFormFillController::AttachToBrowser(nsIDocShell* aDocShell,
-                                      nsIAutoCompletePopup* aPopup) {
+nsFormFillController::AttachToDocument(Document* aDocument,
+                                       nsIAutoCompletePopup* aPopup) {
   MOZ_LOG(sLogger, LogLevel::Debug,
-          ("AttachToBrowser for docShell %p with popup %p", aDocShell, aPopup));
-  NS_ENSURE_TRUE(aDocShell && aPopup, NS_ERROR_ILLEGAL_VALUE);
+          ("AttachToDocument for document %p with popup %p", aDocument, aPopup));
+  NS_ENSURE_TRUE(aDocument && aPopup, NS_ERROR_ILLEGAL_VALUE);
 
-  mDocShells.AppendElement(aDocShell);
-  mPopups.AppendElement(aPopup);
-
-  // Listen for focus events on the domWindow of the docShell
-  nsCOMPtr<nsPIDOMWindowOuter> window = GetWindowForDocShell(aDocShell);
-  AddWindowListeners(window);
+  mPopups.Put(aDocument, aPopup);
 
   nsFocusManager* fm = nsFocusManager::GetFocusManager();
   if (fm) {
     nsCOMPtr<nsIContent> focusedContent = fm->GetFocusedElement();
     HandleFocus(
         MOZ_KnownLive(HTMLInputElement::FromNodeOrNull(focusedContent)));
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsFormFillController::AttachPopupElementToBrowser(nsIDocShell* aDocShell,
-                                                  dom::Element* aPopupEl) {
+nsFormFillController::AttachPopupElementToDocument(Document* aDocument,
+                                                   dom::Element* aPopupEl) {
   MOZ_LOG(sLogger, LogLevel::Debug,
-          ("AttachPopupElementToBrowser for docShell %p with popup %p",
-           aDocShell, aPopupEl));
-  NS_ENSURE_TRUE(aDocShell && aPopupEl, NS_ERROR_ILLEGAL_VALUE);
+          ("AttachPopupElementToDocument for document %p with popup %p",
+           aDocument, aPopupEl));
+  NS_ENSURE_TRUE(aDocument && aPopupEl, NS_ERROR_ILLEGAL_VALUE);
 
   nsCOMPtr<nsIAutoCompletePopup> popup = aPopupEl->AsAutoCompletePopup();
   NS_ENSURE_STATE(popup);
 
-  return AttachToBrowser(aDocShell, popup);
+  return AttachToDocument(aDocument, popup);
 }
 
 NS_IMETHODIMP
-nsFormFillController::DetachFromBrowser(nsIDocShell* aDocShell) {
-  int32_t index = GetIndexOfDocShell(aDocShell);
-  NS_ENSURE_TRUE(index >= 0, NS_ERROR_FAILURE);
-
-  // Stop listening for focus events on the domWindow of the docShell
-  nsCOMPtr<nsPIDOMWindowOuter> window =
-      GetWindowForDocShell(mDocShells.SafeElementAt(index));
-  RemoveWindowListeners(window);
-
-  mDocShells.RemoveElementAt(index);
-  mPopups.RemoveElementAt(index);
-
+nsFormFillController::DetachFromDocument(Document* aDocument) {
+  mPopups.Remove(aDocument);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsFormFillController::MarkAsLoginManagerField(HTMLInputElement* aInput) {
   /*
    * The Login Manager can supply autocomplete results for username fields,
    * when a user has multiple logins stored for a site. It uses this
@@ -831,17 +808,17 @@ nsFormFillController::OnSearchCompletion
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////
 //// nsIDOMEventListener
 
 NS_IMETHODIMP
-nsFormFillController::HandleEvent(Event* aEvent) {
+nsFormFillController::HandleFormEvent(Event* aEvent) {
   WidgetEvent* internalEvent = aEvent->WidgetEventPtr();
   NS_ENSURE_STATE(internalEvent);
 
   switch (internalEvent->mMessage) {
     case eFocus:
       return Focus(aEvent);
     case eMouseDown:
       return MouseDown(aEvent);
@@ -1234,125 +1211,31 @@ nsFormFillController::ShowPopup() {
 }
 
 NS_IMETHODIMP nsFormFillController::GetPasswordPopupAutomaticallyOpened(
     bool* _retval) {
   *_retval = mPasswordPopupAutomaticallyOpened;
   return NS_OK;
 }
 
-////////////////////////////////////////////////////////////////////////
-//// nsFormFillController
-
-void nsFormFillController::AddWindowListeners(nsPIDOMWindowOuter* aWindow) {
-  MOZ_LOG(sLogger, LogLevel::Debug,
-          ("AddWindowListeners for window %p", aWindow));
-  if (!aWindow) {
-    return;
-  }
-
-  EventTarget* target = aWindow->GetChromeEventHandler();
-  if (!target) {
-    return;
-  }
-
-  EventListenerManager* elm = target->GetOrCreateListenerManager();
-  if (NS_WARN_IF(!elm)) {
-    return;
-  }
-
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("focus"),
-                              TrustedEventsAtCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("blur"),
-                              TrustedEventsAtCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("pagehide"),
-                              TrustedEventsAtCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("mousedown"),
-                              TrustedEventsAtCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("input"),
-                              TrustedEventsAtCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("keydown"),
-                              TrustedEventsAtCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("keypress"),
-                              TrustedEventsAtSystemGroupCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("compositionstart"),
-                              TrustedEventsAtCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("compositionend"),
-                              TrustedEventsAtCapture());
-  elm->AddEventListenerByType(this, NS_LITERAL_STRING("contextmenu"),
-                              TrustedEventsAtCapture());
-
-  // Note that any additional listeners added should ensure that they ignore
-  // untrusted events, which might be sent by content that's up to no good.
-}
-
-void nsFormFillController::RemoveWindowListeners(nsPIDOMWindowOuter* aWindow) {
-  MOZ_LOG(sLogger, LogLevel::Debug,
-          ("RemoveWindowListeners for window %p", aWindow));
-  if (!aWindow) {
-    return;
-  }
-
-  StopControllingInput();
-
-  RefPtr<Document> doc = aWindow->GetDoc();
-  RemoveForDocument(doc);
-
-  EventTarget* target = aWindow->GetChromeEventHandler();
-  if (!target) {
-    return;
-  }
-
-  EventListenerManager* elm = target->GetOrCreateListenerManager();
-  if (NS_WARN_IF(!elm)) {
-    return;
-  }
-
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("focus"),
-                                 TrustedEventsAtCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("blur"),
-                                 TrustedEventsAtCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("pagehide"),
-                                 TrustedEventsAtCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("mousedown"),
-                                 TrustedEventsAtCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("input"),
-                                 TrustedEventsAtCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("keydown"),
-                                 TrustedEventsAtCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("keypress"),
-                                 TrustedEventsAtSystemGroupCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("compositionstart"),
-                                 TrustedEventsAtCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("compositionend"),
-                                 TrustedEventsAtCapture());
-  elm->RemoveEventListenerByType(this, NS_LITERAL_STRING("contextmenu"),
-                                 TrustedEventsAtCapture());
-}
-
 void nsFormFillController::StartControllingInput(HTMLInputElement* aInput) {
   MOZ_LOG(sLogger, LogLevel::Verbose, ("StartControllingInput for %p", aInput));
   // Make sure we're not still attached to an input
   StopControllingInput();
 
-  if (!mController) {
+  if (!mController || !aInput) {
     return;
   }
 
-  // Find the currently focused docShell
-  nsCOMPtr<nsIDocShell> docShell = GetDocShellForInput(aInput);
-  int32_t index = GetIndexOfDocShell(docShell);
-  if (index < 0) {
+  nsCOMPtr<nsIAutoCompletePopup> popup = mPopups.Get(aInput->OwnerDoc());
+  if (!popup) {
     return;
   }
 
-  MOZ_ASSERT(aInput, "How did we get a docshell index??");
-
-  // Cache the popup for the focused docShell
-  mFocusedPopup = mPopups.SafeElementAt(index);
+  mFocusedPopup = popup;
 
   aInput->AddMutationObserverUnlessExists(this);
   mFocusedInput = aInput;
 
   if (Element* list = mFocusedInput->GetList()) {
     list->AddMutationObserverUnlessExists(this);
     mListNode = list;
   }
@@ -1405,45 +1288,8 @@ nsIDocShell* nsFormFillController::GetDo
     HTMLInputElement* aInput) {
   NS_ENSURE_TRUE(aInput, nullptr);
 
   nsCOMPtr<nsPIDOMWindowOuter> win = aInput->OwnerDoc()->GetWindow();
   NS_ENSURE_TRUE(win, nullptr);
 
   return win->GetDocShell();
 }
-
-nsPIDOMWindowOuter* nsFormFillController::GetWindowForDocShell(
-    nsIDocShell* aDocShell) {
-  nsCOMPtr<nsIContentViewer> contentViewer;
-  aDocShell->GetContentViewer(getter_AddRefs(contentViewer));
-  NS_ENSURE_TRUE(contentViewer, nullptr);
-
-  RefPtr<Document> doc = contentViewer->GetDocument();
-  NS_ENSURE_TRUE(doc, nullptr);
-
-  return doc->GetWindow();
-}
-
-int32_t nsFormFillController::GetIndexOfDocShell(nsIDocShell* aDocShell) {
-  if (!aDocShell) {
-    return -1;
-  }
-
-  // Loop through our cached docShells looking for the given docShell
-  uint32_t count = mDocShells.Length();
-  for (uint32_t i = 0; i < count; ++i) {
-    if (mDocShells[i] == aDocShell) {
-      return i;
-    }
-  }
-
-  // Recursively check the parent docShell of this one
-  nsCOMPtr<nsIDocShellTreeItem> treeItem = aDocShell;
-  nsCOMPtr<nsIDocShellTreeItem> parentItem;
-  treeItem->GetInProcessParent(getter_AddRefs(parentItem));
-  if (parentItem) {
-    nsCOMPtr<nsIDocShell> parentShell = do_QueryInterface(parentItem);
-    return GetIndexOfDocShell(parentShell);
-  }
-
-  return -1;
-}
--- a/toolkit/components/satchel/nsFormFillController.h
+++ b/toolkit/components/satchel/nsFormFillController.h
@@ -10,16 +10,17 @@
 #include "nsIAutoCompleteInput.h"
 #include "nsIAutoCompleteSearch.h"
 #include "nsIAutoCompleteController.h"
 #include "nsIAutoCompletePopup.h"
 #include "nsIFormAutoComplete.h"
 #include "nsIDOMEventListener.h"
 #include "nsCOMPtr.h"
 #include "nsDataHashtable.h"
+#include "nsInterfaceHashtable.h"
 #include "nsIDocShell.h"
 #include "nsILoginAutoCompleteSearch.h"
 #include "nsIMutationObserver.h"
 #include "nsTArray.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsILoginReputation.h"
 
 // X.h defines KeyPress
@@ -35,26 +36,24 @@ namespace mozilla {
 namespace dom {
 class HTMLInputElement;
 }  // namespace dom
 }  // namespace mozilla
 
 class nsFormFillController final : public nsIFormFillController,
                                    public nsIAutoCompleteInput,
                                    public nsIAutoCompleteSearch,
-                                   public nsIDOMEventListener,
                                    public nsIFormAutoCompleteObserver,
                                    public nsIMutationObserver {
  public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_NSIFORMFILLCONTROLLER
   NS_DECL_NSIAUTOCOMPLETESEARCH
   NS_DECL_NSIAUTOCOMPLETEINPUT
   NS_DECL_NSIFORMAUTOCOMPLETEOBSERVER
-  NS_DECL_NSIDOMEVENTLISTENER
   NS_DECL_NSIMUTATIONOBSERVER
 
   NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsFormFillController,
                                            nsIFormFillController)
 
   MOZ_CAN_RUN_SCRIPT nsresult Focus(mozilla::dom::Event* aEvent);
   MOZ_CAN_RUN_SCRIPT nsresult KeyDown(mozilla::dom::Event* aKeyEvent);
   MOZ_CAN_RUN_SCRIPT nsresult KeyPress(mozilla::dom::Event* aKeyEvent);
@@ -91,18 +90,16 @@ class nsFormFillController final : publi
                                         nsIAutoCompleteResult** aResult);
 
   MOZ_CAN_RUN_SCRIPT void RevalidateDataList();
   bool RowMatch(nsFormHistory* aHistory, uint32_t aIndex,
                 const nsAString& aInputName, const nsAString& aInputValue);
 
   inline nsIDocShell* GetDocShellForInput(
       mozilla::dom::HTMLInputElement* aInput);
-  inline nsPIDOMWindowOuter* GetWindowForDocShell(nsIDocShell* aDocShell);
-  inline int32_t GetIndexOfDocShell(nsIDocShell* aDocShell);
 
   void MaybeRemoveMutationObserver(nsINode* aNode);
 
   void RemoveForDocument(mozilla::dom::Document* aDoc);
 
   bool IsTextControl(nsINode* aNode);
 
   nsresult StartQueryLoginReputation(mozilla::dom::HTMLInputElement* aInput);
@@ -114,18 +111,17 @@ class nsFormFillController final : publi
   nsCOMPtr<nsILoginReputationService> mLoginReputationService;
   mozilla::dom::HTMLInputElement* mFocusedInput;
 
   // mListNode is a <datalist> element which, is set, has the form fill
   // controller as a mutation observer for it.
   nsINode* mListNode;
   nsCOMPtr<nsIAutoCompletePopup> mFocusedPopup;
 
-  nsTArray<nsCOMPtr<nsIDocShell> > mDocShells;
-  nsTArray<nsCOMPtr<nsIAutoCompletePopup> > mPopups;
+  nsInterfaceHashtable<nsRefPtrHashKey<mozilla::dom::Document>, nsIAutoCompletePopup> mPopups;
 
   // The observer passed to StartSearch. It will be notified when the search is
   // complete or the data from a datalist changes.
   nsCOMPtr<nsIAutoCompleteObserver> mLastListener;
 
   // This is cleared by StopSearch().
   nsCOMPtr<nsIFormAutoComplete> mLastFormAutoComplete;
   nsString mLastSearchString;
--- a/toolkit/components/satchel/nsIFormFillController.idl
+++ b/toolkit/components/satchel/nsIFormFillController.idl
@@ -1,18 +1,19 @@
 /* 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/. */
 
 #include "nsISupports.idl"
 
-interface nsIDocShell;
 interface nsIAutoCompletePopup;
 
+webidl Document;
 webidl Element;
+webidl Event;
 webidl HTMLInputElement;
 
 /*
  * nsIFormFillController is an interface for controlling form fill behavior
  * on HTML documents.  Any number of docShells can be controller concurrently.
  * While a docShell is attached, all HTML documents that are loaded within it
  * will have a focus listener attached that will listen for when a text input
  * is focused.  When this happens, the input will be bound to the
@@ -29,32 +30,32 @@ interface nsIFormFillController : nsISup
 
   /*
    * Whether the autocomplete popup on a password field was automatically opened
    * by the form fill controller (upon focus).
    */
   readonly attribute boolean passwordPopupAutomaticallyOpened;
 
   /*
-   * Start controlling form fill behavior for the given browser
+   * Start controlling form fill behavior for the given document
    *
-   * @param docShell - The docShell to attach to
+   * @param document - The document to attach to
    * @param popup - The popup to show when autocomplete results are available
    */
   [can_run_script]
-  void attachToBrowser(in nsIDocShell docShell, in nsIAutoCompletePopup popup);
+  void attachToDocument(in Document document, in nsIAutoCompletePopup popup);
   [can_run_script]
-  void attachPopupElementToBrowser(in nsIDocShell docShell, in Element popup);
+  void attachPopupElementToDocument(in Document document, in Element popup);
 
   /*
    * Stop controlling form fill behavior for the given browser
    *
-   * @param docShell - The docShell to detach from
+   * @param document - The document to detach from
    */
-  [can_run_script] void detachFromBrowser(in nsIDocShell docShell);
+  [can_run_script] void detachFromDocument(in Document document);
 
   /*
    * Mark the specified <input> element as being managed by password manager.
    * Autocomplete requests will be handed off to the password manager, and will
    * not be stored in form history.
    *
    * @param aInput - The HTML <input> element to tag
    */
@@ -67,9 +68,11 @@ interface nsIFormFillController : nsISup
    * @param aInput - The HTML <input> element to mark
    */
   [can_run_script] void markAsAutofillField(in HTMLInputElement aInput);
 
   /*
    * Open the autocomplete popup, if possible.
    */
   [can_run_script] void showPopup();
+
+  [can_run_script] void handleFormEvent(in Event aEvent);
 };
--- a/toolkit/components/satchel/test/browser/browser_close_tab.js
+++ b/toolkit/components/satchel/test/browser/browser_close_tab.js
@@ -30,37 +30,17 @@ add_task(async function test() {
     });
 
     // show popup
     await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
     await TestUtils.waitForCondition(() => {
       return autoCompletePopup.popupOpen;
     });
 
-    let listener;
-    let errorLogPromise = new Promise(resolve => {
-      listener = ({ message }) => {
-        const ERROR_MSG =
-          "AutoCompletePopup: No messageManager for " +
-          'message "FormAutoComplete:PopupClosed"';
-        if (message.includes(ERROR_MSG)) {
-          Assert.ok(
-            true,
-            "Got the error message for inexistent messageManager."
-          );
-          Services.console.unregisterListener(listener);
-          resolve();
-        }
-      };
-    });
-    Services.console.registerListener(listener);
-
     gBrowser.removeCurrentTab();
 
     await TestUtils.waitForCondition(() => {
       return !autoCompletePopup.popupOpen;
     });
 
     Assert.ok(!autoCompletePopup.popupOpen, "Ensure the popup is closed.");
-
-    await errorLogPromise;
   });
 });
--- a/toolkit/components/satchel/test/test_datalist_shadow_dom.html
+++ b/toolkit/components/satchel/test/test_datalist_shadow_dom.html
@@ -110,14 +110,14 @@ async function runTests() {
   testNum++;
   checkMenuEntries(["Second", "Secomundo"]);
   synthesizeKey("KEY_ArrowDown");
   synthesizeKey("KEY_Enter");
   checkForm("Second");
   SimpleTest.finish();
 }
 
-window.onload = runTests();
+window.onpageshow = runTests;
 
 </script>
 </pre>
 </body>
 </html>
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -2,43 +2,28 @@
 /* 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/. */
 
 /* eslint-env mozilla/frame-script */
 /* eslint no-unused-vars: ["error", {args: "none"}] */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-var { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
 var { ActorManagerChild } = ChromeUtils.import(
   "resource://gre/modules/ActorManagerChild.jsm"
 );
 
 ActorManagerChild.attach(this);
 
 ChromeUtils.defineModuleGetter(
   this,
-  "AutoCompletePopup",
-  "resource://gre/modules/AutoCompletePopupContent.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
   "AutoScrollController",
   "resource://gre/modules/AutoScrollController.jsm"
 );
 
-XPCOMUtils.defineLazyServiceGetter(
-  this,
-  "formFill",
-  "@mozilla.org/satchel/form-fill-controller;1",
-  "nsIFormFillController"
-);
-
 var global = this;
 
 var AutoScrollListener = {
   handleEvent(event) {
     if (event.isTrusted & !event.defaultPrevented && event.button == 1) {
       if (!this._controller) {
         this._controller = new AutoScrollController(global);
       }
@@ -47,53 +32,8 @@ var AutoScrollListener = {
   },
 };
 Services.els.addSystemEventListener(
   global,
   "mousedown",
   AutoScrollListener,
   true
 );
-
-let AutoComplete = {
-  _connected: false,
-
-  init() {
-    addEventListener("unload", this, { once: true });
-    addEventListener("DOMContentLoaded", this, { once: true });
-    // WebExtension browserAction is preloaded and does not receive DCL, wait
-    // on pageshow so we can hookup the formfill controller.
-    addEventListener("pageshow", this, { capture: true, once: true });
-
-    XPCOMUtils.defineLazyProxy(
-      this,
-      "popup",
-      () => new AutoCompletePopup(global),
-      { QueryInterface: null }
-    );
-    this.init = null;
-  },
-
-  handleEvent(event) {
-    switch (event.type) {
-      case "DOMContentLoaded":
-      case "pageshow":
-        // We need to wait for a content viewer to be available
-        // before we can attach our AutoCompletePopup handler,
-        // since nsFormFillController assumes one will exist
-        // when we call attachToBrowser.
-        if (!this._connected) {
-          formFill.attachToBrowser(docShell, this.popup);
-          this._connected = true;
-        }
-        break;
-
-      case "unload":
-        if (this._connected) {
-          formFill.detachFromBrowser(docShell);
-          this._connected = false;
-        }
-        break;
-    }
-  },
-};
-
-AutoComplete.init();
--- a/toolkit/content/tests/chrome/file_autocomplete_with_composition.js
+++ b/toolkit/content/tests/chrome/file_autocomplete_with_composition.js
@@ -63,22 +63,27 @@ nsDoTestsForAutoCompleteWithComposition.
     var test = this._tests[this._testingIndex];
     if (
       this._controller.input.completeDefaultIndex != test.completeDefaultIndex
     ) {
       this._controller.input.completeDefaultIndex = test.completeDefaultIndex;
     }
     test.execute(this._window);
 
-    waitForCondition(() => {
-      return (
+    if (test.popup) {
+      waitForCondition(
+        () => this._controller.input.popupOpen,
+        this._checkResult.bind(this)
+      );
+    } else {
+      waitForCondition(() => {
         this._controller.searchStatus >=
-        Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH
-      );
-    }, this._checkResult.bind(this));
+          Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+      }, this._checkResult.bind(this));
+    }
   },
 
   _checkResult() {
     var test = this._tests[this._testingIndex];
     this._is(
       this._getTargetValue(),
       test.value,
       this._description + ", " + test.description + ": value"
--- a/toolkit/modules/ActorManagerParent.jsm
+++ b/toolkit/modules/ActorManagerParent.jsm
@@ -111,16 +111,60 @@ let ACTORS = {
     child: {
       moduleURI: "resource://gre/actors/AudioPlaybackChild.jsm",
       observers: ["audio-playback"],
     },
 
     allFrames: true,
   },
 
+  AutoComplete: {
+    parent: {
+      moduleURI: "resource://gre/actors/AutoCompleteParent.jsm",
+      messages: [
+        "FormAutoComplete:SelectBy",
+        "FormAutoComplete:SetSelectedIndex",
+        "FormAutoComplete:MaybeOpenPopup",
+        "FormAutoComplete:Invalidate",
+        "FormAutoComplete:ClosePopup",
+        "FormAutoComplete:Disconnect",
+        // These two messages are also used, but are currently synchronous calls
+        // through the per-process message manager.
+        // "FormAutoComplete:GetSelectedIndex",
+        // "FormAutoComplete:SelectBy"
+      ],
+    },
+
+    child: {
+      moduleURI: "resource://gre/actors/AutoCompleteChild.jsm",
+      events: {
+        DOMContentLoaded: {},
+        pageshow: { capture: true },
+        pagehide: { capture: true },
+        unload: { capture: true },
+        focus: { capture: true },
+        blur: { capture: true },
+        mousedown: { capture: true },
+        input: { capture: true },
+        keydown: { capture: true },
+        keypress: { capture: true, mozSystemGroup: true },
+        compositionstart: { capture: true },
+        compositionend: { capture: true },
+        contextmenu: { capture: true },
+      },
+      messages: [
+        "FormAutoComplete:HandleEnter",
+        "FormAutoComplete:PopupClosed",
+        "FormAutoComplete:PopupOpened",
+      ],
+    },
+
+    allFrames: true,
+  },
+
   Autoplay: {
     parent: {
       moduleURI: "resource://gre/actors/AutoplayParent.jsm",
     },
 
     child: {
       moduleURI: "resource://gre/actors/AutoplayChild.jsm",
       events: {
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -160,17 +160,16 @@ with Files('docs/**'):
 
 EXTRA_JS_MODULES += [
     'AboutPagesUtils.jsm',
     'ActorChild.jsm',
     'ActorManagerChild.jsm',
     'ActorManagerParent.jsm',
     'AppMenuNotifications.jsm',
     'AsyncPrefs.jsm',
-    'AutoCompletePopupContent.jsm',
     'AutoScrollController.jsm',
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CanonicalJSON.jsm',
     'CertUtils.jsm',
     'CharsetMenu.jsm',
     'Color.jsm',
     'Console.jsm',