Bug 1180944 - Implement one-off searches from Awesomebar. r?mak draft
authorDrew Willcoxon <adw@mozilla.com>
Fri, 17 Jun 2016 21:55:42 -0700
changeset 379833 d5822f6d3832a65ce7910dffdd7bd4ff3f3980c7
parent 379832 870e3a7e84496627ab66053772107ff4c138567a
child 523585 c9e78b9ace4b1de88429f349e359a7af06099b6a
push id21070
push userdwillcoxon@mozilla.com
push dateSat, 18 Jun 2016 04:56:01 +0000
reviewersmak
bugs1180944
milestone50.0a1
Bug 1180944 - Implement one-off searches from Awesomebar. r?mak More big changes for you Marco, sorry... The previous commit didn't handle engine changes completely right. For example, if you typed something, hit Tab to select a one-off, typed some more, and hit Return, the search would happen with the current engine, not the selected one-off. The bottom line is that one-offs make the engineName included in the moz-action URI by UnifiedComplete pretty meaningless. It can't be a static thing anymore. So when a new one-off is selected, should we change the value of all searchengine results in the autocomplete controller, or the front end? (If we don't change them at all, that's kind of basically the same as changing them in the front end since ultimately the URL that's loaded had better match the selected one-off.) This commit does it in the front end. The autocomplete popup now has an overrideSearchEngineName property that _adjustAcItem checks. If the property is non-null, then _adjustAcItem changes its item's url attribute and the engine name in its label. handleCommand in urlbar now uses the url attribute of the selected popup item when loading URLs. If there's no selection, then it uses the input value as before. At first I tried keeping input.value (or _value) up to date for the selected item, but that was a pain. I think it's simpler to keep the url attributes of items updated and then use those. But since handleCommand can be called when the popup is already closed, I modified the relevant idl to pass a selected popup index to it. This commit also funnels all URL loads in urlbar through handleCommand. And it splits out the continueOperation nested function into its own method called _loadURL. Since handleEnter ends up calling handleCommand, and handle command needs an event to determine where to open the URL, I modified handleEnter to take an event. I think these two idl changes make sense in general, not only for this bug. For the value -> textValue changes in the tests, I made those changes when I was trying to keep input.value up to date. I'm not sure they're necessary anymore, but they seem like good changes anyway? Since textValue will always be what the user sees, but value may be a moz-action URI. MozReview-Commit-ID: y79gIO9TP3
browser/base/content/browser.xul
browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js
browser/base/content/test/urlbar/browser_bug1070778.js
browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js
browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
browser/base/content/test/urlbar/browser_urlbarOneOffs.js
browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
browser/base/content/test/urlbar/browser_urlbarStop.js
browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js
browser/base/content/urlbarBindings.xml
browser/components/search/content/search.xml
toolkit/components/autocomplete/nsAutoCompleteController.cpp
toolkit/components/autocomplete/nsAutoCompleteController.h
toolkit/components/autocomplete/nsIAutoCompleteController.idl
toolkit/components/autocomplete/nsIAutoCompleteInput.idl
toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.js
toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js
toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js
toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js
toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js
toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.js
toolkit/components/autocomplete/tests/unit/test_hiddenResult.js
toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js
toolkit/components/autocomplete/tests/unit/test_stopSearch.js
toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
toolkit/content/browser-child.js
toolkit/content/widgets/autocomplete.xml
xpfe/components/autocomplete/resources/content/autocomplete.xml
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -705,17 +705,17 @@
                      completeselectedindex="true"
                      shrinkdelay="250"
                      tabscrolling="true"
                      showcommentcolumn="true"
                      showimagecolumn="true"
                      enablehistory="true"
                      maxrows="10"
                      newlines="stripsurroundingwhitespace"
-                     ontextentered="this.handleCommand(param);"
+                     ontextentered="this.handleCommand(...args);"
                      ontextreverted="return this.handleRevert();"
                      pageproxystate="invalid"
                      onfocus="document.getElementById('identity-box').style.MozUserFocus= 'normal'"
                      onblur="setTimeout(() => { document.getElementById('identity-box').style.MozUserFocus = ''; }, 0);">
               <box id="notification-popup-box" hidden="true" align="center">
                 <image id="default-notification-icon" class="notification-anchor-icon" role="button"
                        aria-label="&urlbar.defaultNotificationAnchor.label;"/>
                 <image id="geo-notification-icon" class="notification-anchor-icon geo-icon" role="button"
--- a/browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js
+++ b/browser/base/content/test/urlbar/browser_autocomplete_edit_completed.js
@@ -26,20 +26,23 @@ add_task(function*() {
   let nextValue = gURLBar.controller.getFinalCompleteValueAt(nextIndex);
   is(list.selectedIndex, nextIndex, "The next item is selected.");
   is(gURLBar.value, nextValue, "The selected URL is completed.");
 
   info("Press backspace");
   EventUtils.synthesizeKey("VK_BACK_SPACE", {});
   yield promiseSearchComplete();
 
-  let editedValue = gURLBar.value;
+  let editedValue = gURLBar.textValue;
   is(list.selectedIndex, initialIndex, "The initial index is selected again.");
   isnot(editedValue, nextValue, "The URL has changed.");
 
+  let docLoad = waitForDocLoadAndStopIt("http://" + editedValue);
+
   info("Press return to load edited URL.");
   EventUtils.synthesizeKey("VK_RETURN", {});
   yield Promise.all([
     promisePopupHidden(gURLBar.popup),
-    waitForDocLoadAndStopIt("http://" + editedValue)]);
+    docLoad,
+  ]);
 
   gBrowser.removeTab(gBrowser.selectedTab);
 });
--- a/browser/base/content/test/urlbar/browser_bug1070778.js
+++ b/browser/base/content/test/urlbar/browser_bug1070778.js
@@ -38,17 +38,17 @@ add_task(function*() {
   is_selected(1);
   // Re-select keyword item
   EventUtils.synthesizeKey("VK_UP", {});
   is_selected(0);
 
   EventUtils.synthesizeKey("b", {});
   yield promiseSearchComplete();
 
-  is(gURLBar.value, "keyword ab", "urlbar should have expected input");
+  is(gURLBar.textValue, "keyword ab", "urlbar should have expected input");
 
   let result = gURLBar.popup.richlistbox.firstChild;
   isnot(result, null, "Should have first item");
   let uri = NetUtil.newURI(result.getAttribute("url"));
   is(uri.spec, makeActionURI("keyword", {url: "http://example.com/?q=ab", input: "keyword ab"}).spec, "Expect correct url");
 
   EventUtils.synthesizeKey("VK_ESCAPE", {});
   yield promisePopupHidden(gURLBar.popup);
--- a/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js
+++ b/browser/base/content/test/urlbar/browser_tabMatchesInAwesomebar_perwindowpb.js
@@ -65,17 +65,17 @@ function* runTest(aSourceWindow, aDestWi
   }
 
   let awaitTabSwitch;
   if (aExpectSwitch) {
     awaitTabSwitch = BrowserTestUtils.removeTab(testTab, {dontRemove: true})
   }
 
   // Execute the selected action.
-  controller.handleEnter(true);
+  controller.handleEnter(true, null);
   info("sent Enter command to the controller");
 
   if (aExpectSwitch) {
     // If we expect a tab switch then the current tab
     // will be closed and we switch to the other tab.
     yield awaitTabSwitch;
   } else {
     // If we don't expect a tab switch then wait for the tab to load.
--- a/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
+++ b/browser/base/content/test/urlbar/browser_urlbarAutoFillTrimURLs.js
@@ -39,18 +39,18 @@ function continue_test() {
     info(`Testing with input: ${aTyped}`);
     gURLBar.inputField.value = aTyped.substr(0, aTyped.length - 1);
     gURLBar.focus();
     gURLBar.selectionStart = aTyped.length - 1;
     gURLBar.selectionEnd = aTyped.length - 1;
 
     EventUtils.synthesizeKey(aTyped.substr(-1), {});
     waitForSearchComplete(function () {
-      info(`Got value: ${gURLBar.value}`);
-      is(gURLBar.value, aExpected, "Autofilled value is as expected");
+      info(`Got value: ${gURLBar.textValue}`);
+      is(gURLBar.textValue, aExpected, "Autofilled value is as expected");
       aCallback();
     });
   }
 
   test_autoFill("http://", "http://", function () {
     test_autoFill("http://au", "http://autofilltrimurl.com/", function () {
       test_autoFill("http://www.autofilltrimurl.com", "http://www.autofilltrimurl.com/", function () {
         // Now ensure selecting from the popup correctly trims.
--- a/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
+++ b/browser/base/content/test/urlbar/browser_urlbarOneOffs.js
@@ -178,17 +178,17 @@ add_task(function* oneOffClick() {
   let resultsPromise = promiseSearchResultsLoaded();
   EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
   yield resultsPromise;
 
   gBrowser.removeTab(gBrowser.selectedTab);
 });
 
 // Presses the Return key when a one-off is selected.
-add_task(function* oneOffClick() {
+add_task(function* oneOffReturn() {
   gBrowser.selectedTab = gBrowser.addTab();
 
   let typedValue = "foo";
   yield promiseAutocompleteResultPopup(typedValue, window, true);
 
   assertState(0, -1, typedValue);
 
   // Tab to select the first one-off.
--- a/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
@@ -20,17 +20,20 @@ add_task(function* prepare() {
     Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
   });
 });
 
 add_task(function* clickSuggestion() {
   gBrowser.selectedTab = gBrowser.addTab();
   gURLBar.focus();
   yield promiseAutocompleteResultPopup("foo");
-  let [idx, suggestion] = yield promiseFirstSuggestion();
+  let [idx, suggestion, engineName] = yield promiseFirstSuggestion();
+  Assert.equal(engineName,
+               "browser_searchSuggestionEngine%20searchSuggestionEngine.xml",
+               "Expected suggestion engine");
   let item = gURLBar.popup.richlistbox.getItemAtIndex(idx);
   let loadPromise = promiseTabLoaded(gBrowser.selectedTab);
   item.click();
   yield loadPromise;
   let uri = Services.search.currentEngine.getSubmission(suggestion).uri;
   Assert.ok(uri.equals(gBrowser.currentURI),
             "The search results page should have loaded");
   gBrowser.removeTab(gBrowser.selectedTab);
@@ -42,24 +45,24 @@ function getFirstSuggestion() {
   let present = false;
   for (let i = 0; i < matchCount; i++) {
     let url = controller.getValueAt(i);
     let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
     if (mozActionMatch) {
       let [, type, paramStr] = mozActionMatch;
       let params = JSON.parse(paramStr);
       if (type == "searchengine" && "searchSuggestion" in params) {
-        return [i, params.searchSuggestion];
+        return [i, params.searchSuggestion, params.engineName];
       }
     }
   }
   return [-1, null];
 }
 
 function promiseFirstSuggestion() {
   return new Promise(resolve => {
-    let pair;
+    let tuple;
     waitForCondition(() => {
-      pair = getFirstSuggestion();
-      return pair[0] >= 0;
-    }, () => resolve(pair));
+      tuple = getFirstSuggestion();
+      return tuple[0] >= 0;
+    }, () => resolve(tuple));
   });
 }
--- a/browser/base/content/test/urlbar/browser_urlbarStop.js
+++ b/browser/base/content/test/urlbar/browser_urlbarStop.js
@@ -15,17 +15,16 @@ add_task(function* () {
   gBrowser.selectedTab = gBrowser.addTab("about:blank");
   is(gURLBar.textValue, "", "location bar is empty");
 
   yield typeAndSubmitAndStop(badURL);
   is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects stopped page in an empty tab");
   gBrowser.removeCurrentTab();
 });
 
-function typeAndSubmitAndStop(url) {
-  gBrowser.userTypedValue = url;
-  URLBarSetURI();
+function* typeAndSubmitAndStop(url) {
+  yield promiseAutocompleteResultPopup(url, window, true);
   is(gURLBar.textValue, gURLBar.trimValue(url), "location bar reflects loading page");
 
   let promise = waitForDocLoadAndStopIt(url, gBrowser.selectedBrowser, false);
   gURLBar.handleCommand();
-  return promise;
+  yield promise;
 }
--- a/browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js
+++ b/browser/base/content/test/urlbar/browser_urlbar_autoFill_backspaced.js
@@ -2,23 +2,23 @@
  * confirm the remaining value.
  */
 
 function* test_autocomplete(data) {
   let {desc, typed, autofilled, modified, keys, action, onAutoFill} = data;
   info(desc);
 
   yield promiseAutocompleteResultPopup(typed);
-  is(gURLBar.value, autofilled, "autofilled value is as expected");
+  is(gURLBar.textValue, autofilled, "autofilled value is as expected");
   if (onAutoFill)
     onAutoFill()
 
   keys.forEach(key => EventUtils.synthesizeKey(key, {}));
 
-  is(gURLBar.value, modified, "backspaced value is as expected");
+  is(gURLBar.textValue, modified, "backspaced value is as expected");
 
   yield promiseSearchComplete();
 
   ok(gURLBar.popup.richlistbox.children.length > 0, "Should get at least 1 result");
   let result = gURLBar.popup.richlistbox.children[0];
   let type = result.getAttribute("type");
   let types = type.split(/\s+/);
   ok(types.indexOf(action) >= 0, `The type attribute "${type}" includes the expected action "${action}"`);
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -330,195 +330,214 @@ file, You can obtain one at http://mozil
           }
 
           // tell widget to revert to last typed text only if the user
           // was scrolling when they hit escape
           return !isScrolling;
         ]]></body>
       </method>
 
-      <method name="handleCommand">
-        <parameter name="aTriggeringEvent"/>
-        <body><![CDATA[
-          if (aTriggeringEvent instanceof MouseEvent && aTriggeringEvent.button == 2)
-            return; // Do nothing for right clicks
-
-          var url = this.value;
-          var mayInheritPrincipal = false;
-          var postData = null;
-
-          let action = this._parseActionUrl(this._value);
-          let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+      <!--
+        This is ultimately called by the autocomplete controller as the result
+        of handleEnter when the Return key is pressed in the textbox.  Since
+        onPopupClick also calls handleEnter, this is also called as a result in
+        that case.
 
-          let matchLastLocationChange = true;
-          if (action) {
-            if (action.type == "switchtab") {
-              url = action.params.url;
-              if (this.hasAttribute("actiontype")) {
-                this.handleRevert();
-                let prevTab = gBrowser.selectedTab;
-                if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) {
-                  gBrowser.removeTab(prevTab);
-                }
-                return;
-              }
-            } else if (action.type == "remotetab") {
-              url = action.params.url;
-            } else if (action.type == "keyword") {
-              url = action.params.url;
-            } else if (action.type == "searchengine") {
-              [url, postData] = this._parseAndRecordSearchEngineAction(action);
-            } else if (action.type == "visiturl") {
-              url = action.params.url;
-            }
-            continueOperation.call(this);
+        @param event
+               The event that triggered the command.
+        @param openUILinkWhere
+               Optional.  The "where" to pass to openUILinkIn.  This method
+               computes the appropriate "where" given the event, but you can
+               use this to override it.
+        @param openUILinkParams
+               Optional.  The parameters to pass to openUILinkIn.  As with
+               "where", this method computes the appropriate parameters, but
+               any parameters you supply here will override those.
+      -->
+      <method name="handleCommand">
+        <parameter name="event"/>
+        <parameter name="selectedPopupIndex"/>
+        <parameter name="openUILinkWhere"/>
+        <parameter name="openUILinkParams"/>
+        <body><![CDATA[
+          let isMouseEvent = event instanceof MouseEvent;
+          if (isMouseEvent && event.button == 2) {
+            // Do nothing for right clicks.
+            return;
           }
-          else {
-            this._canonizeURL(aTriggeringEvent, response => {
-              [url, postData, mayInheritPrincipal] = response;
-              if (url) {
-                matchLastLocationChange = (lastLocationChange ==
-                                           gBrowser.selectedBrowser.lastLocationChange);
-                continueOperation.call(this);
-              }
-            });
+
+          let where = "current";
+          // If the current tab is empty, ignore Alt+Enter (just reuse this tab)
+          let altEnter = !isMouseEvent &&
+                         event &&
+                         event.altKey &&
+                         !isTabEmpty(gBrowser.selectedTab);
+          if (isMouseEvent || altEnter) {
+            // Use the standard UI link behaviors for clicks or Alt+Enter
+            where = isMouseEvent ? whereToOpenLink(event, false, false) : "tab";
           }
 
-          function continueOperation()
-          {
-            this.value = url;
-            gBrowser.userTypedValue = url;
-            if (gInitialPages.includes(url)) {
-              gBrowser.selectedBrowser.initialPageLoadedFromURLBar = url;
-            }
-            try {
-              addToUrlbarHistory(url);
-            } catch (ex) {
-              // Things may go wrong when adding url to session history,
-              // but don't let that interfere with the loading of the url.
-              Cu.reportError(ex);
-            }
-
-            let loadCurrent = () => {
-              this._loadURL(aTriggeringEvent, url, "current", {
-                disallowInheritPrincipal: !mayInheritPrincipal,
-                postData: postData,
-              });
-            };
-
-            // Focus the content area before triggering loads, since if the load
-            // occurs in a new tab, we want focus to be restored to the content
-            // area when the current tab is re-selected.
-            gBrowser.selectedBrowser.focus();
-
-            let isMouseEvent = aTriggeringEvent instanceof MouseEvent;
-
-            // If the current tab is empty, ignore Alt+Enter (just reuse this tab)
-            let altEnter = !isMouseEvent && aTriggeringEvent &&
-              aTriggeringEvent.altKey && !isTabEmpty(gBrowser.selectedTab);
-
-            if (isMouseEvent || altEnter) {
-              // Use the standard UI link behaviors for clicks or Alt+Enter
-              let where = "tab";
-              if (isMouseEvent)
-                where = whereToOpenLink(aTriggeringEvent, false, false);
-
-              if (where == "current") {
-                if (matchLastLocationChange) {
-                  loadCurrent();
-                }
-              } else {
-                this._loadURL(aTriggeringEvent, url, where, {
-                  postData: postData,
-                });
-              }
-            } else {
-              if (matchLastLocationChange) {
-                loadCurrent();
-              }
+          // For the URL to load, prefer the url attribute of the selected
+          // result over the input's value.  The reason is that if there is a
+          // selected result and it's a searchengine action, then its url will
+          // have been updated for the currently selected one-off search engine.
+          // Note that when this method is called, the popup may be closed,
+          // which is why this method has a selectedPopupIndex param.
+          let url;
+          if (typeof(selectedPopupIndex) != "number" ||
+              selectedPopupIndex < 0) {
+            selectedPopupIndex = this.popup.selectedIndex;
+          }
+          if (selectedPopupIndex >= 0) {
+            let item = this.popup.richlistbox.children[selectedPopupIndex];
+            if (item) {
+              url = item.getAttribute("url");
             }
           }
+          url = url || this.value;
+
+          let mayInheritPrincipal = false;
+          let postData = null;
+          let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+          let matchLastLocationChange = true;
+
+let action = this._parseActionUrl(url);
+          if (action) {
+            switch (action.type) {
+              case "visiturl":
+              case "keyword":
+              case "remotetab":
+                url = action.params.url;
+                break;
+              case "switchtab":
+                url = action.params.url;
+                if (this.hasAttribute("actiontype")) {
+                  this.handleRevert();
+                  let prevTab = gBrowser.selectedTab;
+                  if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) {
+                    gBrowser.removeTab(prevTab);
+                  }
+                  return;
+                }
+                break;
+              case "searchengine":
+                [url, postData] =
+                  this._parseAndRecordSearchEngineAction(action, event, where,
+                                                         openUILinkParams);
+                break;
+            }
+            this._loadURL(url, postData, where, openUILinkParams,
+                          matchLastLocationChange, mayInheritPrincipal);
+            return;
+          }
+
+          this._canonizeURL(event, response => {
+            [url, postData, mayInheritPrincipal] = response;
+            if (url) {
+              matchLastLocationChange =
+                lastLocationChange ==
+                gBrowser.selectedBrowser.lastLocationChange;
+              this._loadURL(url, postData, where, openUILinkParams,
+                            matchLastLocationChange, mayInheritPrincipal);
+            }
+          });
         ]]></body>
       </method>
 
       <method name="_loadURL">
-        <parameter name="triggeringEvent"/>
         <parameter name="url"/>
-        <parameter name="where"/>
-        <parameter name="overrideParams"/>
+        <parameter name="postData"/>
+        <parameter name="openUILinkWhere"/>
+        <parameter name="openUILinkParams"/>
+        <parameter name="matchLastLocationChange"/>
+        <parameter name="mayInheritPrincipal"/>
         <body><![CDATA[
-          let current = where == "current";
+          this.value = url;
+          gBrowser.userTypedValue = url;
+          if (gInitialPages.includes(url)) {
+            gBrowser.selectedBrowser.initialPageLoadedFromURLBar = url;
+          }
+          try {
+            addToUrlbarHistory(url);
+          } catch (ex) {
+            // Things may go wrong when adding url to session history,
+            // but don't let that interfere with the loading of the url.
+            Cu.reportError(ex);
+          }
+
+          let current = openUILinkWhere == "current";
 
           let params;
           if (current) {
             params = {
+              postData: postData,
               allowThirdPartyFixup: true,
               indicateErrorPageLoad: true,
               disallowInheritPrincipal: true,
               allowPinnedTabHostChange: true,
+              disallowInheritPrincipal: !mayInheritPrincipal,
               allowPopups: url.startsWith("javascript:"),
             };
           } else {
             params = {
+              postData: postData,
               allowThirdPartyFixup: true,
               initiatingDoc: document,
             };
           }
 
-          if (overrideParams) {
-            for (let key in overrideParams) {
-              params[key] = overrideParams[key];
+          if (openUILinkParams) {
+            for (let key in openUILinkParams) {
+              params[key] = openUILinkParams[key];
             }
           }
 
-          let action = this._parseActionUrl(this.value);
+          // Focus the content area before triggering loads, since if the load
+          // occurs in a new tab, we want focus to be restored to the content
+          // area when the current tab is re-selected.
+          gBrowser.selectedBrowser.focus();
+
+          if (current && !matchLastLocationChange) {
+            return;
+          }
 
           if (!current) {
             this.handleRevert();
           }
 
           try {
-            openUILinkIn(url, where, params);
+            openUILinkIn(url, openUILinkWhere, params);
           } catch (ex) {
             // This load can throw an exception in certain cases, which means
             // we'll want to replace the URL with the loaded URL:
             if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
               this.handleRevert();
             }
           }
 
           if (current) {
             // Ensure the start of the URL is visible for UX reasons:
             this.selectionStart = this.selectionEnd = 0;
           }
-
-          // Record one-off search telemetry if appropriate.
-          if (action && action.type == "searchengine") {
-            this.popup.oneOffSearchButtons.recordTelemetry(triggeringEvent,
-                                                           where, params);
-          }
         ]]></body>
       </method>
 
       <method name="_parseAndRecordSearchEngineAction">
         <parameter name="action"/>
+        <parameter name="event"/>
+        <parameter name="openUILinkWhere"/>
+        <parameter name="openUILinkParams"/>
         <body><![CDATA[
           let engine =
             Services.search.getEngineByName(action.params.engineName);
+          BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
+          this.popup.oneOffSearchButtons.recordTelemetry(event, openUILinkWhere,
+                                                         openUILinkParams);
           let query = action.params.searchSuggestion ||
                       action.params.searchQuery;
-          return this._recordSearchEngineAction(engine, query);
-        ]]></body>
-      </method>
-
-      <method name="_recordSearchEngineAction">
-        <parameter name="engine"/>
-        <parameter name="query"/>
-        <body><![CDATA[
-          BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
           let submission = engine.getSubmission(query, null, "keyword");
           return [submission.uri.spec, submission.postData];
         ]]></body>
       </method>
 
       <method name="_canonizeURL">
         <parameter name="aTriggeringEvent"/>
         <parameter name="aCallback"/>
@@ -952,33 +971,34 @@ file, You can obtain one at http://mozil
             this.gotResultForCurrentQuery = false;
             this.mController.handleText();
           }
           this.resetActionType();
         ]]></body>
       </method>
 
       <method name="handleEnter">
+        <parameter name="event"/>
         <body><![CDATA[
           // We need to ensure we're using a selected autocomplete result.
           // A result should automatically be selected by default,
           // however autocomplete is async and therefore we may not
           // have a result set relating to the current input yet. If that
           // happens, we need to mark that when the first result does get added,
           // it needs to be handled as if enter was pressed with that first
           // result selected.
           // If anything other than the default (first) result is selected, then
           // it must have been manually selected by the human. We let this
           // explicit choice be used, even if it may be related to a previous
           // input.
           // However, if the default result is automatically selected, we
           // ensure that it corresponds to the current input.
 
           if (this.popup.selectedIndex != 0 || this.gotResultForCurrentQuery) {
-            return this.mController.handleEnter(false);
+            return this.mController.handleEnter(false, event);
           }
 
           this.handleEnterWhenGotResult = true;
 
           return true;
         ]]></body>
       </method>
 
@@ -1148,17 +1168,17 @@ file, You can obtain one at http://mozil
               index: controller.selection.currentIndex,
               kind: "mouse"
             };
           }
 
           // Check for unmodified left-click, and use default behavior
           if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
               !aEvent.altKey && !aEvent.metaKey) {
-            controller.handleEnter(true);
+            controller.handleEnter(true, aEvent);
             return;
           }
 
           // Check for middle-click or modified clicks on the search bar
           if (popupForSearchBar) {
             // Handle search bar popup clicks
             var search = controller.getValueAt(this.selectedIndex);
 
@@ -1539,106 +1559,32 @@ file, You can obtain one at http://mozil
               resolve();
             };
             this.addEventListener("transitionend", onTransitionEnd, true);
           });
           ]]>
         </body>
       </method>
 
-      <method name="onPopupClick">
-        <parameter name="aEvent"/>
-        <body>
-          <![CDATA[
-          // Ignore right-clicks
-          if (aEvent.button == 2)
-            return;
-
-          var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
-
-          // Check for unmodified left-click, and use default behavior
-          if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
-              !aEvent.altKey && !aEvent.metaKey) {
-            controller.handleEnter(true);
-            return;
-          }
-
-          // Check for middle-click or modified clicks on the URL bar
-          if (gURLBar && this.mInput == gURLBar) {
-            var url = controller.getValueAt(this.selectedIndex);
-            var options = {};
-
-            // close the autocomplete popup and revert the entered address
-            this.closePopup();
-            controller.handleEscape();
-
-            // Check if this is meant to be an action
-            let action = this.mInput._parseActionUrl(url);
-            if (action) {
-              // TODO (bug 1054816): Centralise the implementation of actions
-              // into a JS module.
-              switch (action.type) {
-                case "switchtab": // Fall through.
-                case "keyword": // Fall through.
-                case "visiturl": {
-                  url = action.params.url;
-                  break;
-                }
-                case "searchengine": {
-                  [url, options.postData] =
-                    this.input._parseAndRecordSearchEngineAction(action);
-                  break;
-                }
-                default: {
-                  return;
-                }
-              }
-            }
-
-            // respect the usual clicking subtleties
-            openUILink(url, aEvent, options);
-          }
-        ]]>
-        </body>
-      </method>
-
       <method name="_visuallySelectedOneOffChanged">
         <body><![CDATA[
-          let selectedNewURL;
-          let oneOff = this.oneOffSearchButtons.visuallySelectedButton;
-          let engine = (oneOff && oneOff.engine) ||
-                       Services.search.currentEngine;
-
-          // Update the moz-actions of all searchengine results to use the
-          // newly selected engine.
+          // Update all searchengine result items to use the newly selected
+          // engine.
           for (let item of this.richlistbox.childNodes) {
             if (item.collapsed) {
               break;
             }
             let url = item.getAttribute("url");
             if (url) {
               let action = item._parseActionUrl(url);
               if (action && action.type == "searchengine") {
-                action.params.engineName = engine.name;
-                let newURL =
-                  PlacesUtils.mozActionURI(action.type, action.params);
-                item.setAttribute("url", newURL);
                 item._adjustAcItem();
-                if (item == this.richlistbox.selectedItem) {
-                  selectedNewURL = newURL;
-                }
               }
             }
           }
-
-          // The urlbar uses its _value property (via value) to determine where
-          // to go when you press Return, so update it too.
-          if (selectedNewURL) {
-            this.input._value = selectedNewURL;
-          }
         ]]></body>
       </method>
 
       <!-- This handles keypress changes to the selection among the one-off
            search buttons and between the one-offs and the listbox.  It returns
            true if the keypress was consumed and false if not. -->
       <method name="handleKeyPress">
         <parameter name="aEvent"/>
@@ -1653,26 +1599,38 @@ file, You can obtain one at http://mozil
       <!-- This is called when a one-off is clicked and when "search in new tab"
            is selected from a one-off context menu. -->
       <method name="handleOneOffSearch">
         <parameter name="event"/>
         <parameter name="engine"/>
         <parameter name="where"/>
         <parameter name="params"/>
         <body><![CDATA[
-          let query = this.input.textValue;
-          let [url, postData] =
-            this.input._recordSearchEngineAction(engine, query);
-          params = params || {};
-          params.postData = postData;
-          gBrowser.selectedBrowser.focus();
-          this.input._loadURL(event, url, where, params);
+          this.input.handleCommand(event, -1, where, params);
         ]]></body>
       </method>
 
+      <!-- Result listitems call this to determine which search engine they
+           should show in their labels and include in their url attributes. -->
+      <property name="overrideSearchEngineName" readonly="true">
+        <getter><![CDATA[
+          // When building the popup, autocomplete reuses an item at index i if
+          // that item's url attribute matches the controller's value at index
+          // i, but only if overrideSearchEngineName matches the engine in the
+          // url attribute.  To absolutely avoid reusing items that shouldn't be
+          // reused, always return a non-null name here by falling back to the
+          // current engine.
+          let engine =
+            (this.oneOffSearchButtons.visuallySelectedButton &&
+             this.oneOffSearchButtons.visuallySelectedButton.engine) ||
+             Services.search.currentEngine;
+          return engine ? engine.name : null;
+        ]]></getter>
+      </property>
+
       <method name="createResultLabel">
         <parameter name="item"/>
         <parameter name="proposedLabel"/>
         <body>
           <![CDATA[
             let parts = [proposedLabel];
 
             let action = this.mInput._parseActionUrl(item.getAttribute("url"));
@@ -1723,17 +1681,17 @@ file, You can obtain one at http://mozil
               this.selectedIndex = 0;
               this.richlistbox.suppressMenuItemEvent = false;
               this._ignoreNextSelect = false;
             }
 
             this.input.gotResultForCurrentQuery = true;
             if (this.input.handleEnterWhenGotResult) {
               this.input.handleEnterWhenGotResult = false;
-              this.input.mController.handleEnter(false);
+              this.input.mController.handleEnter(false, null);
             }
           ]]>
         </body>
       </method>
 
     </implementation>
     <handlers>
 
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -1159,16 +1159,22 @@
             this._textbox = val;
           }
         ]]></setter>
       </property>
       <field name="_textbox"><![CDATA[
         null
       ]]></field>
 
+      <!-- Set this to a string that identifies your one-offs consumer.  It'll
+           be appended to telemetry recorded with recordTelemetry(). -->
+      <field name="telemetryID"><![CDATA[
+        ""
+      ]]></field>
+
       <!-- The query string currently shown in the one-offs.  If the textbox
            property is non-null, then this is automatically updated on
            input. -->
       <property name="query">
         <getter><![CDATA[
           return this._query;
         ]]></getter>
         <setter><![CDATA[
@@ -1234,22 +1240,16 @@
       <!-- The number of one-offs, including the add-engine button (if shown)
            and the search-settings button. -->
       <property name="numButtons" readonly="true">
         <getter><![CDATA[
           return this.getSelectableButtons(true).length;
         ]]></getter>
       </property>
 
-      <!-- Set this to a string that identifies your one-offs consumer.  It'll
-           be appended to telemetry recorded with recordTelemetry(). -->
-      <field name="telemetryID"><![CDATA[
-        ""
-      ]]></field>
-
       <property name="bundle" readonly="true">
         <getter><![CDATA[
           if (!this._bundle) {
             const kBundleURI = "chrome://browser/locale/search.properties";
             this._bundle = Services.strings.createBundle(kBundleURI);
           }
           return this._bundle;
         ]]></getter>
@@ -1818,16 +1818,20 @@
                The "params" passed to openUILink.
         @return True if telemetry was recorded and false if not.
       -->
       <method name="recordTelemetry">
         <parameter name="aEvent"/>
         <parameter name="aOpenUILinkWhere"/>
         <parameter name="aOpenUILinkParams"/>
         <body><![CDATA[
+          if (!aEvent) {
+            return;
+          }
+
           let source = null;
           let type = "unknown";
           let engine = null;
           let target = aEvent.originalTarget;
 
           if (aEvent instanceof KeyboardEvent) {
             type = "key";
             if (this.selectedButton) {
@@ -1925,16 +1929,22 @@
         // For some reason, if the context menu had been opened prior to the
         // click, the suggestions popup won't be closed after loading the search
         // in the current tab - so we hide it manually. Some focusing magic
         // that happens when a search is loaded ensures that the popup is opened
         // again if it needs to be, so we don't need to worry about which cases
         // require manual hiding.
         this.popup.hidePopup();
 
+        // Make sure the clicked button is selected.  In practice this should
+        // always be the case since you have to mouse over the button to click
+        // it, and the mouseover handler selects the button.  So mostly this is
+        // for tests.
+        this.selectedButton = button;
+
         this.handleSearchCommand(event, engine);
       ]]></handler>
 
       <handler event="command"><![CDATA[
         let target = event.originalTarget;
         if (target.classList.contains("addengine-item")) {
           // On success, hide the panel and tell event listeners to reshow it to
           // show the new engine.
--- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
@@ -284,40 +284,42 @@ nsAutoCompleteController::HandleText()
   }
 
   StartSearches();
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsAutoCompleteController::HandleEnter(bool aIsPopupSelection, bool *_retval)
+nsAutoCompleteController::HandleEnter(bool aIsPopupSelection,
+                                      nsIDOMEvent *aEvent,
+                                      bool *_retval)
 {
   *_retval = false;
   if (!mInput)
     return NS_OK;
 
   nsCOMPtr<nsIAutoCompleteInput> input(mInput);
+  int32_t selectedIndex = -1;
 
   // allow the event through unless there is something selected in the popup
   input->GetPopupOpen(_retval);
   if (*_retval) {
     nsCOMPtr<nsIAutoCompletePopup> popup;
     input->GetPopup(getter_AddRefs(popup));
 
     if (popup) {
-      int32_t selectedIndex;
       popup->GetSelectedIndex(&selectedIndex);
       *_retval = selectedIndex >= 0;
     }
   }
 
   // Stop the search, and handle the enter.
   StopSearch();
-  EnterMatch(aIsPopupSelection);
+  EnterMatch(aIsPopupSelection, aEvent, selectedIndex);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAutoCompleteController::HandleEscape(bool *_retval)
 {
   *_retval = false;
@@ -383,17 +385,17 @@ nsAutoCompleteController::HandleEndCompo
   mCompositionState = eCompositionState_Committing;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAutoCompleteController::HandleTab()
 {
   bool cancel;
-  return HandleEnter(false, &cancel);
+  return HandleEnter(false, nullptr, &cancel);
 }
 
 NS_IMETHODIMP
 nsAutoCompleteController::HandleKeyNavigation(uint32_t aKey, bool *_retval)
 {
   // By default, don't cancel the event
   *_retval = false;
 
@@ -1351,17 +1353,19 @@ nsAutoCompleteController::ClearSearchTim
   if (mTimer) {
     mTimer->Cancel();
     mTimer = nullptr;
   }
   return NS_OK;
 }
 
 nsresult
-nsAutoCompleteController::EnterMatch(bool aIsPopupSelection)
+nsAutoCompleteController::EnterMatch(bool aIsPopupSelection,
+                                     nsIDOMEvent *aEvent,
+                                     int32_t aSelectedPopupIndex)
 {
   nsCOMPtr<nsIAutoCompleteInput> input(mInput);
   nsCOMPtr<nsIAutoCompletePopup> popup;
   input->GetPopup(getter_AddRefs(popup));
   NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
 
   bool forceComplete;
   input->GetForceComplete(&forceComplete);
@@ -1487,17 +1491,17 @@ nsAutoCompleteController::EnterMatch(boo
     input->SelectTextRange(value.Length(), value.Length());
     mSearchString = value;
   }
 
   obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);
   ClosePopup();
 
   bool cancel;
-  input->OnTextEntered(&cancel);
+  input->OnTextEntered(aEvent, aSelectedPopupIndex, &cancel);
 
   return NS_OK;
 }
 
 nsresult
 nsAutoCompleteController::RevertTextValue()
 {
   // StopSearch() can call PostSearchCleanup() which might result
--- a/toolkit/components/autocomplete/nsAutoCompleteController.h
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.h
@@ -50,17 +50,19 @@ protected:
   nsresult ClearSearchTimer();
   void MaybeCompletePlaceholder();
 
   void HandleSearchResult(nsIAutoCompleteSearch *aSearch,
                           nsIAutoCompleteResult *aResult);
   nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult);
   nsresult PostSearchCleanup();
 
-  nsresult EnterMatch(bool aIsPopupSelection);
+  nsresult EnterMatch(bool aIsPopupSelection,
+                      nsIDOMEvent *aEvent,
+                      int32_t aSelectedPopupIndex);
   nsresult RevertTextValue();
 
   nsresult CompleteDefaultIndex(int32_t aResultIndex);
   nsresult CompleteValue(nsString &aValue);
 
   nsresult GetResultAt(int32_t aIndex, nsIAutoCompleteResult** aResult,
                        int32_t* aRowIndex);
   nsresult GetResultValueAt(int32_t aIndex, bool aGetFinalValue,
--- a/toolkit/components/autocomplete/nsIAutoCompleteController.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteController.idl
@@ -1,17 +1,18 @@
 /* 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 nsIAutoCompleteInput;
+interface nsIDOMEvent;
 
-[scriptable, uuid(ff9f8465-204a-47a6-b3c9-0628b3856684)]
+[scriptable, uuid(c6dcd364-99a8-48a9-9611-77e6a18ac9f3)]
 interface nsIAutoCompleteController : nsISupports
 {
   /*
    * Possible values for the searchStatus attribute
    */
   const unsigned short STATUS_NONE = 1;
   const unsigned short STATUS_SEARCHING = 2;
   const unsigned short STATUS_COMPLETE_NO_MATCH = 3;
@@ -58,19 +59,26 @@ interface nsIAutoCompleteController : ns
   void handleText();
 
   /*
    * Notify the controller that the user wishes to enter the current text. If
    * aIsPopupSelection is true, then a selection was made from the popup, so
    * fill this value into the input field before continuing. If false, just
    * use the current value of the input field.
    *
+   * @param aIsPopupSelection
+   *        Pass true if the selection was made from the popup.
+   * @param aEvent
+   *        Optional.  The event that triggered the enter, like a key event if
+   *        the user pressed the Return key or a click event if the user clicked
+   *        a popup item.
    * @return True if the controller wishes to prevent event propagation and default event
    */
-  boolean handleEnter(in boolean aIsPopupSelection);
+  boolean handleEnter(in boolean aIsPopupSelection,
+                      in nsIDOMEvent aEvent);
 
   /*
    * Notify the controller that the user wishes to revert autocomplete
    *
    * @return True if the controller wishes to prevent event propagation and default event
    */
   boolean handleEscape();
 
--- a/toolkit/components/autocomplete/nsIAutoCompleteInput.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteInput.idl
@@ -2,17 +2,17 @@
  * 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"
 #include "nsIAutoCompleteController.idl"
 
 interface nsIAutoCompletePopup;
 
-[scriptable, uuid(B068E70F-F82C-4C12-AD87-82E271C5C180)]
+[scriptable, uuid(d2f4b70b-e679-4295-9f91-a1ca57da7fca)]
 interface nsIAutoCompleteInput : nsISupports
 {  
   /*
    * The result view that will be used to display results
    */
   readonly attribute nsIAutoCompletePopup popup;
   
   /*
@@ -119,19 +119,25 @@ interface nsIAutoCompleteInput : nsISupp
   /*
    * Notification that the search concluded successfully
    */
   void onSearchComplete();
 
   /*
    * Notification that the user selected and entered a result item
    *
+   * @param aEvent
+   *        The event that triggered the enter.  Will be null if it's unknown.
+   * @param aSelectedPopupIndex
+   *        The index in the popup that was selected when the enter was made.
+   *        -1 if unknown.
    * @return True if the user wishes to prevent the enter
    */
-  boolean onTextEntered();
+  boolean onTextEntered(in nsIDOMEvent aEvent,
+                        in long aSelectedPopupIndex);
 
   /*
    * Notification that the user cancelled the autocomplete session
    *
    * @return True if the user wishes to prevent the revert
    */
   boolean onTextReverted();
 
--- a/toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.js
+++ b/toolkit/components/autocomplete/tests/unit/test_completeDefaultIndex_casing.js
@@ -25,17 +25,17 @@ add_test(function test_keyNavigation() {
     aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_RIGHT);
     do_check_eq(aController.input.textValue, "mozilla");
   });
 });
 
 add_test(function test_handleEnter() {
   doSearch("MOZ", "mozilla", function(aController) {
     do_check_eq(aController.input.textValue, "MOZilla");
-    aController.handleEnter(false);
+    aController.handleEnter(false, null);
     do_check_eq(aController.input.textValue, "mozilla");
   });
 });
 
 function doSearch(aSearchString, aResultValue, aOnCompleteCallback) {
   let search = new AutoCompleteSearchBase("search",
                                           new AutoCompleteResult([ "mozilla", "toolkit" ], 0));
   registerAutoCompleteSearch(search);
--- a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue.js
@@ -11,17 +11,17 @@ function AutoCompleteInput(aSearches) {
 AutoCompleteInput.prototype = Object.create(AutoCompleteInputBase.prototype);
 
 add_test(function test_handleEnter_mouse() {
   doSearch("moz", "mozilla.com", "http://www.mozilla.com", function(aController) {
     do_check_eq(aController.input.textValue, "moz");
     do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
     // Keyboard interaction is tested by test_finalCompleteValueSelectedIndex.js
     // so here just test popup selection.
-    aController.handleEnter(true);
+    aController.handleEnter(true, null);
     do_check_eq(aController.input.textValue, "http://www.mozilla.com");
   });
 });
 
 function doSearch(aSearchString, aResultValue, aFinalCompleteValue, aOnCompleteCallback) {
   let search = new AutoCompleteSearchBase(
     "search",
     new AutoCompleteResult([ aResultValue ], [ aFinalCompleteValue ])
--- a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValueSelectedIndex.js
@@ -36,17 +36,17 @@ add_test(function test_handleEnter() {
 
     Assert.equal(aController.input.popup.selectedIndex, 0);
     aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_DOWN);
     Assert.equal(aController.input.popup.selectedIndex, 1);
     // Simulate mouse interaction changing selectedIndex
     // ie NOT keyboard interaction:
     aController.input.popup.selectedIndex = 0;
 
-    aController.handleEnter(false);
+    aController.handleEnter(false, null);
     // Verify that the keyboard-selected thing got inserted,
     // and not the mouse selection:
     Assert.equal(aController.input.textValue, "http://www.mozilla.org");
   });
 
   // Then the case where we do not:
   doSearch("moz", results, function(aController) {
     Assert.equal(aController.input.textValue, "moz");
@@ -56,17 +56,17 @@ add_test(function test_handleEnter() {
     Assert.equal(aController.input.popup.selectedIndex, 0);
     aController.input.popupOpen = true;
     // Simulate mouse interaction changing selectedIndex
     // ie NOT keyboard interaction:
     aController.input.popup.selectedIndex = 1;
     Assert.equal(selectByWasCalled, false);
     Assert.equal(aController.input.popup.selectedIndex, 1);
 
-    aController.handleEnter(false);
+    aController.handleEnter(false, null);
     // Verify that the input stayed the same, because no selection was made
     // with the keyboard:
     Assert.equal(aController.input.textValue, "moz");
   });
 });
 
 function doSearch(aSearchString, aResults, aOnCompleteCallback) {
   selectByWasCalled = false;
--- a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_defaultIndex.js
@@ -20,17 +20,17 @@ add_test(function test_handleEnter() {
   ];
   doSearch("moz", results, controller => {
     let input = controller.input;
     Assert.equal(input.textValue, "mozilla.com");
     Assert.equal(controller.getFinalCompleteValueAt(0), results[0][1]);
     Assert.equal(controller.getFinalCompleteValueAt(1), results[1][1]);
     Assert.equal(input.popup.selectedIndex, 0);
 
-    controller.handleEnter(false);
+    controller.handleEnter(false, null);
     // Verify that the keyboard-selected thing got inserted,
     // and not the mouse selection:
     Assert.equal(controller.input.textValue, "https://www.mozilla.com");
   });
 });
 
 function doSearch(aSearchString, aResults, aOnCompleteCallback) {
   let search = new AutoCompleteSearchBase(
--- a/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalCompleteValue_forceComplete.js
@@ -19,56 +19,56 @@ function run_test() {
   run_next_test();
 }
 
 add_test(function test_handleEnterWithDirectMatchCompleteSelectedIndex() {
   doSearch("moz", "mozilla.com", "http://www.mozilla.com",
     { forceComplete: true, completeSelectedIndex: true }, function(aController) {
     do_check_eq(aController.input.textValue, "moz");
     do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
-    aController.handleEnter(false);
+    aController.handleEnter(false, null);
     // After enter the final complete value should be shown in the input.
     do_check_eq(aController.input.textValue, "http://www.mozilla.com");
   });
 });
 
 add_test(function test_handleEnterWithDirectMatch() {
   doSearch("mozilla", "mozilla.com", "http://www.mozilla.com",
     { forceComplete: true, completeDefaultIndex: true }, function(aController) {
     // Should autocomplete the search string to a suggestion.
     do_check_eq(aController.input.textValue, "mozilla.com");
     do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
-    aController.handleEnter(false);
+    aController.handleEnter(false, null);
     // After enter the final complete value should be shown in the input.
     do_check_eq(aController.input.textValue, "http://www.mozilla.com");
   });
 });
 
 add_test(function test_handleEnterWithNoMatch() {
   doSearch("mozilla", "mozilla.com", "http://www.mozilla.com",
     { forceComplete: true, completeDefaultIndex: true }, function(aController) {
     // Should autocomplete the search string to a suggestion.
     do_check_eq(aController.input.textValue, "mozilla.com");
     do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
     // Now input something that does not match...
     aController.input.textValue = "mozillax";
     // ... and confirm. We don't want one of the values from the previous
     // results to be taken, since what's now in the input field doesn't match.
-    aController.handleEnter(false);
+    aController.handleEnter(false, null);
     do_check_eq(aController.input.textValue, "mozillax");
   });
 });
 
 add_test(function test_handleEnterWithIndirectMatch() {
   doSearch("com", "mozilla.com", "http://www.mozilla.com",
     { forceComplete: true, completeDefaultIndex: true }, function(aController) {
     // Should autocomplete the search string to a suggestion.
     do_check_eq(aController.input.textValue, "com >> mozilla.com");
     do_check_eq(aController.getFinalCompleteValueAt(0), "http://www.mozilla.com");
-    aController.handleEnter(false);
+    aController.handleEnter(false, null);
     // After enter the final complete value from the suggestion should be shown
     // in the input.
     do_check_eq(aController.input.textValue, "http://www.mozilla.com");
   });
 });
 
 function doSearch(aSearchString, aResultValue, aFinalCompleteValue,
                   aInputProps, aOnCompleteCallback) {
--- a/toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.js
+++ b/toolkit/components/autocomplete/tests/unit/test_finalDefaultCompleteValue.js
@@ -26,17 +26,17 @@ add_test(function test_keyNavigation() {
     aController.handleKeyNavigation(Ci.nsIDOMKeyEvent.DOM_VK_RIGHT);
     do_check_eq(aController.input.textValue, "mozilla.com");
   });
 });
 
 add_test(function test_handleEnter() {
   doSearch("moz", "mozilla.com", "http://www.mozilla.com", function(aController) {
     do_check_eq(aController.input.textValue, "mozilla.com");
-    aController.handleEnter(false);
+    aController.handleEnter(false, null);
     do_check_eq(aController.input.textValue, "http://www.mozilla.com");
   });
 });
 
 function doSearch(aSearchString, aResultValue, aFinalCompleteValue, aOnCompleteCallback) {
   let search = new AutoCompleteSearchBase(
     "search",
     new AutoCompleteResult([ aResultValue ], [ aFinalCompleteValue ])
--- a/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js
+++ b/toolkit/components/autocomplete/tests/unit/test_hiddenResult.js
@@ -57,17 +57,17 @@ function run_test() {
 
   input.onSearchComplete = function() {
     // Hidden results should still be able to do inline autocomplete
     do_check_eq(input.textValue, "mozillaTest1");
 
     // Now, let's fill the textbox with the first result of the popup.
     // The first search is marked as hidden, so we must always get the
     // second search.
-    controller.handleEnter(true);
+    controller.handleEnter(true, null);
     do_check_eq(input.textValue, "mozillaTest2");
 
     // Only one item in the popup.
     do_check_eq(controller.matchCount, 1);
 
     // Unregister searches
     unregisterAutoCompleteSearch(searchNormal);
     unregisterAutoCompleteSearch(searchTypeAhead);
--- a/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js
+++ b/toolkit/components/autocomplete/tests/unit/test_popupSelectionVsDefaultCompleteValue.js
@@ -25,17 +25,17 @@ AutoCompleteInput.prototype = Object.cre
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_handleEnter() {
   doSearch("moz", function(aController) {
     do_check_eq(aController.input.textValue, "mozilla.com");
-    aController.handleEnter(true);
+    aController.handleEnter(true, null);
     do_check_eq(aController.input.textValue, "mozilla.org");
   });
 });
 
 function doSearch(aSearchString, aOnCompleteCallback) {
   let typeAheadSearch = new AutoCompleteSearchBase(
     "typeAheadSearch",
     new AutoCompleteTypeAheadResult([ "mozilla.com" ], [ "http://www.mozilla.com" ])
--- a/toolkit/components/autocomplete/tests/unit/test_stopSearch.js
+++ b/toolkit/components/autocomplete/tests/unit/test_stopSearch.js
@@ -125,17 +125,17 @@ var gTests = [
     controller.handleText();
   },
   function(controller) {
     print("handleEscape");
     controller.handleEscape();
   },
   function(controller) {
     print("handleEnter");
-    controller.handleEnter(false);
+    controller.handleEnter(false, null);
   },
   function(controller) {
     print("handleTab");
     controller.handleTab();
   },
 
   function(controller) {
     print("handleKeyNavigation");
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -250,17 +250,17 @@ function* check_autocomplete(test) {
   if (test.autofilled) {
     // Check the autoFilled result.
     Assert.equal(input.textValue, test.autofilled,
                  "Autofilled value is correct");
 
     // Now force completion and check correct casing of the result.
     // This ensures the controller is able to do its magic case-preserving
     // stuff and correct replacement of the user's casing with result's one.
-    controller.handleEnter(false);
+    controller.handleEnter(false, null);
     Assert.equal(input.textValue, test.completed,
                  "Completed value is correct");
   }
 }
 
 var addBookmark = Task.async(function* (aBookmarkObj) {
   Assert.ok(!!aBookmarkObj.uri, "Bookmark object contains an uri");
   let parentId = aBookmarkObj.parentId ? aBookmarkObj.parentId
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -585,17 +585,17 @@ var AutoCompletePopup = {
     this._input = null;
     this._popupOpen = false;
 
     addMessageListener("FormAutoComplete:HandleEnter", message => {
       this.selectedIndex = message.data.selectedIndex;
 
       let controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
                   getService(Components.interfaces.nsIAutoCompleteController);
-      controller.handleEnter(message.data.isPopupSelection);
+      controller.handleEnter(message.data.isPopupSelection, null);
     });
 
     addEventListener("unload", function() {
       AutoCompletePopup.destroy();
     });
   },
 
   destroy: function() {
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -39,17 +39,16 @@
 
       <children includes="toolbarbutton"/>
     </content>
 
     <implementation implements="nsIAutoCompleteInput, nsIDOMXULMenuListElement">
       <field name="mController">null</field>
       <field name="mSearchNames">null</field>
       <field name="mIgnoreInput">false</field>
-      <field name="mEnterEvent">null</field>
 
       <field name="_searchBeginHandler">null</field>
       <field name="_searchCompleteHandler">null</field>
       <field name="_textEnteredHandler">null</field>
       <field name="_textRevertedHandler">null</field>
 
       <constructor><![CDATA[
         this.mController = Components.classes["@mozilla.org/autocomplete/controller;1"].
@@ -227,21 +226,23 @@
           }
 
           if (this._searchCompleteHandler)
             this._searchCompleteHandler();
         ]]></body>
       </method>
 
       <method name="onTextEntered">
+        <parameter name="event"/>
+        <parameter name="selectedPopupIndex"/>
         <body><![CDATA[
           let rv = false;
-          if (this._textEnteredHandler)
-            rv = this._textEnteredHandler(this.mEnterEvent);
-          this.mEnterEvent = null;
+          if (this._textEnteredHandler) {
+            rv = this._textEnteredHandler(event, selectedPopupIndex);
+          }
           return rv;
         ]]></body>
       </method>
 
       <method name="onTextReverted">
         <body><![CDATA[
           if (this._textRevertedHandler)
             return this._textRevertedHandler();
@@ -411,19 +412,23 @@
         ]]></body>
       </method>
 
       <!-- ::::::::::::: event dispatching ::::::::::::: -->
 
       <method name="initEventHandler">
         <parameter name="aEventType"/>
         <body><![CDATA[
-          let handlerString = this.getAttribute("on" + aEventType);
-          if (handlerString) {
-            return (new Function("eventType", "param", handlerString)).bind(this, aEventType);
+          let handlerCode = this.getAttribute("on" + aEventType);
+          if (handlerCode) {
+            return (...args) => {
+              let fn = (new Function("eventType", "args", handlerCode))
+                       .bind(this, aEventType, args);
+              return fn();
+            };
           }
           return null;
         ]]></body>
       </method>
 
       <!-- ::::::::::::: key handling ::::::::::::: -->
 
       <field name="_selectionDetails">null</field>
@@ -489,24 +494,23 @@
               cancel = this.mController.handleEscape();
               break;
             case KeyEvent.DOM_VK_RETURN:
               if (AppConstants.platform == "macosx") {
                 // Prevent the default action, since it will beep on Mac
                 if (aEvent.metaKey)
                   aEvent.preventDefault();
               }
-              this.mEnterEvent = aEvent;
               if (this.mController.selection) {
                 this._selectionDetails = {
                   index: this.mController.selection.currentIndex,
                   kind: "key"
                 };
               }
-              cancel = this.handleEnter();
+              cancel = this.handleEnter(aEvent);
               break;
             case KeyEvent.DOM_VK_DELETE:
               if (AppConstants.platform == "macosx" && !aEvent.shiftKey) {
                 break;
               }
               cancel = this.handleDelete();
               break;
             case KeyEvent.DOM_VK_BACK_SPACE:
@@ -531,18 +535,19 @@
             aEvent.preventDefault();
           }
 
           return true;
         ]]></body>
       </method>
 
       <method name="handleEnter">
+        <parameter name="event"/>
         <body><![CDATA[
-          return this.mController.handleEnter(false);
+          return this.mController.handleEnter(false, event || null);
         ]]></body>
       </method>
 
       <method name="handleDelete">
         <body><![CDATA[
           return this.mController.handleDelete();
         ]]></body>
       </method>
@@ -642,17 +647,17 @@
               for (let i = 0; i < this.mController.matchCount; i++) {
                 let matchVal = this.mController.getFinalCompleteValueAt(i);
                 if (matchVal.toLowerCase() == filledVal) {
                   this.popup.selectedIndex = i;
                   break;
                 }
               }
             }
-            this.mController.handleEnter(false);
+            this.mController.handleEnter(false, null);
           }
           if (!this.ignoreBlurWhileSearching)
             this.detachController();
         }
       ]]></handler>
     </handlers>
   </binding>
 
@@ -941,17 +946,17 @@ extends="chrome://global/content/binding
           return aIndex;
         ]]></body>
       </method>
 
       <method name="onPopupClick">
         <parameter name="aEvent"/>
         <body><![CDATA[
           var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
-          controller.handleEnter(true);
+          controller.handleEnter(true, aEvent);
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="popupshowing"><![CDATA[
         // If normalMaxRows wasn't already set by the input, then set it here
         // so that we restore the correct number when the popup is hidden.
@@ -1245,25 +1250,34 @@ extends="chrome://global/content/binding
               // due to new results, but only when: the item is the same, *OR*
               // we are about to replace the currently mouse-selected item, to
               // avoid surprising the user.
               let iface = Components.interfaces.nsIAutoCompletePopup;
               if (item.getAttribute("text") == trimmedSearchString &&
                   invalidateReason == iface.INVALIDATE_REASON_NEW_RESULT &&
                   (item.getAttribute("url") == url ||
                    this.richlistbox.mouseSelectedIndex === this._currentIndex)) {
-                item.collapsed = false;
-                // Call adjustSiteIconStart only after setting collapsed=false.
-                // The calculations it does may be wrong otherwise.
-                item.adjustSiteIconStart(this._siteIconStart);
-                // The popup may have changed size between now and the last time
-                // the item was shown, so always handle over/underflow.
-                item.handleOverUnderflow();
-                this._currentIndex++;
-                continue;
+                // Additionally, if the item is a searchengine action, then it
+                // should only be reused if the engine name is the same as the
+                // popup's override engine name, if any.
+                let action = item._parseActionUrl(url);
+                if (!action ||
+                    action.type != "searchengine" ||
+                    !this.overrideSearchEngineName ||
+                    action.params.engineName == this.overrideSearchEngineName) {
+                  item.collapsed = false;
+                  // Call adjustSiteIconStart only after setting collapsed=
+                  // false.  The calculations it does may be wrong otherwise.
+                  item.adjustSiteIconStart(this._siteIconStart);
+                  // The popup may have changed size between now and the last
+                  // time the item was shown, so always handle over/underflow.
+                  item.handleOverUnderflow();
+                  this._currentIndex++;
+                  continue;
+                }
               }
             }
             else {
               // need to create a new item
               item = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "richlistitem");
               item.setAttribute("dir", this.style.direction);
             }
 
@@ -1890,16 +1904,27 @@ extends="chrome://global/content/binding
               emphasiseUrl = false;
 
               // The order here is not localizable, we default to appending
               // "- Search with Engine" to the search string, to be able to
               // properly generate emphasis pairs. That said, no localization
               // changed the order while it was possible, so doesn't look like
               // there's a strong need for that.
               let {engineName, searchSuggestion, searchQuery} = action.params;
+
+              // Override the engine name if the popup defines an override.
+              let override = popup.overrideSearchEngineName;
+              if (override && override != engineName) {
+                engineName = override;
+                action.params.engineName = override;
+                let newURL =
+                  PlacesUtils.mozActionURI(action.type, action.params);
+                this.setAttribute("url", newURL);
+              }
+
               let engineStr =
                 this._stringBundle.formatStringFromName("searchWithEngine",
                                                         [engineName], 1);
               this._setUpDescription(this._actionText, engineStr, true);
 
               // Make the title by generating an array of pairs and its
               // corresponding interpolation string (e.g., "%1$S") to pass to
               // _generateEmphasisPairs.
--- a/xpfe/components/autocomplete/resources/content/autocomplete.xml
+++ b/xpfe/components/autocomplete/resources/content/autocomplete.xml
@@ -1575,17 +1575,17 @@
 
     <handlers>
       <handler event="mouseout" action="this.popup.selectedIndex = -1;"/>
 
       <handler event="mouseup"><![CDATA[
         var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
         if (rc != -1) {
           this.popup.selectedIndex = rc;
-          this.popup.view.handleEnter(true);
+          this.popup.view.handleEnter(true, null);
         }
       ]]></handler>
 
       <handler event="mousemove"><![CDATA[
         if (Date.now() - this.mLastMoveTime > 30) {
           var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
           if (rc != -1 && rc != this.popup.selectedIndex) 
             this.popup.selectedIndex = rc;