Bug 1398416 - Part 3: Implement form history results. r=mak
authorDrew Willcoxon <adw@mozilla.com>
Thu, 21 May 2020 19:31:27 +0000
changeset 531522 fd143c3d9027c00237fd2054f75e91fb925942b2
parent 531521 979cea3e1aa94b6234d7863df2f905d066329816
child 531523 b9d97c8a708eb0c8b567d352833884c3103b0afc
push id116701
push userdwillcoxon@mozilla.com
push dateThu, 21 May 2020 19:41:23 +0000
treeherderautoland@fd143c3d9027 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1398416
milestone78.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 1398416 - Part 3: Implement form history results. r=mak Differential Revision: https://phabricator.services.mozilla.com/D75685
browser/app/profile/firefox.js
browser/components/extensions/test/xpcshell/test_ext_urlbar.js
browser/components/urlbar/UrlbarController.jsm
browser/components/urlbar/UrlbarInput.jsm
browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
browser/components/urlbar/UrlbarPrefs.jsm
browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm
browser/components/urlbar/UrlbarProvidersManager.jsm
browser/components/urlbar/UrlbarUtils.jsm
browser/components/urlbar/UrlbarView.jsm
browser/components/urlbar/docs/telemetry.rst
browser/components/urlbar/tests/UrlbarTestUtils.jsm
browser/components/urlbar/tests/browser/browser_action_searchengine.js
browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js
browser/components/urlbar/tests/browser/browser_canonizeURL.js
browser/components/urlbar/tests/browser/browser_decode.js
browser/components/urlbar/tests/browser/browser_oneOffs.js
browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js
browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js
browser/components/urlbar/tests/browser/browser_oneOffs_settings.js
browser/components/urlbar/tests/browser/browser_remove_match.js
browser/components/urlbar/tests/browser/browser_searchSuggestions.js
browser/components/urlbar/tests/browser/browser_searchTelemetry.js
browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js
browser/components/urlbar/tests/unit/data/engine-suggestions.xml
browser/components/urlbar/tests/unit/head.js
browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
browser/components/urlbar/tests/unit/test_muxer.js
browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js
browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js
browser/components/urlbar/tests/unit/test_providersManager_filtering.js
browser/components/urlbar/tests/unit/test_search_suggestions.js
browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
browser/modules/BrowserUsageTelemetry.jsm
browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
browser/modules/test/browser/browser_UsageTelemetry_urlbar_extension.js
browser/modules/test/browser/browser_UsageTelemetry_urlbar_places.js
browser/modules/test/browser/browser_UsageTelemetry_urlbar_remotetab.js
browser/modules/test/browser/browser_UsageTelemetry_urlbar_tip.js
browser/modules/test/browser/browser_UsageTelemetry_urlbar_topsite.js
toolkit/components/search/SearchSuggestionController.jsm
toolkit/components/telemetry/Events.yaml
toolkit/components/telemetry/Histograms.json
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -277,17 +277,21 @@ pref("browser.urlbar.filter.javascript",
 // the maximum number of results to show in autocomplete when doing richResults
 pref("browser.urlbar.maxRichResults", 10);
 // The amount of time (ms) to wait after the user has stopped typing
 // before starting to perform autocomplete.  50 is the default set in
 // autocomplete.xml.
 pref("browser.urlbar.delay", 50);
 
 // The maximum number of historical search results to show.
+#ifdef EARLY_BETA_OR_EARLIER
+pref("browser.urlbar.maxHistoricalSearchSuggestions", 2);
+#else
 pref("browser.urlbar.maxHistoricalSearchSuggestions", 0);
+#endif
 
 // When true, URLs in the user's history that look like search result pages
 // are styled to look like search engine results instead of the usual history
 // results.
 pref("browser.urlbar.restyleSearches", false);
 
 // The default behavior for the urlbar can be configured to use any combination
 // of the match filters with each additional filter adding more results (union).
--- a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
+++ b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
@@ -44,16 +44,17 @@ add_task(async function startup() {
   Services.prefs.setCharPref("browser.search.region", "US");
   Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
   Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
   Services.prefs.setBoolPref(
     "browser.search.separatePrivateDefault.ui.enabled",
     false
   );
   await AddonTestUtils.promiseStartupManager();
+  await UrlbarTestUtils.initXPCShellDependencies();
 
   // Add a test engine and make it default so that when we do searches below,
   // Firefox doesn't try to include search suggestions from the actual default
   // engine from over the network.
   let engine = await Services.search.addEngineWithDetails("Test engine", {
     template: "http://example.com/?s=%S",
     alias: "@testengine",
   });
--- a/browser/components/urlbar/UrlbarController.jsm
+++ b/browser/components/urlbar/UrlbarController.jsm
@@ -8,16 +8,17 @@ var EXPORTED_SYMBOLS = ["UrlbarControlle
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
+  FormHistory: "resource://gre/modules/FormHistory.jsm",
   PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
   UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
   URLBAR_SELECTED_RESULT_TYPES: "resource:///modules/BrowserUsageTelemetry.jsm",
 });
 
 const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
@@ -496,32 +497,36 @@ class UrlbarController {
     if (!result) {
       return;
     }
 
     // Do not modify existing telemetry types.  To add a new type:
     //
     // * Set telemetryType appropriately below.
     // * Add the type to BrowserUsageTelemetry.URLBAR_SELECTED_RESULT_TYPES.
-    // * See n_values in Histograms.json for FX_URLBAR_SELECTED_RESULT_TYPE and
-    //   FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE.  If your new type causes the
-    //   number of types to become larger than n_values, you'll need to replace
-    //   these histograms with new ones.  See "Changing a histogram" in the
-    //   telemetry docs for more.
+    // * See n_values in Histograms.json for FX_URLBAR_SELECTED_RESULT_TYPE_2
+    //   and FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2.  If your new type causes
+    //   the number of types to become larger than n_values, you'll need to
+    //   replace these histograms with new ones.  See "Changing a histogram" in
+    //   the histogram telemetry doc for more.
     // * Add a test named browser_UsageTelemetry_urlbar_newType.js to
     //   browser/modules/test/browser.
     let telemetryType;
     switch (result.type) {
       case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
         telemetryType = "switchtab";
         break;
       case UrlbarUtils.RESULT_TYPE.SEARCH:
-        telemetryType = result.payload.suggestion
-          ? "searchsuggestion"
-          : "searchengine";
+        if (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY) {
+          telemetryType = "formhistory";
+        } else {
+          telemetryType = result.payload.suggestion
+            ? "searchsuggestion"
+            : "searchengine";
+        }
         break;
       case UrlbarUtils.RESULT_TYPE.URL:
         if (result.autofill) {
           telemetryType = "autofill";
         } else if (
           result.source == UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL &&
           result.heuristic
         ) {
@@ -555,24 +560,24 @@ class UrlbarController {
       telemetryType = "topsite";
     }
 
     Services.telemetry
       .getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX")
       .add(resultIndex);
     if (telemetryType in URLBAR_SELECTED_RESULT_TYPES) {
       Services.telemetry
-        .getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE")
+        .getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE_2")
         .add(URLBAR_SELECTED_RESULT_TYPES[telemetryType]);
       Services.telemetry
-        .getKeyedHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE")
+        .getKeyedHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2")
         .add(telemetryType, resultIndex);
     } else {
       Cu.reportError(
-        "Unknown FX_URLBAR_SELECTED_RESULT_TYPE type: " + telemetryType
+        "Unknown FX_URLBAR_SELECTED_RESULT_TYPE_2 type: " + telemetryType
       );
     }
   }
 
   /**
    * Internal function handling deletion of entries. We only support removing
    * of history entries - other result sources will be ignored.
    *
@@ -597,16 +602,37 @@ class UrlbarController {
     if (!index) {
       Cu.reportError("Failed to find the selected result in the results");
       return false;
     }
 
     queryContext.results.splice(index, 1);
     this.notify(NOTIFICATIONS.QUERY_RESULT_REMOVED, index);
 
+    // form history
+    if (selectedResult.type == UrlbarUtils.RESULT_TYPE.SEARCH) {
+      if (!queryContext.formHistoryName) {
+        return false;
+      }
+      FormHistory.update(
+        {
+          op: "remove",
+          fieldname: queryContext.formHistoryName,
+          value: selectedResult.payload.suggestion,
+        },
+        {
+          handleError(error) {
+            Cu.reportError(`Removing form history failed: ${error}`);
+          },
+        }
+      );
+      return true;
+    }
+
+    // Places history
     PlacesUtils.history
       .remove(selectedResult.payload.url)
       .catch(Cu.reportError);
     return true;
   }
 
   /**
    * Notifies listeners of results.
@@ -839,16 +865,19 @@ class TelemetryEvent {
       return "none";
     }
     let row = element.closest(".urlbarView-row");
     if (row.result) {
       switch (row.result.type) {
         case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
           return "switchtab";
         case UrlbarUtils.RESULT_TYPE.SEARCH:
+          if (row.result.source == UrlbarUtils.RESULT_SOURCE.HISTORY) {
+            return "formhistory";
+          }
           return row.result.payload.suggestion ? "searchsuggestion" : "search";
         case UrlbarUtils.RESULT_TYPE.URL:
           if (row.result.autofill) {
             return "autofill";
           }
           if (row.result.heuristic) {
             return "visit";
           }
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -9,16 +9,17 @@ var EXPORTED_SYMBOLS = ["UrlbarInput"];
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
   ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm",
+  FormHistory: "resource://gre/modules/FormHistory.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   ReaderMode: "resource://gre/modules/ReaderMode.jsm",
   Services: "resource://gre/modules/Services.jsm",
   UrlbarController: "resource:///modules/UrlbarController.jsm",
   UrlbarEventBufferer: "resource:///modules/UrlbarEventBufferer.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
   UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
   UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm",
@@ -30,16 +31,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
 
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "ClipboardHelper",
   "@mozilla.org/widget/clipboardhelper;1",
   "nsIClipboardHelper"
 );
 
+const DEFAULT_FORM_HISTORY_NAME = "searchbar-history";
 const SEARCH_BUTTON_ID = "urlbar-search-button";
 
 let getBoundsWithoutFlushing = element =>
   element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
 let px = number => number.toFixed(2) + "px";
 
 /**
  * Implements the text input part of the address bar UI.
@@ -87,16 +89,17 @@ class UrlbarInput {
     }
 
     this.controller = new UrlbarController({
       input: this,
       eventTelemetryCategory: options.eventTelemetryCategory,
     });
     this.view = new UrlbarView(this);
     this.valueIsTyped = false;
+    this.formHistoryName = DEFAULT_FORM_HISTORY_NAME;
     this.lastQueryContextPromise = Promise.resolve();
     this._actionOverrideKeyCount = 0;
     this._autofillPlaceholder = "";
     this._lastSearchString = "";
     this._lastValidURLStr = "";
     this._valueOnLastSearch = "";
     this._resultForCurrentValue = null;
     this._suppressStartQuery = false;
@@ -673,20 +676,38 @@ class UrlbarInput {
 
         if (result.payload.inPrivateWindow) {
           where = "window";
           openParams.private = true;
         }
 
         const actionDetails = {
           isSuggestion: !!result.payload.suggestion,
+          isFormHistory: result.source == UrlbarUtils.RESULT_SOURCE.HISTORY,
           alias: result.payload.keyword,
         };
         const engine = Services.search.getEngineByName(result.payload.engine);
         this._recordSearch(engine, event, actionDetails);
+
+        // Add the search to form history.  This also updates any existing form
+        // history for the search.
+        if (!this.isPrivate && !result.payload.inPrivateWindow) {
+          FormHistory.update(
+            {
+              op: "bump",
+              fieldname: this.formHistoryName,
+              value: result.payload.suggestion || result.payload.query,
+            },
+            {
+              handleError(error) {
+                Cu.reportError(`Error saving form history: ${error}`);
+              },
+            }
+          );
+        }
         break;
       }
       case UrlbarUtils.RESULT_TYPE.TIP: {
         let scalarName;
         if (element.classList.contains("urlbarView-tip-help")) {
           url = result.payload.helpUrl;
           if (!url) {
             Cu.reportError("helpUrl not specified");
@@ -937,16 +958,17 @@ class UrlbarInput {
         allowAutofill,
         isPrivate: this.isPrivate,
         maxResults: UrlbarPrefs.get("maxRichResults"),
         searchString,
         userContextId: this.window.gBrowser.selectedBrowser.getAttribute(
           "usercontextid"
         ),
         currentPage: this.window.gBrowser.currentURI.spec,
+        formHistoryName: this.formHistoryName,
         allowSearchSuggestions:
           !event ||
           !UrlbarUtils.isPasteEvent(event) ||
           !event.data ||
           event.data.length <= UrlbarPrefs.get("maxCharsForSearchSuggestions"),
       })
     );
   }
@@ -1505,18 +1527,20 @@ class UrlbarInput {
    * @param {nsISearchEngine} engine
    *   The engine to generate the query for.
    * @param {Event} event
    *   The event that triggered this query.
    * @param {object} searchActionDetails
    *   The details associated with this search query.
    * @param {boolean} searchActionDetails.isSuggestion
    *   True if this query was initiated from a suggestion from the search engine.
-   * @param {alias} searchActionDetails.alias
+   * @param {boolean} searchActionDetails.alias
    *   True if this query was initiated via a search alias.
+   * @param {boolean} searchActionDetails.isFormHistory
+   *   True if this query was initiated from a form history result.
    */
   _recordSearch(engine, event, searchActionDetails = {}) {
     const isOneOff = this.view.oneOffSearchButtons.maybeRecordTelemetry(event);
     // Infer the type of the event which triggered the search.
     let eventType = "unknown";
     if (event instanceof KeyboardEvent) {
       eventType = "key";
     } else if (event instanceof MouseEvent) {
--- a/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
+++ b/browser/components/urlbar/UrlbarMuxerUnifiedComplete.jsm
@@ -10,18 +10,19 @@
 
 var EXPORTED_SYMBOLS = ["UrlbarMuxerUnifiedComplete"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 XPCOMUtils.defineLazyModuleGetters(this, {
   Log: "resource://gre/modules/Log.jsm",
+  PlacesSearchAutocompleteProvider:
+    "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
-  UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
   UrlbarMuxer: "resource:///modules/UrlbarUtils.jsm",
   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(this, "logger", () =>
   Log.repository.getLogger("Urlbar.Muxer.UnifiedComplete")
 );
 
@@ -54,77 +55,77 @@ class MuxerUnifiedComplete extends Urlba
     return "UnifiedComplete";
   }
 
   /**
    * Sorts results in the given UrlbarQueryContext.
    *
    * @param {UrlbarQueryContext} context
    *   The query context.
-   * @returns {boolean}
-   *   True if the muxer sorted the results and false if not.  The muxer may
-   *   decide it can't sort if there aren't yet enough results to make good
-   *   decisions, for example to avoid flicker in the view.
    */
   sort(context) {
     // This method is called multiple times per keystroke, so it should be as
-    // fast and efficient as possible.  We do one pass through active providers
-    // and two passes through the results: one to collect info for the second
-    // pass, and then a second to build the unsorted list of results.  If you
-    // find yourself writing something like context.results.find(), filter(),
-    // sort(), etc., modify one or both passes instead.
-
-    // Collect info from the active providers.
-    for (let providerName of context.activeProviders) {
-      let provider = UrlbarProvidersManager.getProvider(providerName);
-
-      // If the provider of the heuristic result is still active and the result
-      // hasn't been created yet, bail.  Otherwise we may show another result
-      // first and then later replace it with the heuristic, causing flicker.
-      if (
-        provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC &&
-        !context.heuristicResult
-      ) {
-        return false;
-      }
-    }
+    // fast and efficient as possible.  We do two passes through the results:
+    // one to collect info for the second pass, and then a second to build the
+    // unsorted list of results.  If you find yourself writing something like
+    // context.results.find(), filter(), sort(), etc., modify one or both passes
+    // instead.
 
     let heuristicResultQuery;
     if (
       context.heuristicResult &&
       context.heuristicResult.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
       context.heuristicResult.payload.query
     ) {
       heuristicResultQuery = context.heuristicResult.payload.query.toLocaleLowerCase();
     }
 
-    // We update canShowPrivateSearch below too.  This is its initial value.
     let canShowPrivateSearch = context.results.length > 1;
+    let canShowTailSuggestions = true;
     let resultsWithSuggestedIndex = [];
-
-    // If we find results other than the heuristic, "Search in Private Window,"
-    // or tail suggestions on the first pass, we should hide tail suggestions on
-    // the second, since tail suggestions are a "last resort".
-    let canShowTailSuggestions = true;
+    let formHistoryResults = new Set();
+    let formHistorySuggestions = new Set();
+    let maxFormHistoryCount = Math.min(
+      UrlbarPrefs.get("maxHistoricalSearchSuggestions"),
+      context.maxResults
+    );
 
     // Do the first pass through the results.  We only collect info for the
     // second pass here.
     for (let result of context.results) {
       // The "Search in a Private Window" result should only be shown when there
       // are other results and all of them are searches.  It should not be shown
       // if the user typed an alias because that's an explicit engine choice.
       if (
         canShowPrivateSearch &&
         (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
           result.payload.keywordOffer ||
           (result.heuristic && result.payload.keyword))
       ) {
         canShowPrivateSearch = false;
       }
 
+      // Include form history up to the max count that doesn't dupe the
+      // heuristic.  The search suggestions provider fetches max count + 1 form
+      // history results so that the muxer can exclude a result that equals the
+      // heuristic if necessary.
+      if (
+        result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+        result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
+        formHistoryResults.size < maxFormHistoryCount &&
+        result.payload.lowerCaseSuggestion &&
+        result.payload.lowerCaseSuggestion != heuristicResultQuery
+      ) {
+        formHistoryResults.add(result);
+        formHistorySuggestions.add(result.payload.lowerCaseSuggestion);
+      }
+
+      // If we find results other than the heuristic, "Search in Private
+      // Window," or tail suggestions on the first pass, we should hide tail
+      // suggestions on the second, since tail suggestions are a "last resort".
       if (
         canShowTailSuggestions &&
         !result.heuristic &&
         (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
           (!result.payload.inPrivateWindow && !result.payload.tail))
       ) {
         canShowTailSuggestions = false;
       }
@@ -143,34 +144,81 @@ class MuxerUnifiedComplete extends Urlba
       }
 
       // Save suggestedIndex results for later.
       if (result.suggestedIndex >= 0) {
         resultsWithSuggestedIndex.push(result);
         continue;
       }
 
-      // Exclude remote search suggestions that dupe the heuristic result.
+      // Exclude form history as determined in the first pass.
       if (
         result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
-        result.payload.suggestion &&
-        result.payload.suggestion.toLocaleLowerCase() === heuristicResultQuery
+        result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
+        !formHistoryResults.has(result)
+      ) {
+        continue;
+      }
+
+      // Exclude remote search suggestions that dupe the heuristic.  We also
+      // want to exclude remote suggestions that dupe form history, but that's
+      // already been done by the search suggestions controller.
+      if (
+        result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+        result.source == UrlbarUtils.RESULT_SOURCE.SEARCH &&
+        result.payload.lowerCaseSuggestion &&
+        result.payload.lowerCaseSuggestion === heuristicResultQuery
       ) {
         continue;
       }
 
       // Exclude tail suggestions if we have non-tail suggestions.
       if (
         !canShowTailSuggestions &&
         groupFromResult(result) == UrlbarUtils.RESULT_GROUP.SUGGESTION &&
         result.payload.tail
       ) {
         continue;
       }
 
+      // Exclude SERPs from browser history that dupe either the heuristic or
+      // included form history.
+      if (
+        result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
+        result.type == UrlbarUtils.RESULT_TYPE.URL
+      ) {
+        let submission;
+        try {
+          // parseSubmissionURL throws if PlacesSearchAutocompleteProvider
+          // hasn't finished initializing, so try-catch this call.  There's no
+          // harm if it throws, we just won't dedupe SERPs this time.
+          submission = PlacesSearchAutocompleteProvider.parseSubmissionURL(
+            result.payload.url
+          );
+        } catch (error) {}
+        if (submission) {
+          let resultQuery = submission.terms.toLocaleLowerCase();
+          if (
+            heuristicResultQuery === resultQuery ||
+            formHistorySuggestions.has(resultQuery)
+          ) {
+            // If the result's URL is the same as a brand new SERP URL created
+            // from the query string modulo certain URL params, then treat the
+            // result as a dupe and exclude it.
+            let [newSerpURL] = UrlbarUtils.getSearchQueryUrl(
+              submission.engine,
+              resultQuery
+            );
+            if (this._serpURLsHaveSameParams(newSerpURL, result.payload.url)) {
+              continue;
+            }
+          }
+        }
+      }
+
       // Include this result.
       unsortedResults.push(result);
     }
 
     // If the heuristic result is a search result, use search buckets, otherwise
     // use normal buckets.
     let buckets =
       context.heuristicResult &&
@@ -210,13 +258,46 @@ class MuxerUnifiedComplete extends Urlba
       let index =
         result.suggestedIndex <= sortedResults.length
           ? result.suggestedIndex
           : sortedResults.length;
       sortedResults.splice(index, 0, result);
     }
 
     context.results = sortedResults;
+  }
+
+  /**
+   * This is a helper for determining whether two SERP URLs are the same for the
+   * purpose of deduping them.  This method checks only URL params, not domains.
+   *
+   * @param {string} url1
+   *   The first URL.
+   * @param {string} url2
+   *   The second URL.
+   * @returns {boolean}
+   *   True if the two URLs have the same URL params for the purpose of deduping
+   *   them.
+   */
+  _serpURLsHaveSameParams(url1, url2) {
+    let params1 = new URL(url1).searchParams;
+    let params2 = new URL(url2).searchParams;
+    // Currently we are conservative, and the two URLs must have exactly the
+    // same params except for "client" for us to consider them the same.
+    for (let params of [params1, params2]) {
+      params.delete("client");
+    }
+    // Check that each remaining url1 param is in url2, and vice versa.
+    for (let [p1, p2] of [
+      [params1, params2],
+      [params2, params1],
+    ]) {
+      for (let [key, value] of p1) {
+        if (!p2.getAll(key).includes(value)) {
+          return false;
+        }
+      }
+    }
     return true;
   }
 }
 
 var UrlbarMuxerUnifiedComplete = new MuxerUnifiedComplete();
--- a/browser/components/urlbar/UrlbarPrefs.jsm
+++ b/browser/components/urlbar/UrlbarPrefs.jsm
@@ -76,19 +76,17 @@ const PREF_URLBAR_DEFAULTS = new Map([
   // If the heuristic result is a search engine result, we use this instead of
   // matchBuckets.
   ["matchBucketsSearch", ""],
 
   // For search suggestion results, we truncate the user's search string to this
   // number of characters before fetching results.
   ["maxCharsForSearchSuggestions", 20],
 
-  // May be removed in the future.  Usually (when this pref is at its default of
-  // zero), search engine results do not include results from the user's local
-  // browser history.  This value can be set to include such results.
+  // The maximum number of form history results to include.
   ["maxHistoricalSearchSuggestions", 0],
 
   // The maximum number of results in the urlbar popup.
   ["maxRichResults", 10],
 
   // Whether addresses and search results typed into the address bar
   // should be opened in new tabs by default.
   ["openintab", false],
--- a/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm
+++ b/browser/components/urlbar/UrlbarProviderSearchSuggestions.jsm
@@ -111,88 +111,96 @@ class ProviderSearchSuggestions extends 
   /**
    * Whether this provider should be invoked for the given context.
    * If this method returns false, the providers manager won't start a query
    * with this provider, to save on resources.
    * @param {UrlbarQueryContext} queryContext The query context object
    * @returns {boolean} Whether this provider should be invoked for the search.
    */
   isActive(queryContext) {
+    // If the sources don't include search or the user used a restriction
+    // character other than search, don't allow any suggestions.
     if (
-      !UrlbarPrefs.get("browser.search.suggest.enabled") ||
-      !UrlbarPrefs.get("suggest.searches")
+      !queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH) ||
+      (queryContext.restrictSource &&
+        queryContext.restrictSource != UrlbarUtils.RESULT_SOURCE.SEARCH)
     ) {
       return false;
     }
 
-    if (!queryContext.allowSearchSuggestions) {
+    // No suggestions for empty search strings.
+    if (!queryContext.searchString.trim()) {
       return false;
     }
 
+    return this._formHistoryCount || this._allowRemoteSuggestions(queryContext);
+  }
+
+  _allowRemoteSuggestions(queryContext) {
+    // Check preferences and other values that immediately prohibit remote
+    // suggestions.
     if (
-      queryContext.isPrivate &&
-      !UrlbarPrefs.get("browser.search.suggest.enabled.private")
+      !queryContext.allowSearchSuggestions ||
+      !UrlbarPrefs.get("suggest.searches") ||
+      !UrlbarPrefs.get("browser.search.suggest.enabled") ||
+      (queryContext.isPrivate &&
+        !UrlbarPrefs.get("browser.search.suggest.enabled.private"))
     ) {
       return false;
     }
 
-    // This condition is met if the user entered a restriction token other than
-    // the token for search.
-    if (!queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH)) {
-      return false;
-    }
-
-    if (
-      queryContext.restrictSource &&
-      queryContext.restrictSource != UrlbarUtils.RESULT_SOURCE.SEARCH
-    ) {
-      return false;
+    // Skip all remaining checks and allow remote suggestions at this point if
+    // the user used a search engine token alias.  We want "@engine query" to
+    // return suggestions from the engine.  We'll return early from startQuery
+    // if the query doesn't match an alias.
+    if (queryContext.searchString.startsWith("@")) {
+      return true;
     }
 
     // If the user is just adding on to a query that previously didn't return
-    // many results, we are unlikely to get any more results.
+    // many remote suggestions, we are unlikely to get any more results.
     if (
       !!this._lastLowResultsSearchSuggestion &&
       queryContext.searchString.length >
         this._lastLowResultsSearchSuggestion.length &&
       queryContext.searchString.startsWith(this._lastLowResultsSearchSuggestion)
     ) {
       return false;
     }
 
-    // Never prohibit suggestions when the user used a search engine token
-    // alias.  We want "@engine query" to return suggestions from the engine.
-    // We'll return early from startQuery if the query doesn't match an alias.
-    if (queryContext.searchString.startsWith("@")) {
-      return true;
-    }
-
-    // We're unlikely to get useful suggestions for single-character queries.
+    // We're unlikely to get useful remote suggestions for single characters.
     if (queryContext.searchString.length < 2) {
       return false;
     }
 
-    // Disallow suggestions if only an origin is typed.
+    // Disallow remote suggestions if only an origin is typed to avoid
+    // disclosing information about sites the user visits.
     if (
       queryContext.tokens.length == 1 &&
       queryContext.tokens[0].type == UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN
     ) {
       return false;
     }
 
-    // Disallow fetching search suggestions for strings containing tokens that
-    // look like URLs or non-alphanumeric origins, to avoid disclosing
-    // information about networks or passwords.
-    return !queryContext.tokens.some(t => {
-      return (
-        t.type == UrlbarTokenizer.TYPE.POSSIBLE_URL ||
-        (t.type == UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN &&
-          !UrlbarTokenizer.REGEXP_SINGLE_WORD_HOST.test(t.value))
-      );
-    });
+    // Disallow remote suggestions for strings containing tokens that look like
+    // URLs or non-alphanumeric origins, to avoid disclosing information about
+    // networks or passwords.
+    if (
+      queryContext.tokens.some(
+        t =>
+          t.type == UrlbarTokenizer.TYPE.POSSIBLE_URL ||
+          (t.type == UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN &&
+            !UrlbarTokenizer.REGEXP_SINGLE_WORD_HOST.test(t.value))
+      )
+    ) {
+      return false;
+    }
+
+    // Allow remote suggestions.
+    return true;
   }
 
   /**
    * Starts querying.
    * @param {object} queryContext The query context object
    * @param {function} addCallback Callback invoked by the provider to add a new
    *        result.
    * @returns {Promise} resolved when the query stops.
@@ -276,17 +284,17 @@ class ProviderSearchSuggestions extends 
     let alias = (aliasEngine && aliasEngine.alias) || "";
     let results = await this._fetchSearchSuggestions(
       queryContext,
       engine,
       query,
       alias
     );
 
-    if (!this.queries.has(queryContext)) {
+    if (!results || !this.queries.has(queryContext)) {
       return;
     }
 
     for (let result of results) {
       addCallback(this, result);
     }
 
     this.queries.delete(queryContext);
@@ -311,119 +319,140 @@ class ProviderSearchSuggestions extends 
     if (this._suggestionsController) {
       this._suggestionsController.stop();
       this._suggestionsController = null;
     }
 
     this.queries.delete(queryContext);
   }
 
+  get _formHistoryCount() {
+    let count = UrlbarPrefs.get("maxHistoricalSearchSuggestions");
+    if (!count) {
+      return 0;
+    }
+    // If there's a form history entry that equals the search string, the search
+    // suggestions controller will include it, and we'll make a result for it.
+    // If the heuristic result ends up being a search result, the muxer will
+    // exclude the form history result since it dupes the heuristic, and the
+    // final list of results would be left with `count` - 1 form history results
+    // instead of `count`.  Therefore we request `count` + 1 entries.  The muxer
+    // will dedupe and limit the final form history count as appropriate.
+    return count + 1;
+  }
+
   async _fetchSearchSuggestions(queryContext, engine, searchString, alias) {
     if (!engine || !searchString) {
       return null;
     }
 
     this._suggestionsController = new SearchSuggestionController();
-    this._suggestionsController.maxLocalResults = UrlbarPrefs.get(
-      "maxHistoricalSearchSuggestions"
-    );
-    this._suggestionsController.maxRemoteResults =
-      queryContext.maxResults -
-      UrlbarPrefs.get("maxHistoricalSearchSuggestions");
+    this._suggestionsController.formHistoryParam = queryContext.formHistoryName;
+    this._suggestionsController.maxLocalResults = this._formHistoryCount;
+
+    let allowRemote = this._allowRemoteSuggestions(queryContext);
+
+    // Request maxResults + 1 remote suggestions for the same reason we request
+    // maxHistoricalSearchSuggestions + 1 form history entries; see
+    // _formHistoryCount.  We allow for the possibility that the engine may
+    // return a suggestion that's the same as the search string.
+    this._suggestionsController.maxRemoteResults = allowRemote
+      ? queryContext.maxResults + 1
+      : 0;
 
     this._suggestionsFetchCompletePromise = this._suggestionsController.fetch(
       searchString,
       queryContext.isPrivate,
       engine,
       queryContext.userContextId
     );
 
     // See `SearchSuggestionsController.fetch` documentation for a description
     // of `fetchData`.
     let fetchData = await this._suggestionsFetchCompletePromise;
     // The fetch was canceled.
     if (!fetchData) {
       return null;
     }
 
-    let suggestions = [];
-    suggestions.push(
-      ...fetchData.local.map(e => ({ entry: e, historical: true })),
-      ...fetchData.remote.map(e => ({ entry: e, historical: false }))
-    );
+    let results = [];
+
+    for (let entry of fetchData.local) {
+      results.push(
+        new UrlbarResult(
+          UrlbarUtils.RESULT_TYPE.SEARCH,
+          UrlbarUtils.RESULT_SOURCE.HISTORY,
+          ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+            engine: engine.name,
+            suggestion: [entry.value, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+            lowerCaseSuggestion: entry.value.toLocaleLowerCase(),
+          })
+        )
+      );
+    }
 
     // If we don't return many results, then keep track of the query. If the
     // user just adds on to the query, we won't fetch more suggestions if the
     // query is very long since we are unlikely to get any.
     if (
-      !suggestions.length &&
+      allowRemote &&
+      !fetchData.remote.length &&
       searchString.length > UrlbarPrefs.get("maxCharsForSearchSuggestions")
     ) {
       this._lastLowResultsSearchSuggestion = searchString;
     }
 
     // If we have only tail suggestions, we only show them if we have no other
     // results. We need to wait for other results to arrive to avoid flickering.
     // We will wait for this timer unless we have suggestions that don't have a
     // tail.
     let tailTimer = new SkippableTimer({
       name: "ProviderSearchSuggestions",
       time: 100,
       logger,
     });
 
-    let results = [];
-    for (let suggestion of suggestions) {
-      if (
-        !suggestion ||
-        suggestion.entry.value == searchString ||
-        looksLikeUrl(suggestion.entry.value)
-      ) {
+    for (let entry of fetchData.remote) {
+      if (looksLikeUrl(entry.value)) {
         continue;
       }
 
-      if (suggestion.entry.tail && suggestion.entry.tailOffsetIndex < 0) {
+      if (entry.tail && entry.tailOffsetIndex < 0) {
         Cu.reportError(
-          `Error in tail suggestion parsing. Value: ${suggestion.entry.value}, tail: ${suggestion.entry.tail}.`
+          `Error in tail suggestion parsing. Value: ${entry.value}, tail: ${entry.tail}.`
         );
         continue;
       }
 
       let tail, tailPrefix;
       if (UrlbarPrefs.get("richSuggestions.tail")) {
-        tail = suggestion.entry.tail;
-        tailPrefix = suggestion.entry.matchPrefix;
+        tail = entry.tail;
+        tailPrefix = entry.matchPrefix;
       }
 
       if (!tail) {
         await tailTimer.fire().catch(Cu.reportError);
       }
 
       try {
         results.push(
           new UrlbarResult(
             UrlbarUtils.RESULT_TYPE.SEARCH,
             UrlbarUtils.RESULT_SOURCE.SEARCH,
             ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
               engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
-              suggestion: [
-                suggestion.entry.value,
-                UrlbarUtils.HIGHLIGHT.SUGGESTED,
-              ],
+              suggestion: [entry.value, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+              lowerCaseSuggestion: entry.value.toLocaleLowerCase(),
               tailPrefix,
               tail: [tail, UrlbarUtils.HIGHLIGHT.SUGGESTED],
-              tailOffsetIndex: suggestion.entry.tailOffsetIndex,
+              tailOffsetIndex: entry.tailOffsetIndex,
               keyword: [alias ? alias : undefined, UrlbarUtils.HIGHLIGHT.TYPED],
               query: [searchString.trim(), UrlbarUtils.HIGHLIGHT.NONE],
               isSearchHistory: false,
-              icon: [
-                engine.iconURI && !suggestion.entry.value
-                  ? engine.iconURI.spec
-                  : "",
-              ],
+              icon: [engine.iconURI && !entry.value ? engine.iconURI.spec : ""],
               keywordOffer: UrlbarUtils.KEYWORD_OFFER.NONE,
             })
           )
         );
       } catch (err) {
         Cu.reportError(err);
         continue;
       }
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -333,60 +333,46 @@ class Query {
     // logic above.
     await Promise.all(activePromises);
 
     if (this.canceled) {
       this.controller = null;
       return;
     }
 
-    this.context.activeProviders = new Set(activeProviders.map(p => p.name));
-
     // Start querying active providers.
     let queryPromises = [];
     for (let provider of activeProviders) {
-      let promise;
       if (provider.type == UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
-        promise = provider.tryMethod(
-          "startQuery",
-          this.context,
-          this.add.bind(this)
+        queryPromises.push(
+          provider.tryMethod("startQuery", this.context, this.add.bind(this))
         );
-      } else {
-        if (!this._sleepTimer) {
-          // Tracks the delay timer. We will fire (in this specific case, cancel
-          // would do the same, since the callback is empty) the timer when the
-          // search is canceled, unblocking start().
-          this._sleepTimer = new SkippableTimer({
-            name: "Query provider timer",
-            time: UrlbarPrefs.get("delay"),
-            logger,
-          });
-        }
-        promise = this._sleepTimer.promise.then(() => {
+        continue;
+      }
+      if (!this._sleepTimer) {
+        // Tracks the delay timer. We will fire (in this specific case, cancel
+        // would do the same, since the callback is empty) the timer when the
+        // search is canceled, unblocking start().
+        this._sleepTimer = new SkippableTimer({
+          name: "Query provider timer",
+          time: UrlbarPrefs.get("delay"),
+          logger,
+        });
+      }
+      queryPromises.push(
+        this._sleepTimer.promise.then(() => {
           if (this.canceled) {
             return undefined;
           }
           return provider.tryMethod(
             "startQuery",
             this.context,
             this.add.bind(this)
           );
-        });
-      }
-      queryPromises.push(
-        promise
-          .catch(Cu.reportError)
-          .then(() => {
-            if (!this.canceled) {
-              this.context.activeProviders.delete(provider.name);
-              this._notifyResultsFromProvider(provider);
-            }
-          })
-          .catch(Cu.reportError)
+        })
       );
     }
 
     logger.info(`Queried ${queryPromises.length} providers`);
     await Promise.all(queryPromises);
 
     if (!this.canceled && this._chunkTimer) {
       // All the providers are done returning results, so we can stop chunking.
@@ -427,17 +413,24 @@ class Query {
       throw new Error("Invalid provider passed to the add callback");
     }
     // Stop returning results as soon as we've been canceled.
     if (this.canceled) {
       return;
     }
     // Check if the result source should be filtered out. Pay attention to the
     // heuristic result though, that is supposed to be added regardless.
-    if (!this.acceptableSources.includes(result.source) && !result.heuristic) {
+    if (
+      !this.acceptableSources.includes(result.source) &&
+      !result.heuristic &&
+      // Treat form history as searches for the purpose of acceptableSources.
+      (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+        result.source != UrlbarUtils.RESULT_SOURCE.HISTORY ||
+        !this.acceptableSources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH))
+    ) {
       return;
     }
 
     // Filter out javascript results for safety. The provider is supposed to do
     // it, but we don't want to risk leaking these out.
     if (
       result.type != UrlbarUtils.RESULT_TYPE.KEYWORD &&
       result.payload.url &&
@@ -468,20 +461,17 @@ class Query {
         callback: () => this._notifyResults(),
         time: CHUNK_RESULTS_DELAY_MS,
         logger,
       });
     }
   }
 
   _notifyResults() {
-    if (!this.muxer.sort(this.context) || !this.context.results.length) {
-      // The muxer needs more results before it can decide how to sort them.
-      return;
-    }
+    this.muxer.sort(this.context);
 
     if (this._chunkTimer) {
       this._chunkTimer.cancel().catch(Cu.reportError);
       this._chunkTimer = null;
     }
 
     // Crop results to the requested number, taking their result spans into
     // account.
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -93,16 +93,17 @@ var UrlbarUtils = {
     TABS: 4,
     OTHER_LOCAL: 5,
     OTHER_NETWORK: 6,
   },
 
   // This defines icon locations that are commonly used in the UI.
   ICON: {
     // DEFAULT is defined lazily so it doesn't eagerly initialize PlacesUtils.
+    HISTORY: "chrome://browser/skin/history.svg",
     SEARCH_GLASS: "chrome://browser/skin/search-glass.svg",
     TIP: "chrome://browser/skin/tip.svg",
   },
 
   // The number of results by which Page Up/Down move the selection.
   PAGE_UP_DOWN_DELTA: 5,
 
   // IME composition states.
@@ -613,36 +614,39 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
         type: "string",
       },
       engine: {
         type: "string",
       },
       icon: {
         type: "string",
       },
+      inPrivateWindow: {
+        type: "boolean",
+      },
       isPinned: {
         type: "boolean",
       },
-      inPrivateWindow: {
+      isPrivateEngine: {
         type: "boolean",
       },
-      isPrivateEngine: {
+      isSearchHistory: {
         type: "boolean",
       },
       keyword: {
         type: "string",
       },
       keywordOffer: {
         type: "number", // UrlbarUtils.KEYWORD_OFFER
       },
-      query: {
+      lowerCaseSuggestion: {
         type: "string",
       },
-      isSearchHistory: {
-        type: "boolean",
+      query: {
+        type: "string",
       },
       suggestion: {
         type: "string",
       },
       tail: {
         type: "string",
       },
       tailPrefix: {
@@ -852,16 +856,18 @@ class UrlbarQueryContext {
    *   If sources is restricting to just SEARCH, this property can be used to
    *   pick a specific search engine, by setting it to the name under which the
    *   engine is registered with the search service.
    * @param {boolean} [options.allowSearchSuggestions]
    *   Whether to allow search suggestions.  This is a veto, meaning that when
    *   false, suggestions will not be fetched, but when true, some other
    *   condition may still prohibit suggestions, like private browsing mode.
    *   Defaults to true.
+   * @param {string} [options.formHistoryName]
+   *   The name under which the local form history is registered.
    */
   constructor(options = {}) {
     this._checkRequiredOptions(options, [
       "allowAutofill",
       "isPrivate",
       "maxResults",
       "searchString",
     ]);
@@ -869,21 +875,22 @@ class UrlbarQueryContext {
     if (isNaN(parseInt(options.maxResults))) {
       throw new Error(
         `Invalid maxResults property provided to UrlbarQueryContext`
       );
     }
 
     // Manage optional properties of options.
     for (let [prop, checkFn, defaultValue] of [
+      ["allowSearchSuggestions", v => true, true],
+      ["currentPage", v => typeof v == "string" && !!v.length],
+      ["engineName", v => typeof v == "string" && !!v.length],
+      ["formHistoryName", v => typeof v == "string" && !!v.length],
       ["providers", v => Array.isArray(v) && v.length],
       ["sources", v => Array.isArray(v) && v.length],
-      ["engineName", v => typeof v == "string" && !!v.length],
-      ["currentPage", v => typeof v == "string" && !!v.length],
-      ["allowSearchSuggestions", v => true, true],
     ]) {
       if (prop in options) {
         if (!checkFn(options[prop])) {
           throw new Error(`Invalid value for option "${prop}"`);
         }
         this[prop] = options[prop];
       } else if (defaultValue !== undefined) {
         this[prop] = defaultValue;
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -892,17 +892,17 @@ class UrlbarView {
       item.removeAttribute("type");
     }
 
     let favicon = item._elements.get("favicon");
     if (
       result.type == UrlbarUtils.RESULT_TYPE.SEARCH ||
       result.type == UrlbarUtils.RESULT_TYPE.KEYWORD
     ) {
-      favicon.src = result.payload.icon || UrlbarUtils.ICON.SEARCH_GLASS;
+      favicon.src = this._iconForSearchResult(result);
     } else {
       favicon.src = result.payload.icon || UrlbarUtils.ICON.DEFAULT;
     }
 
     if (result.payload.isPinned) {
       item.toggleAttribute("pinned", true);
     } else {
       item.removeAttribute("pinned");
@@ -1022,16 +1022,28 @@ class UrlbarView {
       title.setAttribute("dir", "auto");
     } else {
       title.removeAttribute("dir");
     }
 
     item._elements.get("titleSeparator").hidden = !action && !setURL;
   }
 
+  _iconForSearchResult(result, engineOverride = null) {
+    return (
+      (result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
+        UrlbarUtils.ICON.HISTORY) ||
+      (engineOverride &&
+        engineOverride.iconURI &&
+        engineOverride.iconURI.spec) ||
+      result.payload.icon ||
+      UrlbarUtils.ICON.SEARCH_GLASS
+    );
+  }
+
   _updateRowForTip(item, result) {
     let favicon = item._elements.get("favicon");
     favicon.src = result.payload.icon || UrlbarUtils.ICON.TIP;
     favicon.id = item.id + "-icon";
 
     let title = item._elements.get("title");
     title.id = item.id + "-title";
     // Add-ons will provide text, rather than l10n ids.
@@ -1488,16 +1500,17 @@ class UrlbarView {
       let favicon = item.querySelector(".urlbarView-favicon");
       if (engine && result.payload.icon) {
         favicon.src =
           (engine.iconURI && engine.iconURI.spec) ||
           UrlbarUtils.ICON.SEARCH_GLASS;
       } else if (!engine) {
         favicon.src = result.payload.icon || UrlbarUtils.ICON.SEARCH_GLASS;
       }
+      favicon.src = this._iconForSearchResult(result, engine);
     }
   }
 
   _on_blur(event) {
     // If the view is open without the input being focused, it will not close
     // automatically when the window loses focus. We might be in this state
     // after a Search Tip is shown on an engine homepage.
     if (!UrlbarPrefs.get("ui.popup.disable_autohide")) {
--- a/browser/components/urlbar/docs/telemetry.rst
+++ b/browser/components/urlbar/docs/telemetry.rst
@@ -46,16 +46,19 @@ FX_URLBAR_SELECTED_RESULT_METHOD
     Before QuantumBar, it was possible to right-click a result to highlight but
     not pick it. Then the user could press Enter. This is no more possible.
 
 FX_URLBAR_SELECTED_RESULT_INDEX
   This probe tracks the indexes of picked results in the results list.
   It's an enumerated histogram with 17 buckets.
 
 FX_URLBAR_SELECTED_RESULT_TYPE
+  This probe was replaced with FX_URLBAR_SELECTED_RESULT_TYPE_2 in Firefox 78
+  because we needed more buckets.
+
   This probe tracks the types of picked results.
   It's an enumerated histogram with 14 buckets.
   Values can be:
 
     0. autofill
     1. bookmark
     2. history
     3. keyword
@@ -65,17 +68,48 @@ FX_URLBAR_SELECTED_RESULT_TYPE
     7. tag
     8. visiturl
     9. remotetab
     10. extension
     11. preloaded-top-site
     12. tip
     13. topsite
 
+FX_URLBAR_SELECTED_RESULT_TYPE_2
+  This probe tracks the types of picked results.
+  It's an enumerated histogram with 32 buckets.
+  Values can be:
+
+    0. autofill
+    1. bookmark
+    2. history
+    3. keyword
+    4. searchengine
+    5. searchsuggestion
+    6. switchtab
+    7. tag
+    8. visiturl
+    9. remotetab
+    10. extension
+    11. preloaded-top-site
+    12. tip
+    13. topsite
+    14. formhistory
+
 FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE
+  This probe was replaced with FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2 in
+  Firefox 78 because we needed more buckets.
+
+  This probe tracks picked result type, for each one it tracks the index where
+  it appeared.
+  It's a keyed histogram where the keys are result types (see
+  URLBAR_SELECTED_RESULT_TYPES). For each key, this records the indexes of
+  picked results for that result type.
+
+FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2
   This probe tracks picked result type, for each one it tracks the index where
   it appeared.
   It's a keyed histogram where the keys are result types (see
   URLBAR_SELECTED_RESULT_TYPES). For each key, this records the indexes of
   picked results for that result type.
 
 Scalars
 -------
@@ -208,17 +242,17 @@ Event Extra
     Number of input characters the user typed or pasted at the time of
     submission.
   - ``selType``
     The type of the selected result at the time of submission.
     This is only present for ``engagement`` events.
     It can be one of: ``none``, ``autofill``, ``visit``, ``bookmark``,
     ``history``, ``keyword``, ``search``, ``searchsuggestion``, ``switchtab``,
     ``remotetab``, ``extension``, ``oneoff``, ``keywordoffer``, ``canonized``,
-    ``tip``, ``tiphelp``
+    ``tip``, ``tiphelp``, ``formhistory``
   - ``selIndex``
     Index of the selected result in the urlbar panel, or -1 for no selection.
     There won't be a selection when a one-off button is the only selection, and
     for the ``paste_go`` or ``drop_go`` objects. There may also not be a
     selection if the system was busy and results arrived too late, then we
     directly decide whether to search or visit the given string without having
     a fully built result.
     This is only present for ``engagement`` events.
@@ -264,26 +298,27 @@ browser.engagement.navigation.*
     - ``contextmenu``
     - ``webextension``
     - ``system``
 
   Recorded actions may be:
 
     - ``search``
       Used for any search from ``contextmenu``, ``system`` and ``webextension``.
+    - ``search_alias``
+      For ``urlbar``, indicates the user confirmed a search through an alias.
     - ``search_enter``
       For ``about_home`` and ``about:newtab`` this counts any search.
       For the other SAPs it tracks typing and then pressing Enter.
+    - ``search_formhistory``
+      For ``urlbar``, indicates the user picked a form history result.
     - ``search_oneoff``
       For ``urlbar`` or ``searchbar``, indicates the user confirmed a search
       using a one-off button.
     - ``search_suggestion``
       For ``urlbar`` or ``searchbar``, indicates the user confirmed a search
       suggestion.
-    - ``search_alias``
-      For ``urlbar``, indicates the user confirmed a search through an alias.
-
 
 navigation.search
   This is a legacy and disabled event telemetry that is currently under
   discussion for removal or modernization. It can't be enabled through a pref.
   it's more or less equivalent to browser.engagement.navigation, but can also
   report the picked search engine.
--- a/browser/components/urlbar/tests/UrlbarTestUtils.jsm
+++ b/browser/components/urlbar/tests/UrlbarTestUtils.jsm
@@ -5,20 +5,26 @@
 
 const EXPORTED_SYMBOLS = ["UrlbarTestUtils"];
 
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   BrowserTestUtils: "resource://testing-common/BrowserTestUtils.jsm",
   BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+  FormHistory: "resource://gre/modules/FormHistory.jsm",
+  PlacesSearchAutocompleteProvider:
+    "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm",
+  TestUtils: "resource://testing-common/TestUtils.jsm",
   UrlbarController: "resource:///modules/UrlbarController.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
   UrlbarProvider: "resource:///modules/UrlbarUtils.jsm",
   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
 });
 
 var UrlbarTestUtils = {
   /**
@@ -365,16 +371,193 @@ var UrlbarTestUtils = {
               },
             },
           },
         },
         options
       )
     );
   },
+
+  /**
+   * Initializes some external components used by the urlbar.  This is necessary
+   * in xpcshell tests but not in browser tests.
+   */
+  async initXPCShellDependencies() {
+    // The FormHistoryStartup component must be initialized since urlbar uses
+    // form history.
+    Cc["@mozilla.org/satchel/form-history-startup;1"]
+      .getService(Ci.nsIObserver)
+      .observe(null, "profile-after-change", null);
+
+    // These two calls are necessary because UrlbarMuxerUnifiedComplete.sort
+    // calls PlacesSearchAutocompleteProvider.parseSubmissionURL, so we need
+    // engines and PlacesSearchAutocompleteProvider.
+    try {
+      await AddonTestUtils.promiseStartupManager();
+    } catch (error) {
+      if (!error.message.includes("already started")) {
+        throw error;
+      }
+    }
+    await PlacesSearchAutocompleteProvider.ensureReady();
+  },
+};
+
+UrlbarTestUtils.formHistory = {
+  /**
+   * Performs an operation on the urlbar's form history.
+   *
+   * @param {object} updateObject
+   *   An object describing the form history operation.  See FormHistory.jsm.
+   * @param {object} window
+   *   The window containing the urlbar.
+   */
+  async update(
+    updateObject = {},
+    window = BrowserWindowTracker.getTopWindow()
+  ) {
+    await new Promise((resolve, reject) => {
+      FormHistory.update(
+        Object.assign(
+          {
+            fieldname: this.getFormHistoryName(window),
+          },
+          updateObject
+        ),
+        {
+          handleError(error) {
+            reject(error);
+          },
+          handleCompletion(errored) {
+            if (!errored) {
+              resolve();
+            }
+          },
+        }
+      );
+    });
+  },
+
+  /**
+   * Adds values to the urlbar's form history.
+   *
+   * @param {array} values
+   *   The form history string values to remove.
+   * @param {object} window
+   *   The window containing the urlbar.
+   */
+  async add(values = [], window = BrowserWindowTracker.getTopWindow()) {
+    for (let value of values) {
+      await this.update(
+        {
+          value,
+          op: "bump",
+        },
+        window
+      );
+    }
+  },
+
+  /**
+   * Removes values from the urlbar's form history.  If you want to remove all
+   * history, use clearFormHistory.
+   *
+   * @param {array} values
+   *   The form history string values to remove.
+   * @param {object} window
+   *   The window containing the urlbar.
+   */
+  async remove(values = [], window = BrowserWindowTracker.getTopWindow()) {
+    for (let value of values) {
+      await this.update(
+        {
+          value,
+          op: "remove",
+        },
+        window
+      );
+    }
+  },
+
+  /**
+   * Removes all values from the urlbar's form history.  If you want to remove
+   * individual values, use removeFormHistory.
+   *
+   * @param {object} window
+   *   The window containing the urlbar.
+   */
+  async clear(window = BrowserWindowTracker.getTopWindow()) {
+    await this.update({ op: "remove" }, window);
+  },
+
+  /**
+   * Searches the urlbar's form history.
+   *
+   * @param {object} criteria
+   *   Criteria to narrow the search.  See FormHistory.search.
+   * @param {object} window
+   *   The window containing the urlbar.
+   * @returns {Promise}
+   *   A promise resolved with an array of found form history entries.
+   */
+  search(criteria = {}, window = BrowserWindowTracker.getTopWindow()) {
+    return new Promise((resolve, reject) => {
+      let results = [];
+      FormHistory.search(
+        null,
+        Object.assign(
+          {
+            fieldname: this.getFormHistoryName(window),
+          },
+          criteria
+        ),
+        {
+          handleResult(result) {
+            results.push(result);
+          },
+          handleError(error) {
+            reject(error);
+          },
+          handleCompletion(errored) {
+            if (!errored) {
+              resolve(results);
+            }
+          },
+        }
+      );
+    });
+  },
+
+  /**
+   * Returns a promise that's resolved on the next form history change.
+   *
+   * @param {string} change
+   *   Null to listen for any change, or one of: add, remove, update
+   * @returns {Promise}
+   *   Resolved on the next specified form history change.
+   */
+  promiseChanged(change = null) {
+    return TestUtils.topicObserved(
+      "satchel-storage-changed",
+      (subject, data) => !change || data == "formhistory-" + change
+    );
+  },
+
+  /**
+   * Returns the form history name for the urlbar in a window.
+   *
+   * @param {object} window
+   *   The window.
+   * @returns {string}
+   *   The form history name of the urlbar in the window.
+   */
+  getFormHistoryName(window = BrowserWindowTracker.getTopWindow()) {
+    return window ? window.gURLBar.formHistoryName : "searchbar-history";
+  },
 };
 
 /**
  * A test provider.  If you need a test provider whose behavior is different
  * from this, then consider modifying the implementation below if you think the
  * new behavior would be useful for other tests.  Otherwise, you can create a
  * new TestProvider instance and then override its methods.
  */
--- a/browser/components/urlbar/tests/browser/browser_action_searchengine.js
+++ b/browser/components/urlbar/tests/browser/browser_action_searchengine.js
@@ -28,16 +28,17 @@ add_task(async function setup() {
   let originalEngine = await Services.search.getDefault();
   await Services.search.setDefault(engine);
 
   registerCleanupFunction(async function() {
     await Services.search.setDefault(originalEngine);
     await Services.search.removeEngine(engine);
     await Services.search.removeEngine(engine2);
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
   });
 });
 
 async function testSearch(win, expectedName, expectedBaseUrl) {
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window: win,
     waitForFocus: SimpleTest.waitForFocus,
     value: "open a search",
--- a/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js
+++ b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js
@@ -34,16 +34,17 @@ add_task(async function() {
     await Services.search.setDefault(originalEngine);
     await Services.search.removeEngine(engine);
     try {
       BrowserTestUtils.removeTab(tab);
     } catch (ex) {
       /* tab may have already been closed in case of failure */
     }
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
   });
 
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
     waitForFocus: SimpleTest.waitForFocus,
     value: "moz",
   });
   Assert.equal(
--- a/browser/components/urlbar/tests/browser/browser_canonizeURL.js
+++ b/browser/components/urlbar/tests/browser/browser_canonizeURL.js
@@ -3,16 +3,21 @@
 
 /**
  * Tests turning non-url-looking values typed in the input field into proper URLs.
  */
 
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
 add_task(async function checkCtrlWorks() {
+  registerCleanupFunction(async function() {
+    await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
+  });
+
   let defaultEngine = await Services.search.getDefault();
   let testcases = [
     ["example", "http://www.example.com/", { ctrlKey: true }],
     // Check that a direct load is not overwritten by a previous canonization.
     ["http://example.com/test/", "http://example.com/test/", {}],
     ["ex-ample", "http://www.ex-ample.com/", { ctrlKey: true }],
     ["  example ", "http://www.example.com/", { ctrlKey: true }],
     [" example/foo ", "http://www.example.com/foo", { ctrlKey: true }],
--- a/browser/components/urlbar/tests/browser/browser_decode.js
+++ b/browser/components/urlbar/tests/browser/browser_decode.js
@@ -79,16 +79,17 @@ add_task(async function actionURILossles
 
   gURLBar.value = "";
   gURLBar.handleRevert();
   gURLBar.blur();
 });
 
 add_task(async function test_resultsDisplayDecoded() {
   await PlacesUtils.history.clear();
+  await UrlbarTestUtils.formHistory.clear();
 
   await PlacesTestUtils.addVisits("http://example.com/%E9%A1%B5");
 
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
     waitForFocus: SimpleTest.waitForFocus,
     value: "example",
   });
--- a/browser/components/urlbar/tests/browser/browser_oneOffs.js
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js
@@ -13,19 +13,21 @@ add_task(async function init() {
   // as the first one-off.
   let engine = await SearchTestUtils.promiseNewSearchEngine(
     getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
   );
   await Services.search.moveEngine(engine, 0);
 
   registerCleanupFunction(async function() {
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
   });
 
   await PlacesUtils.history.clear();
+  await UrlbarTestUtils.formHistory.clear();
 
   let visits = [];
   for (let i = 0; i < gMaxResults; i++) {
     visits.push({
       uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i),
       // TYPED so that the visit shows up when the urlbar's drop-down arrow is
       // pressed.
       transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
@@ -223,16 +225,17 @@ add_task(async function oneOffClick() {
     gBrowser.selectedBrowser,
     false,
     "http://mochi.test:8888/?terms=foo.bar"
   );
   EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
   await resultsPromise;
 
   gBrowser.removeTab(gBrowser.selectedTab);
+  await UrlbarTestUtils.formHistory.clear();
 });
 
 // Presses the Return key when a one-off is selected.
 add_task(async function oneOffReturn() {
   gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
 
   // We are explicitly using something that looks like a url, to make the test
   // stricter. Even if it looks like a url, we should search.
@@ -254,16 +257,17 @@ add_task(async function oneOffReturn() {
     gBrowser.selectedBrowser,
     false,
     "http://mochi.test:8888/?terms=foo.bar"
   );
   EventUtils.synthesizeKey("KEY_Enter");
   await resultsPromise;
 
   gBrowser.removeTab(gBrowser.selectedTab);
+  await UrlbarTestUtils.formHistory.clear();
 });
 
 add_task(async function hiddenOneOffs() {
   // Disable all the engines but the current one, check the oneoffs are
   // hidden and that moving up selects the last match.
   let defaultEngine = await Services.search.getDefault();
   let engines = (await Services.search.getVisibleEngines()).filter(
     e => e.name != defaultEngine.name
--- a/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js
@@ -33,20 +33,22 @@ add_task(async function setup() {
   originalEngine = await Services.search.getDefault();
   newEngine = await SearchTestUtils.promiseNewSearchEngine(
     getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
   );
   await Services.search.moveEngine(newEngine, 0);
 
   registerCleanupFunction(async function() {
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
     await Services.search.setDefault(originalEngine);
   });
 
   await PlacesUtils.history.clear();
+  await UrlbarTestUtils.formHistory.clear();
 
   let visits = [];
   for (let i = 0; i < gMaxResults; i++) {
     visits.push({
       uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i),
       // TYPED so that the visit shows up when the urlbar's drop-down arrow is
       // pressed.
       transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
@@ -88,16 +90,17 @@ async function searchInTab(checkFn) {
     );
     EventUtils.synthesizeMouseAtCenter(openInTab, {});
 
     let newTab = await tabOpenAndLoaded;
 
     checkFn(testBrowser, newTab);
 
     BrowserTestUtils.removeTab(newTab);
+    await UrlbarTestUtils.formHistory.clear();
   });
 }
 
 add_task(async function searchInNewTab_opensBackground() {
   Services.prefs.setBoolPref("browser.tabs.loadInBackground", true);
   await searchInTab((testBrowser, newTab) => {
     Assert.equal(
       newTab.linkedBrowser.currentURI.spec,
--- a/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js
@@ -13,72 +13,116 @@ const TEST_ENGINE2_BASENAME = "searchSug
 const serverInfo = {
   scheme: "http",
   host: "localhost",
   port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml
 };
 
 add_task(async function init() {
   await PlacesUtils.history.clear();
+  await UrlbarTestUtils.formHistory.clear();
   await SpecialPowers.pushPrefEnv({
-    set: [["browser.urlbar.suggest.searches", true]],
+    set: [
+      ["browser.urlbar.suggest.searches", true],
+      ["browser.urlbar.maxHistoricalSearchSuggestions", 2],
+    ],
   });
   let engine = await SearchTestUtils.promiseNewSearchEngine(
     getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
   );
   let engine2 = await SearchTestUtils.promiseNewSearchEngine(
     getRootDirectory(gTestPath) + TEST_ENGINE2_BASENAME
   );
   let oldDefaultEngine = await Services.search.getDefault();
   await Services.search.moveEngine(engine2, 0);
   await Services.search.moveEngine(engine, 0);
   await Services.search.setDefault(engine);
   registerCleanupFunction(async function() {
     await Services.search.setDefault(oldDefaultEngine);
 
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
   });
 });
 
-async function withSecondSuggestion(testFn) {
+async function withSuggestions(testFn) {
+  // First run with remote suggestions, and then run with form history.
+  await withSuggestionOnce(false, testFn);
+  await withSuggestionOnce(true, testFn);
+}
+
+async function withSuggestionOnce(useFormHistory, testFn) {
+  if (useFormHistory) {
+    // Add foofoo twice so it's more frecent so it appears first so that the
+    // order of form history results matches the order of remote suggestion
+    // results.
+    await UrlbarTestUtils.formHistory.add(["foofoo", "foofoo", "foobar"]);
+  }
   await BrowserTestUtils.withNewTab(gBrowser, async () => {
-    let typedValue = "foo";
+    let value = "foo";
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
       window,
+      value,
+      fireInputEvent: true,
       waitForFocus: SimpleTest.waitForFocus,
-      value: typedValue,
-      fireInputEvent: true,
     });
     let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
-    assertState(0, -1, typedValue);
-
-    // Down to select the first search suggestion.
-    for (let i = index; i > 0; --i) {
-      EventUtils.synthesizeKey("KEY_ArrowDown");
-    }
-    assertState(index, -1, "foofoo");
-
-    // Down to select the next search suggestion.
-    EventUtils.synthesizeKey("KEY_ArrowDown");
-    assertState(index + 1, -1, "foobar");
-
+    await assertState({
+      inputValue: value,
+      resultIndex: 0,
+    });
     await withHttpServer(serverInfo, () => {
-      return testFn(index + 1);
+      return testFn(index, useFormHistory);
     });
   });
   await PlacesUtils.history.clear();
+  await UrlbarTestUtils.formHistory.clear();
+}
+
+async function selectSecondSuggestion(index, isFormHistory) {
+  // Down to select the first search suggestion.
+  for (let i = index; i > 0; --i) {
+    EventUtils.synthesizeKey("KEY_ArrowDown");
+  }
+  await assertState({
+    inputValue: "foofoo",
+    resultIndex: index,
+    suggestion: {
+      isFormHistory,
+    },
+  });
+
+  // Down to select the next search suggestion.
+  EventUtils.synthesizeKey("KEY_ArrowDown");
+  await assertState({
+    inputValue: "foobar",
+    resultIndex: index + 1,
+    suggestion: {
+      isFormHistory,
+    },
+  });
 }
 
 // Presses the Return key when a one-off is selected after selecting a search
 // suggestion.
 add_task(async function test_returnAfterSuggestion() {
-  await withSecondSuggestion(async index => {
+  await withSuggestions(async (index, usingFormHistory) => {
+    await selectSecondSuggestion(index, usingFormHistory);
+
     // Alt+Down to select the first one-off.
     EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
-    assertState(index, 0, "foobar");
+    await assertState({
+      inputValue: "foobar",
+      resultIndex: index + 1,
+      oneOffIndex: 0,
+      suggestion: {
+        isFormHistory: usingFormHistory,
+      },
+    });
+
     let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
     Assert.ok(
       !BrowserTestUtils.is_visible(heuristicResult.element.action),
       "The heuristic action should not be visible"
     );
 
     let resultsPromise = BrowserTestUtils.browserLoaded(
       gBrowser.selectedBrowser,
@@ -88,103 +132,105 @@ add_task(async function test_returnAfter
     EventUtils.synthesizeKey("KEY_Enter");
     await resultsPromise;
   });
 });
 
 // Presses the Return key when a non-default one-off is selected after selecting
 // a search suggestion.
 add_task(async function test_returnAfterSuggestion_nonDefault() {
-  await withSecondSuggestion(async index => {
+  await withSuggestions(async (index, usingFormHistory) => {
+    await selectSecondSuggestion(index, usingFormHistory);
+
     // Alt+Down twice to select the second one-off.
     EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
     EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
-    assertState(index, 1, "foobar");
+    await assertState({
+      inputValue: "foobar",
+      resultIndex: index + 1,
+      oneOffIndex: 1,
+      suggestion: {
+        isFormHistory: usingFormHistory,
+      },
+    });
 
     let resultsPromise = BrowserTestUtils.browserLoaded(
       gBrowser.selectedBrowser,
       false,
       `http://localhost:20709/?terms=foobar`
     );
     EventUtils.synthesizeKey("KEY_Enter");
     await resultsPromise;
   });
 });
 
 // Clicks a one-off engine after selecting a search suggestion.
 add_task(async function test_clickAfterSuggestion() {
-  await withSecondSuggestion(async () => {
+  await withSuggestions(async (index, usingFormHistory) => {
+    await selectSecondSuggestion(index, usingFormHistory);
+
     let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(
       window
     ).getSelectableButtons(true);
     let resultsPromise = BrowserTestUtils.browserLoaded(
       gBrowser.selectedBrowser,
       false,
       `http://mochi.test:8888/?terms=foobar`
     );
     EventUtils.synthesizeMouseAtCenter(oneOffs[0], {});
     await resultsPromise;
   });
 });
 
 // Clicks a non-default one-off engine after selecting a search suggestion.
 add_task(async function test_clickAfterSuggestion_nonDefault() {
-  await withSecondSuggestion(async () => {
+  await withSuggestions(async (index, usingFormHistory) => {
+    await selectSecondSuggestion(index, usingFormHistory);
+
     let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(
       window
     ).getSelectableButtons(true);
     let resultsPromise = BrowserTestUtils.browserLoaded(
       gBrowser.selectedBrowser,
       false,
       `http://localhost:20709/?terms=foobar`
     );
     EventUtils.synthesizeMouseAtCenter(oneOffs[1], {});
     await resultsPromise;
   });
 });
 
-// Selects a non-default one-off engine and then selects a search suggestion.
+// Selects a non-default one-off engine and then clicks a search suggestion.
 add_task(async function test_selectOneOffThenSuggestion() {
-  await BrowserTestUtils.withNewTab(gBrowser, async () => {
-    let typedValue = "foo";
-    await UrlbarTestUtils.promiseAutocompleteResultPopup({
-      window,
-      waitForFocus: SimpleTest.waitForFocus,
-      value: typedValue,
-      fireInputEvent: true,
-    });
-    let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
-    assertState(0, -1, typedValue);
-
+  await withSuggestions(async (index, usingFormHistory) => {
     // Select a non-default one-off engine.
     EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
     EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
-    assertState(0, 1, "foo");
+    await assertState({
+      inputValue: "foo",
+      resultIndex: 0,
+      oneOffIndex: 1,
+    });
 
     let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
     Assert.ok(
       BrowserTestUtils.is_visible(heuristicResult.element.action),
       "The heuristic action should be visible because the result is selected"
     );
 
     // Now click the second suggestion.
-    await withHttpServer(serverInfo, async () => {
-      let result = await UrlbarTestUtils.getDetailsOfResultAt(
-        window,
-        index + 1
-      );
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index + 1);
 
-      let resultsPromise = BrowserTestUtils.browserLoaded(
-        gBrowser.selectedBrowser,
-        false,
-        `http://localhost:20709/?terms=foobar`
-      );
-      EventUtils.synthesizeMouseAtCenter(result.element.row, {});
-      await resultsPromise;
-    });
+    let resultsPromise = BrowserTestUtils.browserLoaded(
+      gBrowser.selectedBrowser,
+      false,
+      `http://localhost:20709/?terms=foobar`
+    );
+    EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+    await resultsPromise;
   });
 });
 
 add_task(async function overridden_engine_not_reused() {
   info(
     "An overridden search suggestion item should not be reused by a search with another engine"
   );
   await BrowserTestUtils.withNewTab(gBrowser, async () => {
@@ -195,50 +241,107 @@ add_task(async function overridden_engin
       value: typedValue,
       fireInputEvent: true,
     });
     let index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
     // Down to select the first search suggestion.
     for (let i = index; i > 0; --i) {
       EventUtils.synthesizeKey("KEY_ArrowDown");
     }
-    assertState(index, -1, "foofoo");
+    await assertState({
+      inputValue: "foofoo",
+      resultIndex: index,
+      suggestion: {
+        isFormHistory: false,
+      },
+    });
+
     // ALT+Down to select the second search engine.
     EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
     EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
-    assertState(index, 1, "foofoo");
+    await assertState({
+      inputValue: "foofoo",
+      resultIndex: index,
+      oneOffIndex: 1,
+      suggestion: {
+        isFormHistory: false,
+      },
+    });
 
     let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
     let label = result.displayed.action;
     // Run again the query, check the label has been replaced.
     await UrlbarTestUtils.promisePopupClose(window);
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
       window,
       waitForFocus: SimpleTest.waitForFocus,
       value: typedValue,
       fireInputEvent: true,
     });
     index = await UrlbarTestUtils.promiseSuggestionsPresent(window);
-    assertState(0, -1, "foo");
+    await assertState({
+      inputValue: "foo",
+      resultIndex: 0,
+    });
     result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
     Assert.notEqual(
       result.displayed.action,
       label,
       "The label should have been updated"
     );
   });
 });
 
-function assertState(expectedIndex, oneOffIndex, textValue = undefined) {
+async function assertState({
+  resultIndex,
+  inputValue,
+  oneOffIndex = -1,
+  suggestion = null,
+}) {
   Assert.equal(
     UrlbarTestUtils.getSelectedRowIndex(window),
-    expectedIndex,
+    resultIndex,
     "Expected result should be selected"
   );
   Assert.equal(
     UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex,
     oneOffIndex,
     "Expected one-off should be selected"
   );
-  if (textValue !== undefined) {
-    Assert.equal(gURLBar.value, textValue, "Expected textValue");
+  if (inputValue !== undefined) {
+    Assert.equal(gURLBar.value, inputValue, "Expected input value");
+  }
+
+  if (suggestion) {
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(
+      window,
+      resultIndex
+    );
+    Assert.equal(
+      result.type,
+      UrlbarUtils.RESULT_TYPE.SEARCH,
+      "Result type should be SEARCH"
+    );
+    if (suggestion.isFormHistory) {
+      Assert.equal(
+        result.source,
+        UrlbarUtils.RESULT_SOURCE.HISTORY,
+        "Result source should be HISTORY"
+      );
+    } else {
+      Assert.equal(
+        result.source,
+        UrlbarUtils.RESULT_SOURCE.SEARCH,
+        "Result source should be SEARCH"
+      );
+    }
+    Assert.equal(
+      typeof result.searchParams.suggestion,
+      "string",
+      "Result should have a suggestion"
+    );
+    Assert.equal(
+      result.searchParams.suggestion,
+      suggestion.value || inputValue,
+      "Result should have the expected suggestion"
+    );
   }
 }
--- a/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js
@@ -10,19 +10,21 @@
 
 let gMaxResults;
 
 add_task(async function init() {
   gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
 
   registerCleanupFunction(async function() {
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
   });
 
   await PlacesUtils.history.clear();
+  await UrlbarTestUtils.formHistory.clear();
 
   let visits = [];
   for (let i = 0; i < gMaxResults; i++) {
     visits.push({
       uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i),
       // TYPED so that the visit shows up when the urlbar's drop-down arrow is
       // pressed.
       transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
--- a/browser/components/urlbar/tests/browser/browser_remove_match.js
+++ b/browser/components/urlbar/tests/browser/browser_remove_match.js
@@ -1,11 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+XPCOMUtils.defineLazyModuleGetters(this, {
+  FormHistory: "resource://gre/modules/FormHistory.jsm",
+});
+
 add_task(async function test_remove_history() {
   const TEST_URL = "http://remove.me/from_urlbar/";
   await PlacesTestUtils.addVisits(TEST_URL);
 
   registerCleanupFunction(async function() {
     await PlacesUtils.history.clear();
   });
 
@@ -42,16 +46,91 @@ add_task(async function test_remove_hist
       TEST_URL,
       "Should not find the test URL in the remaining results"
     );
   }
 
   await UrlbarTestUtils.promisePopupClose(window);
 });
 
+add_task(async function test_remove_form_history() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+  });
+
+  let formHistoryValue = "foobar";
+  await UrlbarTestUtils.formHistory.add([formHistoryValue]);
+
+  let formHistory = (
+    await UrlbarTestUtils.formHistory.search({
+      value: formHistoryValue,
+    })
+  ).map(entry => entry.value);
+  Assert.deepEqual(
+    formHistory,
+    [formHistoryValue],
+    "Should find form history after adding it"
+  );
+
+  let promiseRemoved = UrlbarTestUtils.formHistory.promiseChanged("remove");
+
+  await UrlbarTestUtils.promiseAutocompleteResultPopup({
+    window,
+    waitForFocus: SimpleTest.waitForFocus,
+    value: "foo",
+  });
+
+  let index = 1;
+  let count = UrlbarTestUtils.getResultCount(window);
+  for (; index < count; index++) {
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+    if (
+      result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+      result.source == UrlbarUtils.RESULT_SOURCE.HISTORY
+    ) {
+      break;
+    }
+  }
+  Assert.ok(index < count, "Result found");
+
+  EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index });
+  Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), index);
+  EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+  await promiseRemoved;
+
+  await TestUtils.waitForCondition(
+    () => UrlbarTestUtils.getResultCount(window) == count - 1,
+    "Waiting for the result to disappear"
+  );
+
+  for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+    Assert.ok(
+      result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
+        result.source != UrlbarUtils.RESULT_SOURCE.HISTORY,
+      "Should not find the form history result in the remaining results"
+    );
+  }
+
+  await UrlbarTestUtils.promisePopupClose(window);
+
+  formHistory = (
+    await UrlbarTestUtils.formHistory.search({
+      value: formHistoryValue,
+    })
+  ).map(entry => entry.value);
+  Assert.deepEqual(
+    formHistory,
+    [],
+    "Should not find form history after removing it"
+  );
+
+  await SpecialPowers.popPrefEnv();
+});
+
 // We shouldn't be able to remove a bookmark item.
 add_task(async function test_remove_bookmark_doesnt() {
   const TEST_URL = "http://dont.remove.me/from_urlbar/";
   await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
     title: "test",
     url: TEST_URL,
   });
--- a/browser/components/urlbar/tests/browser/browser_searchSuggestions.js
+++ b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js
@@ -17,23 +17,25 @@ const MAX_CHARS_PREF = "browser.urlbar.m
 add_task(async function prepare() {
   let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
   let engine = await SearchTestUtils.promiseNewSearchEngine(
     getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
   );
   let oldDefaultEngine = await Services.search.getDefault();
   await Services.search.setDefault(engine);
+  await UrlbarTestUtils.formHistory.clear();
   registerCleanupFunction(async function() {
     Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
     await Services.search.setDefault(oldDefaultEngine);
 
     // Clicking suggestions causes visits to search results pages, so clear that
     // history now.
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
   });
 });
 
 add_task(async function clickSuggestion() {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
   gURLBar.focus();
   await UrlbarTestUtils.promiseAutocompleteResultPopup({
     window,
@@ -51,17 +53,28 @@ add_task(async function clickSuggestion(
   let loadPromise = BrowserTestUtils.browserLoaded(
     gBrowser.selectedBrowser,
     false,
     uri.spec
   );
   let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, idx);
   EventUtils.synthesizeMouseAtCenter(element, {}, window);
   await loadPromise;
+
+  let formHistory = (await UrlbarTestUtils.formHistory.search()).map(
+    entry => entry.value
+  );
+  Assert.deepEqual(
+    formHistory,
+    ["foofoo"],
+    "Should find form history after adding it"
+  );
+
   BrowserTestUtils.removeTab(tab);
+  await UrlbarTestUtils.formHistory.clear();
 });
 
 async function testPressEnterOnSuggestion(
   expectedUrl = null,
   keyModifiers = {}
 ) {
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
   gURLBar.focus();
@@ -72,33 +85,53 @@ async function testPressEnterOnSuggestio
   });
   let [idx, suggestion, engineName] = await getFirstSuggestion();
   Assert.equal(
     engineName,
     "browser_searchSuggestionEngine searchSuggestionEngine.xml",
     "Expected suggestion engine"
   );
 
+  let hasExpectedUrl = !!expectedUrl;
   if (!expectedUrl) {
     expectedUrl = (await Services.search.getDefault()).getSubmission(suggestion)
       .uri.spec;
   }
 
   let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt(
     expectedUrl,
     gBrowser.selectedBrowser
   );
 
+  let promiseFormHistory;
+  if (!hasExpectedUrl) {
+    promiseFormHistory = UrlbarTestUtils.formHistory.promiseChanged("add");
+  }
+
   for (let i = 0; i < idx; ++i) {
     EventUtils.synthesizeKey("KEY_ArrowDown");
   }
   EventUtils.synthesizeKey("KEY_Enter", keyModifiers);
 
   await promiseLoad;
+
+  if (!hasExpectedUrl) {
+    await promiseFormHistory;
+    let formHistory = (await UrlbarTestUtils.formHistory.search()).map(
+      entry => entry.value
+    );
+    Assert.deepEqual(
+      formHistory,
+      ["foofoo"],
+      "Should find form history after adding it"
+    );
+  }
+
   BrowserTestUtils.removeTab(tab);
+  await UrlbarTestUtils.formHistory.clear();
 }
 
 add_task(async function plainEnterOnSuggestion() {
   await testPressEnterOnSuggestion();
 });
 
 add_task(async function ctrlEnterOnSuggestion() {
   await testPressEnterOnSuggestion("http://www.foofoo.com/", { ctrlKey: true });
@@ -218,16 +251,62 @@ add_task(async function pasteMoreThanMax
   // Paste again.  The string is longer than maxChars, so suggestions should not
   // be fetched.
   await selectAndPaste(value);
   await assertSuggestions([]);
 
   await SpecialPowers.popPrefEnv();
 });
 
+add_task(async function heuristicAddsFormHistory() {
+  await UrlbarTestUtils.formHistory.clear();
+  let formHistory = (await UrlbarTestUtils.formHistory.search()).map(
+    entry => entry.value
+  );
+  Assert.deepEqual(formHistory, [], "Form history should be empty initially");
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+  gURLBar.focus();
+  await UrlbarTestUtils.promiseAutocompleteResultPopup({
+    window,
+    waitForFocus: SimpleTest.waitForFocus,
+    value: "foo",
+  });
+
+  let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+  Assert.ok(result.heuristic);
+  Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+  Assert.equal(result.searchParams.query, "foo");
+
+  let uri = (await Services.search.getDefault()).getSubmission("foo").uri;
+  let loadPromise = BrowserTestUtils.browserLoaded(
+    gBrowser.selectedBrowser,
+    false,
+    uri.spec
+  );
+  let formHistoryPromise = UrlbarTestUtils.formHistory.promiseChanged("add");
+  let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+  EventUtils.synthesizeMouseAtCenter(element, {}, window);
+  await loadPromise;
+
+  await formHistoryPromise;
+  formHistory = (await UrlbarTestUtils.formHistory.search()).map(
+    entry => entry.value
+  );
+  Assert.deepEqual(
+    formHistory,
+    ["foo"],
+    "Should find form history after adding it"
+  );
+
+  BrowserTestUtils.removeTab(tab);
+  await UrlbarTestUtils.formHistory.clear();
+});
+
 async function getFirstSuggestion() {
   let results = await getSuggestionResults();
   if (!results.length) {
     return [-1, null, null];
   }
   let result = results[0];
   return [
     result.index,
--- a/browser/components/urlbar/tests/browser/browser_searchTelemetry.js
+++ b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js
@@ -1,30 +1,36 @@
 "use strict";
 
 const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
 // Must run first.
 add_task(async function prepare() {
-  let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
-  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [SUGGEST_URLBAR_PREF, true],
+      [MAX_FORM_HISTORY_PREF, 2],
+    ],
+  });
+
   let engine = await SearchTestUtils.promiseNewSearchEngine(
     getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME
   );
   let oldDefaultEngine = await Services.search.getDefault();
   await Services.search.setDefault(engine);
 
   registerCleanupFunction(async function() {
-    Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
     await Services.search.setDefault(oldDefaultEngine);
 
     // Clicking urlbar results causes visits to their associated pages, so clear
     // that history now.
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear();
   });
 
   // Move the mouse away from the urlbar one-offs so that a one-off engine is
   // not inadvertently selected.
   await new Promise(resolve => {
     EventUtils.synthesizeNativeMouseMove(
       window.document.documentElement,
       0,
@@ -49,16 +55,17 @@ add_task(async function heuristicResultM
       UrlbarUtils.RESULT_TYPE.SEARCH,
       "Should be of type search"
     );
     let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
     let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
     EventUtils.synthesizeMouseAtCenter(element, {});
     await loadPromise;
     BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
   });
 });
 
 add_task(async function heuristicResultKeyboard() {
   await compareCounts(async function() {
     let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
     gURLBar.focus();
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
@@ -71,16 +78,17 @@ add_task(async function heuristicResultK
       result.type,
       UrlbarUtils.RESULT_TYPE.SEARCH,
       "Should be of type search"
     );
     let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
     EventUtils.sendKey("return");
     await loadPromise;
     BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
   });
 });
 
 add_task(async function searchSuggestionMouse() {
   await compareCounts(async function() {
     let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
     gURLBar.focus();
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
@@ -93,16 +101,17 @@ add_task(async function searchSuggestion
     let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
     let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
       window,
       idx
     );
     EventUtils.synthesizeMouseAtCenter(element, {});
     await loadPromise;
     BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
   });
 });
 
 add_task(async function searchSuggestionKeyboard() {
   await compareCounts(async function() {
     let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
     gURLBar.focus();
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
@@ -114,16 +123,70 @@ add_task(async function searchSuggestion
     Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
     let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
     while (idx--) {
       EventUtils.sendKey("down");
     }
     EventUtils.sendKey("return");
     await loadPromise;
     BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
+  });
+});
+
+add_task(async function formHistoryMouse() {
+  await compareCounts(async function() {
+    await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+    let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+    gURLBar.focus();
+    await UrlbarTestUtils.promiseAutocompleteResultPopup({
+      window,
+      waitForFocus: SimpleTest.waitForFocus,
+      value: "foo",
+    });
+    let index = await getFirstSuggestionIndex();
+    Assert.greaterOrEqual(index, 0, "there should be a first suggestion");
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+    Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+    Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY);
+    let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+    let element = await UrlbarTestUtils.waitForAutocompleteResultAt(
+      window,
+      index
+    );
+    EventUtils.synthesizeMouseAtCenter(element, {});
+    await loadPromise;
+    BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
+  });
+});
+
+add_task(async function formHistoryKeyboard() {
+  await compareCounts(async function() {
+    await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+    let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+    gURLBar.focus();
+    await UrlbarTestUtils.promiseAutocompleteResultPopup({
+      window,
+      waitForFocus: SimpleTest.waitForFocus,
+      value: "foo",
+    });
+    let index = await getFirstSuggestionIndex();
+    Assert.greaterOrEqual(index, 0, "there should be a first suggestion");
+    let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index);
+    Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+    Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY);
+    let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+    while (index--) {
+      EventUtils.sendKey("down");
+    }
+    EventUtils.sendKey("return");
+    await loadPromise;
+    BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
   });
 });
 
 /**
  * This does three things: gets current telemetry/FHR counts, calls
  * clickCallback, gets telemetry/FHR counts again to compare them to the old
  * counts.
  *
--- a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js
+++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js
@@ -350,16 +350,78 @@ const tests = [
         numChars: "3",
         selIndex: val => parseInt(val) > 0,
         selType: "bookmark",
       },
     };
   },
 
   async function(win) {
+    info("Type something, select remote search suggestion, Enter.");
+    win.gURLBar.select();
+    let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+    await UrlbarTestUtils.promiseAutocompleteResultPopup({
+      window: win,
+      waitForFocus: SimpleTest.waitForFocus,
+      value: "foo",
+      fireInputEvent: true,
+    });
+    while (win.gURLBar.untrimmedValue != "foofoo") {
+      EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+    }
+    EventUtils.synthesizeKey("VK_RETURN", {}, win);
+    await promise;
+    return {
+      category: "urlbar",
+      method: "engagement",
+      object: "enter",
+      value: "typed",
+      extra: {
+        elapsed: val => parseInt(val) > 0,
+        numChars: "3",
+        selIndex: val => parseInt(val) > 0,
+        selType: "searchsuggestion",
+      },
+    };
+  },
+
+  async function(win) {
+    info("Type something, select form history, Enter.");
+    await SpecialPowers.pushPrefEnv({
+      set: [["browser.urlbar.maxHistoricalSearchSuggestions", 2]],
+    });
+    await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]);
+    win.gURLBar.select();
+    let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+    await UrlbarTestUtils.promiseAutocompleteResultPopup({
+      window: win,
+      waitForFocus: SimpleTest.waitForFocus,
+      value: "foo",
+      fireInputEvent: true,
+    });
+    while (win.gURLBar.untrimmedValue != "foofoo") {
+      EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+    }
+    EventUtils.synthesizeKey("VK_RETURN", {}, win);
+    await promise;
+    return {
+      category: "urlbar",
+      method: "engagement",
+      object: "enter",
+      value: "typed",
+      extra: {
+        elapsed: val => parseInt(val) > 0,
+        numChars: "3",
+        selIndex: val => parseInt(val) > 0,
+        selType: "formhistory",
+      },
+    };
+  },
+
+  async function(win) {
     info("Type @, Enter on a keywordoffer");
     win.gURLBar.select();
     await UrlbarTestUtils.promiseAutocompleteResultPopup({
       window: win,
       waitForFocus: SimpleTest.waitForFocus,
       value: "@",
       fireInputEvent: true,
     });
@@ -992,30 +1054,32 @@ add_task(async function test() {
   ]);
 
   registerCleanupFunction(async function() {
     await Services.search.setDefault(oldDefaultEngine);
     await Services.search.removeEngine(aliasEngine);
     await PlacesUtils.keywords.remove("kw");
     await PlacesUtils.bookmarks.remove(bm);
     await PlacesUtils.history.clear();
+    await UrlbarTestUtils.formHistory.clear(window);
   });
 
   // This is not necessary after each loop, because assertEvents does it.
   Services.telemetry.clearEvents();
 
   for (let i = 0; i < tests.length; i++) {
     info(`Running test at index ${i}`);
     let events = await tests[i](window);
     if (!Array.isArray(events)) {
       events = [events];
     }
     // Always blur to ensure it's not accounted as an additional abandonment.
     gURLBar.blur();
     TelemetryTestUtils.assertEvents(events, { category: "urlbar" });
+    await UrlbarTestUtils.formHistory.clear(window);
   }
 
   for (let i = 0; i < noEventTests.length; i++) {
     info(`Running no event test at index ${i}`);
     await noEventTests[i](window);
     // Always blur to ensure it's not accounted as an additional abandonment.
     gURLBar.blur();
     TelemetryTestUtils.assertEvents([], { category: "urlbar" });
--- a/browser/components/urlbar/tests/unit/data/engine-suggestions.xml
+++ b/browser/components/urlbar/tests/unit/data/engine-suggestions.xml
@@ -5,10 +5,12 @@
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
 <ShortName>engine-suggestions.xml</ShortName>
 <Url type="application/x-suggestions+json"
      method="GET"
      template="http://localhost:9000/suggest?{searchTerms}"/>
 <Url type="text/html"
      method="GET"
      template="http://localhost:9000/search"
-     rel="searchform"/>
+     rel="searchform">
+  <Param name="terms" value="{searchTerms}"/>
+</Url>
 </SearchPlugin>
--- a/browser/components/urlbar/tests/unit/head.js
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -36,16 +36,20 @@ const { sinon } = ChromeUtils.import("re
 AddonTestUtils.init(this, false);
 AddonTestUtils.createAppInfo(
   "xpcshell@tests.mozilla.org",
   "XPCShell",
   "42",
   "42"
 );
 
+add_task(async function initXPCShellDependencies() {
+  await UrlbarTestUtils.initXPCShellDependencies();
+});
+
 /**
  * @param {string} searchString The search string to insert into the context.
  * @param {object} properties Overrides for the default values.
  * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled
  *          required options.
  */
 function createContext(searchString = "foo", properties = {}) {
   info(`Creating new queryContext with searchString: ${searchString}`);
@@ -350,21 +354,48 @@ function makeSearchResult(
         UrlbarUtils.HIGHLIGHT.TYPED,
       ],
       isSearchHistory: false,
       icon: [engineIconUri ? engineIconUri : ""],
       keywordOffer,
     })
   );
 
+  if (typeof suggestion == "string") {
+    result.payload.lowerCaseSuggestion = result.payload.suggestion.toLocaleLowerCase();
+  }
+
   result.heuristic = heuristic;
   return result;
 }
 
 /**
+ * Creates a UrlbarResult for a form history result.
+ * @param {UrlbarQueryContext} queryContext
+ *   The context that this result will be displayed in.
+ * @param {string} options.suggestion
+ *   The form history suggestion.
+ * @param {string} options.engineName
+ *   The name of the engine that will do the search when the result is picked.
+ * @returns {UrlbarResult}
+ */
+function makeFormHistoryResult(queryContext, { suggestion, engineName }) {
+  UrlbarTokenizer.tokenize(queryContext);
+  return new UrlbarResult(
+    UrlbarUtils.RESULT_TYPE.SEARCH,
+    UrlbarUtils.RESULT_SOURCE.HISTORY,
+    ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+      engine: engineName,
+      suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED],
+      lowerCaseSuggestion: suggestion.toLocaleLowerCase(),
+    })
+  );
+}
+
+/**
  * Creates a UrlbarResult for a history result.
  * @param {UrlbarQueryContext} queryContext
  *   The context that this result will be displayed in.
  * @param {string} options.title
  *   The page title.
  * @param {string} options.uri
  *   The page URI.
  * @param {array} [options.tags]
@@ -372,29 +403,29 @@ function makeSearchResult(
  * @param {string} [options.iconUri]
  *   A URI for the page's icon.
  * @param {boolean} [options.heuristic]
  *   True if this is a heuristic result. Defaults to false.
  * @returns {UrlbarResult}
  */
 function makeVisitResult(
   queryContext,
-  { title, uri, iconUri, tags = [], heuristic = false }
+  { title, uri, iconUri, tags = null, heuristic = false }
 ) {
   UrlbarTokenizer.tokenize(queryContext);
 
   let payload = {
     url: [uri, UrlbarUtils.HIGHLIGHT.TYPED],
     // Check against undefined so consumers can pass in the empty string.
     icon: [typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`],
     title: [title, UrlbarUtils.HIGHLIGHT.TYPED],
   };
 
-  if (!heuristic) {
-    payload.tags = [tags, UrlbarUtils.HIGHLIGHT.TYPED];
+  if (!heuristic || tags) {
+    payload.tags = [tags || [], UrlbarUtils.HIGHLIGHT.TYPED];
   }
 
   let result = new UrlbarResult(
     UrlbarUtils.RESULT_TYPE.URL,
     UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
     ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload)
   );
 
@@ -464,16 +495,17 @@ async function check_results({ context, 
   let controller = UrlbarTestUtils.newMockController({
     input: {
       isPrivate: context.isPrivate,
       window: {
         location: {
           href: AppConstants.BROWSER_CHROME_URL,
         },
       },
+      autofillFirstResult() {},
     },
   });
   await controller.startQuery(context);
 
   Assert.equal(
     context.results.length,
     matches.length,
     "Found the expected number of results."
--- a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js
@@ -2,17 +2,16 @@
  * 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/. */
 
 /**
  * Test for restrictions set through UrlbarQueryContext.sources.
  */
 
 add_task(async function setup() {
-  await AddonTestUtils.promiseStartupManager();
   let engine = await addTestSuggestionsEngine();
   let oldDefaultEngine = await Services.search.getDefault();
   Services.search.setDefault(engine);
   registerCleanupFunction(async () =>
     Services.search.setDefault(oldDefaultEngine)
   );
 });
 
--- a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js
@@ -172,20 +172,16 @@ var testData = [
   ],
 
   // Test using a non-bmKeywordData object, to test the behavior of
   // getShortcutOrURIAndPostData for non-keywords (setupKeywords only adds keywords for
   // bmKeywordData objects)
   [{ keyword: "http://gavinsharp.com" }, new keywordResult(null, null, true)],
 ];
 
-add_task(async function setup() {
-  await AddonTestUtils.promiseStartupManager();
-});
-
 add_task(async function test_getshortcutoruri() {
   await setupKeywords();
 
   for (let item of testData) {
     let [data, result] = item;
 
     let query = data.keyword;
     if (data.searchWord) {
--- a/browser/components/urlbar/tests/unit/test_muxer.js
+++ b/browser/components/urlbar/tests/unit/test_muxer.js
@@ -1,21 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-add_task(async function setup() {
-  // These two lines are necessary because UrlbarMuxerUnifiedComplete.sort calls
-  // PlacesSearchAutocompleteProvider.parseSubmissionURL, so we need engines and
-  // PlacesSearchAutocompleteProvider.
-  await AddonTestUtils.promiseStartupManager();
-  await PlacesSearchAutocompleteProvider.ensureReady();
-});
-
 add_task(async function test_muxer() {
   Assert.throws(
     () => UrlbarProvidersManager.registerMuxer(),
     /invalid muxer/,
     "Should throw with no arguments"
   );
   Assert.throws(
     () => UrlbarProvidersManager.registerMuxer({}),
@@ -177,34 +169,47 @@ add_task(async function test_preselected
     matches2[1],
     ...matches1,
     matches2[0],
     matches2[2],
   ]);
 });
 
 add_task(async function test_suggestions() {
+  Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
+
   let matches = [
     new UrlbarResult(
       UrlbarUtils.RESULT_TYPE.URL,
       UrlbarUtils.RESULT_SOURCE.HISTORY,
       { url: "http://mozilla.org/a" }
     ),
     new UrlbarResult(
       UrlbarUtils.RESULT_TYPE.URL,
       UrlbarUtils.RESULT_SOURCE.HISTORY,
       { url: "http://mozilla.org/b" }
     ),
     new UrlbarResult(
       UrlbarUtils.RESULT_TYPE.SEARCH,
+      UrlbarUtils.RESULT_SOURCE.HISTORY,
+      {
+        engine: "mozSearch",
+        query: "moz",
+        suggestion: "mozzarella",
+        lowerCaseSuggestion: "mozzarella",
+      }
+    ),
+    new UrlbarResult(
+      UrlbarUtils.RESULT_TYPE.SEARCH,
       UrlbarUtils.RESULT_SOURCE.SEARCH,
       {
         engine: "mozSearch",
         query: "moz",
         suggestion: "mozilla",
+        lowerCaseSuggestion: "mozilla",
       }
     ),
     new UrlbarResult(
       UrlbarUtils.RESULT_TYPE.SEARCH,
       UrlbarUtils.RESULT_SOURCE.SEARCH,
       {
         engine: "mozSearch",
         query: "moz",
@@ -221,18 +226,21 @@ add_task(async function test_suggestions
 
   let providerName = registerBasicTestProvider(matches);
 
   let context = createContext(undefined, {
     providers: [providerName],
   });
   let controller = UrlbarTestUtils.newMockController();
 
-  info("Check results, the order should be: moz, a, b, @moz, c");
+  info("Check results, the order should be: mozzarella, moz, a, b, @moz, c");
   await UrlbarProvidersManager.startQuery(context, controller);
   Assert.deepEqual(context.results, [
     matches[2],
+    matches[3],
     matches[0],
     matches[1],
-    matches[3],
     matches[4],
+    matches[5],
   ]);
+
+  Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
 });
--- a/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js
+++ b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js
@@ -5,20 +5,16 @@
 
 // This is a simple test to check the UnifiedComplete provider works, it is not
 // intended to check all the edge cases, because that component is already
 // covered by a good amount of tests.
 
 const SUGGEST_PREF = "browser.urlbar.suggest.searches";
 const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
 
-add_task(async function setup() {
-  await AddonTestUtils.promiseStartupManager();
-});
-
 add_task(async function test_unifiedComplete() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
   let engine = await addTestSuggestionsEngine();
   Services.search.defaultEngine = engine;
   let oldCurrentEngine = Services.search.defaultEngine;
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref(SUGGEST_PREF);
--- a/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js
+++ b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js
@@ -11,30 +11,32 @@ add_task(async function test_duplicates(
     { uri: TEST_URL + "#", title: "Test history" },
   ]);
 
   let controller = UrlbarTestUtils.newMockController();
   let searchString = "^Hist";
   let context = createContext(searchString, { isPrivate: false });
   await controller.startQuery(context);
 
+  // The first result will be a search heuristic, which we don't care about for
+  // this test.
   info(
     "Results:\n" +
       context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n")
   );
   Assert.equal(
     context.results.length,
-    1,
+    2,
     "Found the expected number of matches"
   );
   Assert.equal(
-    context.results[0].type,
+    context.results[1].type,
     UrlbarUtils.RESULT_TYPE.URL,
     "Should have a history  result"
   );
   Assert.equal(
-    context.results[0].payload.url,
+    context.results[1].payload.url,
     TEST_URL + "#",
     "Check result URL"
   );
 
   await PlacesUtils.history.clear();
 });
--- a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -1,21 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-add_task(async function setup() {
-  // These two lines are necessary because UrlbarMuxerUnifiedComplete.sort calls
-  // PlacesSearchAutocompleteProvider.parseSubmissionURL, so we need engines and
-  // PlacesSearchAutocompleteProvider.
-  await AddonTestUtils.promiseStartupManager();
-  await PlacesSearchAutocompleteProvider.ensureReady();
-});
-
 add_task(async function test_filtering_disable_only_source() {
   let match = new UrlbarResult(
     UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
     UrlbarUtils.RESULT_SOURCE.TABS,
     { url: "http://mozilla.org/foo/" }
   );
   let providerName = registerBasicTestProvider([match]);
   let context = createContext(undefined, { providers: [providerName] });
--- a/browser/components/urlbar/tests/unit/test_search_suggestions.js
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions.js
@@ -13,16 +13,18 @@ const { FormHistory } = ChromeUtils.impo
 const ENGINE_NAME = "engine-suggestions.xml";
 // This is fixed to match the port number in engine-suggestions.xml.
 const SERVER_PORT = 9000;
 const SUGGEST_PREF = "browser.urlbar.suggest.searches";
 const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled";
 const PRIVATE_ENABLED_PREF = "browser.search.suggest.enabled.private";
 const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled";
 const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults";
+const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions";
+const SEARCH_STRING = "hello";
 
 var suggestionsFn;
 var previousSuggestionsFn;
 
 /**
  * Set the current suggestion funciton.
  * @param {function} fn
  *   A function that that a search string and returns an array of strings that
@@ -48,16 +50,64 @@ async function cleanup() {
 async function cleanUpSuggestions() {
   await cleanup();
   if (previousSuggestionsFn) {
     suggestionsFn = previousSuggestionsFn;
     previousSuggestionsFn = null;
   }
 }
 
+function makeExpectedFormHistoryResults(context, minCount = 0) {
+  let count = Math.max(
+    minCount,
+    Services.prefs.getIntPref(MAX_FORM_HISTORY_PREF, 0)
+  );
+  let results = [];
+  for (let i = 0; i < count; i++) {
+    results.push(
+      makeFormHistoryResult(context, {
+        suggestion: `${SEARCH_STRING} world Form History ${i}`,
+        engineName: ENGINE_NAME,
+      })
+    );
+  }
+  return results;
+}
+
+function makeExpectedRemoteSuggestionResults(
+  context,
+  { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
+) {
+  return [
+    makeSearchResult(context, {
+      query,
+      engineName: ENGINE_NAME,
+      suggestion: suggestionPrefix + " foo",
+    }),
+    makeSearchResult(context, {
+      query,
+      engineName: ENGINE_NAME,
+      suggestion: suggestionPrefix + " bar",
+    }),
+  ];
+}
+
+function makeExpectedSuggestionResults(
+  context,
+  { suggestionPrefix = SEARCH_STRING, query = undefined } = {}
+) {
+  return [
+    ...makeExpectedFormHistoryResults(context),
+    ...makeExpectedRemoteSuggestionResults(context, {
+      suggestionPrefix,
+      query,
+    }),
+  ];
+}
+
 add_task(async function setup() {
   Services.prefs.setCharPref(
     "browser.urlbar.matchBuckets",
     "general:5,suggestion:Infinity"
   );
 
   let engine = await addTestSuggestionsEngine(searchStr => {
     return suggestionsFn(searchStr);
@@ -71,180 +121,159 @@ add_task(async function setup() {
   let oldDefaultEngine = await Services.search.getDefault();
   registerCleanupFunction(async () => {
     Services.search.setDefault(oldDefaultEngine);
     Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
   });
   Services.search.setDefault(engine);
   Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
 
-  // We must make sure the FormHistoryStartup component is initialized.
-  Cc["@mozilla.org/satchel/form-history-startup;1"]
-    .getService(Ci.nsIObserver)
-    .observe(null, "profile-after-change", null);
-  await updateSearchHistory("bump", "hello Fred!");
-  await updateSearchHistory("bump", "hello Barney!");
+  // Add some form history.
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
+  for (let result of makeExpectedFormHistoryResults(context, 2)) {
+    await updateSearchHistory("bump", result.payload.suggestion);
+  }
 });
 
 add_task(async function disabled_urlbarSuggestions() {
   Services.prefs.setBoolPref(SUGGEST_PREF, false);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
-  let context = createContext("hello", { isPrivate: false });
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context),
     ],
   });
   await cleanUpSuggestions();
 });
 
 add_task(async function disabled_allSuggestions() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false);
-  let context = createContext("hello", { isPrivate: false });
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context),
     ],
   });
   await cleanUpSuggestions();
 });
 
 add_task(async function disabled_privateWindow() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
   Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false);
-  let context = createContext("hello", { isPrivate: true });
+  let context = createContext(SEARCH_STRING, { isPrivate: true });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context),
     ],
   });
   await cleanUpSuggestions();
 });
 
 add_task(async function enabled_by_pref_privateWindow() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
   Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true);
-  const query = "hello";
-  let context = createContext(query, { isPrivate: true });
+  let context = createContext(SEARCH_STRING, { isPrivate: true });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " foo",
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " bar",
-      }),
+      ...makeExpectedSuggestionResults(context),
     ],
   });
   await cleanUpSuggestions();
 
   Services.prefs.clearUserPref(PRIVATE_ENABLED_PREF);
 });
 
 add_task(async function singleWordQuery() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
-  const query = "hello";
-  let context = createContext(query, { isPrivate: false });
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
 
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " foo",
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " bar",
-      }),
+      ...makeExpectedSuggestionResults(context),
     ],
   });
 
   await cleanUpSuggestions();
 });
 
 add_task(async function multiWordQuery() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
-  const query = "hello world";
+  const query = `${SEARCH_STRING} world`;
   let context = createContext(query, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " foo",
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " bar",
-      }),
+      ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
     ],
   });
 
   await cleanUpSuggestions();
 });
 
 add_task(async function suffixMatch() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
 
   setSuggestionsFn(searchStr => {
     let prefixes = ["baz", "quux"];
     return prefixes.map(p => p + " " + searchStr);
   });
 
-  const query = "hello";
-  let context = createContext(query, { isPrivate: false });
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
 
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context),
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
-        suggestion: "baz " + query,
+        suggestion: "baz " + SEARCH_STRING,
       }),
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
-        suggestion: "quux " + query,
+        suggestion: "quux " + SEARCH_STRING,
       }),
     ],
   });
 
   await cleanUpSuggestions();
 });
 
 add_task(async function queryIsNotASubstring() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
 
   setSuggestionsFn(searchStr => {
     return ["aaa", "bbb"];
   });
 
-  const query = "hello";
-  let context = createContext(query, { isPrivate: false });
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
 
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context),
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
         suggestion: "aaa",
       }),
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
         suggestion: "bbb",
       }),
@@ -258,79 +287,68 @@ add_task(async function restrictToken() 
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
 
   // Add a visit and a bookmark.  Actually, make the bookmark visited too so
   // that it's guaranteed, with its higher frecency, to appear above the search
   // suggestions.
   await PlacesTestUtils.addVisits([
     {
-      uri: Services.io.newURI("http://example.com/hello-visit"),
-      title: "hello visit",
+      uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-visit`),
+      title: `${SEARCH_STRING} visit`,
     },
     {
-      uri: Services.io.newURI("http://example.com/hello-bookmark"),
-      title: "hello bookmark",
+      uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
+      title: `${SEARCH_STRING} bookmark`,
     },
   ]);
 
   await PlacesTestUtils.addBookmarkWithDetails({
-    uri: Services.io.newURI("http://example.com/hello-bookmark"),
-    title: "hello bookmark",
+    uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`),
+    title: `${SEARCH_STRING} bookmark`,
   });
 
-  const query = "hello";
-  let context = createContext(query, { isPrivate: false });
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
 
   // Do an unrestricted search to make sure everything appears in it, including
   // the visit and bookmark.
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
       makeBookmarkResult(context, {
-        uri: "http://example.com/hello-bookmark",
-        title: "hello bookmark",
+        uri: `http://example.com/${SEARCH_STRING}-bookmark`,
+        title: `${SEARCH_STRING} bookmark`,
       }),
       makeVisitResult(context, {
-        uri: "http://example.com/hello-visit",
-        title: "hello visit",
+        uri: `http://example.com/${SEARCH_STRING}-visit`,
+        title: `${SEARCH_STRING} visit`,
       }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " foo",
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " bar",
-      }),
+      ...makeExpectedSuggestionResults(context),
     ],
   });
 
   // Now do a restricted search to make sure only suggestions appear.
-  context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} ${query}`, {
-    isPrivate: false,
-  });
+  context = createContext(
+    `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`,
+    {
+      isPrivate: false,
+    }
+  );
   await check_results({
     context,
     matches: [
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
-        query,
+        query: SEARCH_STRING,
         heuristic: true,
       }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        query,
-        suggestion: query + " foo",
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        query,
-        suggestion: query + " bar",
+      ...makeExpectedSuggestionResults(context, {
+        suggestionPrefix: SEARCH_STRING,
+        query: SEARCH_STRING,
       }),
     ],
   });
 
   // Typing the search restriction char shows only the Search Engine entry with
   // no query.
   context = createContext(UrlbarTokenizer.RESTRICT.SEARCH, {
     isPrivate: false,
@@ -398,345 +416,322 @@ add_task(async function restrictToken() 
     ],
   });
 
   await cleanUpSuggestions();
 });
 
 add_task(async function mixup_frecency() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
-  // At most, we should have 12 results in this subtest. We set this to 20 to
+  // At most, we should have 14 results in this subtest. We set this to 20 to
   // make we're not cutting off any results and we are actually getting 12.
   Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, 20);
 
   // Add a visit and a bookmark.  Actually, make the bookmark visited too so
   // that it's guaranteed, with its higher frecency, to appear above the search
   // suggestions.
   await PlacesTestUtils.addVisits([
     {
       uri: Services.io.newURI("http://example.com/lo0"),
-      title: "low frecency 0",
+      title: `${SEARCH_STRING} low frecency 0`,
     },
     {
       uri: Services.io.newURI("http://example.com/lo1"),
-      title: "low frecency 1",
+      title: `${SEARCH_STRING} low frecency 1`,
     },
     {
       uri: Services.io.newURI("http://example.com/lo2"),
-      title: "low frecency 2",
+      title: `${SEARCH_STRING} low frecency 2`,
     },
     {
       uri: Services.io.newURI("http://example.com/lo3"),
-      title: "low frecency 3",
+      title: `${SEARCH_STRING} low frecency 3`,
     },
     {
       uri: Services.io.newURI("http://example.com/lo4"),
-      title: "low frecency 4",
+      title: `${SEARCH_STRING} low frecency 4`,
     },
   ]);
 
   for (let i = 0; i < 5; i++) {
     await PlacesTestUtils.addVisits([
       {
         uri: Services.io.newURI("http://example.com/hi0"),
-        title: "high frecency 0",
+        title: `${SEARCH_STRING} high frecency 0`,
         transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
       },
       {
         uri: Services.io.newURI("http://example.com/hi1"),
-        title: "high frecency 1",
+        title: `${SEARCH_STRING} high frecency 1`,
         transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
       },
       {
         uri: Services.io.newURI("http://example.com/hi2"),
-        title: "high frecency 2",
+        title: `${SEARCH_STRING} high frecency 2`,
         transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
       },
       {
         uri: Services.io.newURI("http://example.com/hi3"),
-        title: "high frecency 3",
+        title: `${SEARCH_STRING} high frecency 3`,
         transition: Ci.nsINavHistoryService.TRANSITION_TYPED,
       },
     ]);
   }
 
   for (let i = 0; i < 4; i++) {
     let href = `http://example.com/hi${i}`;
     await PlacesTestUtils.addBookmarkWithDetails({
       uri: href,
-      title: `high frecency ${i}`,
+      title: `${SEARCH_STRING} high frecency ${i}`,
     });
   }
 
   // Do an unrestricted search to make sure everything appears in it, including
   // the visit and bookmark.
-  const query = "frecency";
-  let context = createContext(query, { isPrivate: false });
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi3",
-        title: "high frecency 3",
+        title: `${SEARCH_STRING} high frecency 3`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi2",
-        title: "high frecency 2",
+        title: `${SEARCH_STRING} high frecency 2`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi1",
-        title: "high frecency 1",
+        title: `${SEARCH_STRING} high frecency 1`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi0",
-        title: "high frecency 0",
+        title: `${SEARCH_STRING} high frecency 0`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo4",
-        title: "low frecency 4",
+        title: `${SEARCH_STRING} low frecency 4`,
       }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " foo",
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " bar",
-      }),
+      ...makeExpectedSuggestionResults(context),
       makeVisitResult(context, {
         uri: "http://example.com/lo3",
-        title: "low frecency 3",
+        title: `${SEARCH_STRING} low frecency 3`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo2",
-        title: "low frecency 2",
+        title: `${SEARCH_STRING} low frecency 2`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo1",
-        title: "low frecency 1",
+        title: `${SEARCH_STRING} low frecency 1`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo0",
-        title: "low frecency 0",
+        title: `${SEARCH_STRING} low frecency 0`,
       }),
     ],
   });
 
   // Change the "general" context mixup.
   Services.prefs.setCharPref(
     "browser.urlbar.matchBuckets",
     "suggestion:1,general:5,suggestion:1"
   );
 
   // Do an unrestricted search to make sure everything appears in it, including
   // the visits and bookmarks.
-  context = createContext(query, { isPrivate: false });
+  context = createContext(SEARCH_STRING, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " foo",
-      }),
+      ...makeExpectedSuggestionResults(context).slice(0, 1),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi3",
-        title: "high frecency 3",
+        title: `${SEARCH_STRING} high frecency 3`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi2",
-        title: "high frecency 2",
+        title: `${SEARCH_STRING} high frecency 2`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi1",
-        title: "high frecency 1",
+        title: `${SEARCH_STRING} high frecency 1`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi0",
-        title: "high frecency 0",
+        title: `${SEARCH_STRING} high frecency 0`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo4",
-        title: "low frecency 4",
+        title: `${SEARCH_STRING} low frecency 4`,
       }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " bar",
-      }),
+      ...makeExpectedSuggestionResults(context).slice(1),
       makeVisitResult(context, {
         uri: "http://example.com/lo3",
-        title: "low frecency 3",
+        title: `${SEARCH_STRING} low frecency 3`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo2",
-        title: "low frecency 2",
+        title: `${SEARCH_STRING} low frecency 2`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo1",
-        title: "low frecency 1",
+        title: `${SEARCH_STRING} low frecency 1`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo0",
-        title: "low frecency 0",
+        title: `${SEARCH_STRING} low frecency 0`,
       }),
     ],
   });
 
   // Change the "search" context mixup.
   Services.prefs.setCharPref(
     "browser.urlbar.matchBucketsSearch",
     "suggestion:2,general:4"
   );
 
-  context = createContext(query, { isPrivate: false });
+  context = createContext(SEARCH_STRING, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " foo",
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " bar",
-      }),
+      ...makeExpectedSuggestionResults(context).slice(0, 2),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi3",
-        title: "high frecency 3",
+        title: `${SEARCH_STRING} high frecency 3`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi2",
-        title: "high frecency 2",
+        title: `${SEARCH_STRING} high frecency 2`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi1",
-        title: "high frecency 1",
+        title: `${SEARCH_STRING} high frecency 1`,
       }),
       makeBookmarkResult(context, {
         uri: "http://example.com/hi0",
-        title: "high frecency 0",
+        title: `${SEARCH_STRING} high frecency 0`,
       }),
+      ...makeExpectedSuggestionResults(context).slice(2),
       makeVisitResult(context, {
         uri: "http://example.com/lo4",
-        title: "low frecency 4",
+        title: `${SEARCH_STRING} low frecency 4`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo3",
-        title: "low frecency 3",
+        title: `${SEARCH_STRING} low frecency 3`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo2",
-        title: "low frecency 2",
+        title: `${SEARCH_STRING} low frecency 2`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo1",
-        title: "low frecency 1",
+        title: `${SEARCH_STRING} low frecency 1`,
       }),
       makeVisitResult(context, {
         uri: "http://example.com/lo0",
-        title: "low frecency 0",
+        title: `${SEARCH_STRING} low frecency 0`,
       }),
     ],
   });
 
   Services.prefs.setCharPref(
     "browser.urlbar.matchBuckets",
     "general:5,suggestion:Infinity"
   );
   Services.prefs.clearUserPref("browser.urlbar.matchBucketsSearch");
   Services.prefs.clearUserPref(MAX_RICH_RESULTS_PREF);
   await cleanUpSuggestions();
 });
 
 add_task(async function prohibit_suggestions() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
-  Services.prefs.setBoolPref("browser.fixup.domainwhitelist.localhost", false);
+  Services.prefs.setBoolPref(
+    `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+    false
+  );
 
-  const query = "localhost";
-  let context = createContext(query, { isPrivate: false });
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " foo",
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: query + " bar",
-      }),
+      ...makeExpectedSuggestionResults(context),
     ],
   });
 
-  Services.prefs.setBoolPref("browser.fixup.domainwhitelist.localhost", true);
+  Services.prefs.setBoolPref(
+    `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+    true
+  );
   registerCleanupFunction(() => {
     Services.prefs.setBoolPref(
-      "browser.fixup.domainwhitelist.localhost",
+      `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
       false
     );
   });
-  context = createContext(query, { isPrivate: false });
+  context = createContext(SEARCH_STRING, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeVisitResult(context, {
-        uri: "http://localhost/",
-        title: "http://localhost/",
+        uri: `http://${SEARCH_STRING}/`,
+        title: `http://${SEARCH_STRING}/`,
         iconUri: "",
         heuristic: true,
       }),
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
         heuristic: false,
       }),
+      ...makeExpectedFormHistoryResults(context),
     ],
   });
 
   // When using multiple words, we should still get suggestions:
-  context = createContext(`${query} other`, { isPrivate: false });
+  let query = `${SEARCH_STRING} world`;
+  context = createContext(query, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: `${query} other foo`,
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: `${query} other bar`,
-      }),
+      ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
     ],
   });
 
-  // Clear the whitelist for localhost, and try preferring DNS for any single
+  // Clear the whitelist for SEARCH_STRING and try preferring DNS for any single
   // word instead:
-  Services.prefs.setBoolPref("browser.fixup.domainwhitelist.localhost", false);
+  Services.prefs.setBoolPref(
+    `browser.fixup.domainwhitelist.${SEARCH_STRING}`,
+    false
+  );
   Services.prefs.setBoolPref("browser.fixup.dns_first_for_single_words", true);
   registerCleanupFunction(() => {
     Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
   });
 
-  context = createContext(query, { isPrivate: false });
+  context = createContext(SEARCH_STRING, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeVisitResult(context, {
-        uri: "http://localhost/",
-        title: "http://localhost/",
+        uri: `http://${SEARCH_STRING}/`,
+        title: `http://${SEARCH_STRING}/`,
         iconUri: "",
         heuristic: true,
       }),
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
         heuristic: false,
       }),
+      ...makeExpectedFormHistoryResults(context),
     ],
   });
 
   context = createContext("somethingelse", { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeVisitResult(context, {
@@ -748,29 +743,23 @@ add_task(async function prohibit_suggest
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
         heuristic: false,
       }),
     ],
   });
 
   // When using multiple words, we should still get suggestions:
-  context = createContext(`${query} other`, { isPrivate: false });
+  query = `${SEARCH_STRING} world`;
+  context = createContext(query, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: `${query} other foo`,
-      }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: `${query} other bar`,
-      }),
+      ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }),
     ],
   });
 
   Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words");
 
   context = createContext("http://1.2.3.4/", { isPrivate: false });
   await check_results({
     context,
@@ -841,41 +830,51 @@ add_task(async function prohibit_suggest
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
     ],
   });
 
   await cleanUpSuggestions();
 });
 
-add_task(async function avoid_url_suggestions() {
+add_task(async function avoid_remote_url_suggestions_1() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
+  Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
 
   setSuggestionsFn(searchStr => {
     let suffixes = [".com", "/test", ":1]", "@test", ". com"];
     return suffixes.map(s => searchStr + s);
   });
 
   const query = "test";
+
+  await updateSearchHistory("bump", `${query}.com`);
+
   let context = createContext(query, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      makeFormHistoryResult(context, {
+        engineName: ENGINE_NAME,
+        suggestion: `${query}.com`,
+      }),
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
         suggestion: `${query}. com`,
       }),
     ],
   });
 
   await cleanUpSuggestions();
+  await UrlbarTestUtils.formHistory.remove([`${query}.com`]);
+  Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
 });
 
-add_task(async function avoid_url_suggestions() {
+add_task(async function avoid_remote_url_suggestions_2() {
   Services.prefs.setBoolPref(SUGGEST_PREF, true);
   Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
 
   setSuggestionsFn(searchStr => {
     let suffixes = ["ed", "eds"];
     return suffixes.map(s => searchStr + s);
   });
 
@@ -1242,77 +1241,212 @@ add_task(async function avoid_url_sugges
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
     ],
   });
 
   await cleanUpSuggestions();
 });
 
-add_task(async function restrict_suggestions_after_no_results() {
-  // We don't fetch suggestions if a query with a length over
+add_task(async function restrict_remote_suggestions_after_no_results() {
+  // We don't fetch remote suggestions if a query with a length over
   // maxCharsForSearchSuggestions returns 0 results. We set it to 4 here to
   // avoid constructing a 100+ character string.
   Services.prefs.setIntPref("browser.urlbar.maxCharsForSearchSuggestions", 4);
   setSuggestionsFn(searchStr => {
     return [];
   });
 
-  const query = "hello";
+  const query = SEARCH_STRING.substring(0, SEARCH_STRING.length - 1);
   let context = createContext(query, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context),
+    ],
+  });
+
+  context = createContext(SEARCH_STRING, { isPrivate: false });
+  await check_results({
+    context,
+    matches: [
+      makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context),
+      // Because the previous search returned no suggestions, we will not fetch
+      // remote suggestions for this query that is just a longer version of the
+      // previous query.
+    ],
+  });
+
+  // Do one more search before resetting maxCharsForSearchSuggestions to reset
+  // the search suggestion provider's _lastLowResultsSearchSuggestion property.
+  // Otherwise it will be stuck at SEARCH_STRING, which interferes with
+  // subsequent tests.
+  context = createContext("not the search string", { isPrivate: false });
+  await check_results({
+    context,
+    matches: [
+      makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
     ],
   });
 
-  context = createContext(`${query}a`, { isPrivate: false });
+  Services.prefs.clearUserPref("browser.urlbar.maxCharsForSearchSuggestions");
+
+  await cleanUpSuggestions();
+});
+
+add_task(async function formHistory() {
+  Services.prefs.setBoolPref(SUGGEST_PREF, true);
+  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
+
+  Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0);
+  let context = createContext(SEARCH_STRING, { isPrivate: false });
+  await check_results({
+    context,
+    matches: [
+      makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedRemoteSuggestionResults(context),
+    ],
+  });
+
+  Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1);
+  context = createContext(SEARCH_STRING, { isPrivate: false });
+  await check_results({
+    context,
+    matches: [
+      makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context).slice(0, 1),
+      ...makeExpectedRemoteSuggestionResults(context),
+    ],
+  });
+
+  Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 2);
+  context = createContext(SEARCH_STRING, { isPrivate: false });
+  await check_results({
+    context,
+    matches: [
+      makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedFormHistoryResults(context).slice(0, 2),
+      ...makeExpectedRemoteSuggestionResults(context),
+    ],
+  });
+
+  // Do a search for exactly the suggestion of the first form history result.
+  // The heuristic's query should be the suggestion; the first form history
+  // result should not be included since it dupes the heuristic; the second form
+  // history result should not be included since it doesn't match; and both
+  // remote suggestions should be included.
+  let firstSuggestion = makeExpectedFormHistoryResults(context)[0].payload
+    .suggestion;
+  context = createContext(firstSuggestion, { isPrivate: false });
+  await check_results({
+    context,
+    matches: [
+      makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
+      ...makeExpectedRemoteSuggestionResults(context, {
+        suggestionPrefix: firstSuggestion,
+      }),
+    ],
+  });
+
+  // Add these form history strings to use below.
+  let formHistoryStrings = ["foo", "foobar", "fooquux"];
+  await UrlbarTestUtils.formHistory.add(formHistoryStrings);
+
+  // Search for "foo".  "foo" shouldn't be included since it dupes the
+  // heuristic.  Both "foobar" and "fooquux" should be included even though the
+  // max form history count is only two and there are three matching form
+  // history results (including "foo").
+  context = createContext("foo", { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      // Because the previous search returned no suggestions, we will not fetch
-      // suggestions for this query that is just a longer version of the
-      // previous query.
+      makeFormHistoryResult(context, {
+        suggestion: "foobar",
+        engineName: ENGINE_NAME,
+      }),
+      makeFormHistoryResult(context, {
+        suggestion: "fooquux",
+        engineName: ENGINE_NAME,
+      }),
+      ...makeExpectedRemoteSuggestionResults(context, {
+        suggestionPrefix: "foo",
+      }),
     ],
   });
 
-  Services.prefs.clearUserPref("browser.urlbar.maxCharsForSearchSuggestions");
-  await cleanUpSuggestions();
-});
+  // Add a visit that matches "foo" and will autofill so that the heuristic is
+  // not a search result.  Now the "foo" and "foobar" form history should be
+  // included.
+  await PlacesTestUtils.addVisits("http://foo.example.com/");
+  context = createContext("foo", { isPrivate: false });
+  await check_results({
+    context,
+    matches: [
+      makeVisitResult(context, {
+        uri: "http://foo.example.com/",
+        title: "foo.example.com",
+        heuristic: true,
+        tags: [],
+      }),
+      makeFormHistoryResult(context, {
+        suggestion: "foo",
+        engineName: ENGINE_NAME,
+      }),
+      makeFormHistoryResult(context, {
+        suggestion: "foobar",
+        engineName: ENGINE_NAME,
+      }),
+      ...makeExpectedRemoteSuggestionResults(context, {
+        suggestionPrefix: "foo",
+      }),
+    ],
+  });
+  await PlacesUtils.history.clear();
 
-add_task(async function historicalSuggestion() {
-  Services.prefs.setBoolPref(SUGGEST_PREF, true);
-  Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
-  Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
-
-  const query = "hello";
-  let context = createContext(query, { isPrivate: false });
+  // Add SERPs for "foobar" and "food" and search for "foo".  The "foo" form
+  // history should be excluded since it dupes the heuristic; the "foobar" and
+  // "fooquux" form history should be included; the "foobar" SERP visit should
+  // be excluded since it dupes the "foobar" form history; the "food" SERP
+  // should be included.
+  let engine = await Services.search.getDefault();
+  let [serpURL1] = UrlbarUtils.getSearchQueryUrl(engine, "foobar");
+  let [serpURL2] = UrlbarUtils.getSearchQueryUrl(engine, "food");
+  await PlacesTestUtils.addVisits([serpURL1, serpURL2]);
+  context = createContext("foo", { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
-        engineName: ENGINE_NAME,
-        suggestion: `${query} Barney!`,
+      makeVisitResult(context, {
+        uri: "http://localhost:9000/search?terms=food",
+        title: "test visit for http://localhost:9000/search?terms=food",
       }),
-      makeSearchResult(context, {
+      makeFormHistoryResult(context, {
+        suggestion: "foobar",
         engineName: ENGINE_NAME,
-        suggestion: `${query} foo`,
       }),
-      makeSearchResult(context, {
+      makeFormHistoryResult(context, {
+        suggestion: "fooquux",
         engineName: ENGINE_NAME,
-        suggestion: `${query} bar`,
+      }),
+      ...makeExpectedRemoteSuggestionResults(context, {
+        suggestionPrefix: "foo",
       }),
     ],
   });
+  await PlacesUtils.history.clear();
+
+  await UrlbarTestUtils.formHistory.remove(formHistoryStrings);
 
   await cleanUpSuggestions();
-  Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
+  await PlacesUtils.history.clear();
+  Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF);
 });
 
 function updateSearchHistory(op, value) {
   return new Promise((resolve, reject) => {
     FormHistory.update(
       { op, fieldname: "searchbar-history", value },
       {
         handleError(error) {
--- a/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js
@@ -35,17 +35,17 @@ add_task(async function engineWithSugges
           alias,
           query: "",
           heuristic: true,
         }),
       ];
       if (alias[0] != "@") {
         expectedMatches.push(
           makeVisitResult(context, {
-            uri: "http://localhost:9000/search",
+            uri: "http://localhost:9000/search?terms=",
             title: historyTitle,
           })
         );
       }
 
       await check_results({
         context,
         matches: expectedMatches,
@@ -59,17 +59,17 @@ add_task(async function engineWithSugges
           alias,
           query: "",
           heuristic: true,
         }),
       ];
       if (alias[0] != "@") {
         expectedMatches.push(
           makeVisitResult(context, {
-            uri: "http://localhost:9000/search",
+            uri: "http://localhost:9000/search?terms=",
             title: historyTitle,
           })
         );
       }
       await check_results({
         context,
         matches: expectedMatches,
       });
@@ -105,17 +105,17 @@ add_task(async function engineWithSugges
             query: historyTitle,
             suggestion: `${historyTitle} bar`,
           })
         );
       }
       if (alias[0] != "@") {
         expectedMatches.push(
           makeVisitResult(context, {
-            uri: "http://localhost:9000/search",
+            uri: "http://localhost:9000/search?terms=",
             title: historyTitle,
           })
         );
       }
       await check_results({
         context,
         matches: expectedMatches,
       });
--- a/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
+++ b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js
@@ -78,21 +78,16 @@ add_task(async function setup() {
     Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF);
     Services.prefs.clearUserPref(TAIL_SUGGESTIONS_PREF);
     Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF);
   });
   Services.search.setDefault(engine);
   Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false);
   Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, true);
   Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true);
-
-  // We must make sure the FormHistoryStartup component is initialized.
-  Cc["@mozilla.org/satchel/form-history-startup;1"]
-    .getService(Ci.nsIObserver)
-    .observe(null, "profile-after-change", null);
 });
 
 /**
  * Tests that non-tail suggestion providers still return results correctly when
  * the tailSuggestions pref is enabled.
  */
 add_task(async function normal_suggestions_provider() {
   let engine = await addTestSuggestionsEngine();
@@ -253,70 +248,49 @@ add_task(async function mixed_results() 
 
 /**
  * Tests that tail suggestions are deduped if their full-text form is a dupe of
  * a local search suggestion. Remaining tail suggestions should also not be
  * shown since we do not mix tail and non-tail suggestions.
  */
 add_task(async function dedupe_local() {
   Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1);
-  await updateSearchHistory("bump", "what time is it in toronto");
+  await UrlbarTestUtils.formHistory.add(["what time is it in toronto"]);
 
   const query = "what time is it in t";
   let context = createContext(query, { isPrivate: false });
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
-      makeSearchResult(context, {
+      makeFormHistoryResult(context, {
         engineName: ENGINE_NAME,
         suggestion: query + "oronto",
-        tail: undefined,
       }),
     ],
   });
 
   Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions");
   await cleanUpSuggestions();
 });
 
 /**
  * Tests that the correct number of suggestion results are displayed if
  * maxResults is limited, even when tail suggestions are returned.
  */
 add_task(async function limit_results() {
+  await UrlbarTestUtils.formHistory.clear();
   const query = "what time is it in t";
   let context = createContext(query, { isPrivate: false });
   context.maxResults = 2;
   await check_results({
     context,
     matches: [
       makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }),
       makeSearchResult(context, {
         engineName: ENGINE_NAME,
         suggestion: query + "oronto",
         tail: "toronto",
       }),
     ],
   });
   await cleanUpSuggestions();
 });
-
-function updateSearchHistory(op, value) {
-  return new Promise((resolve, reject) => {
-    FormHistory.update(
-      { op, fieldname: "searchbar-history", value },
-      {
-        handleError(error) {
-          do_throw("Error occurred updating form history: " + error);
-          reject(error);
-        },
-        handleCompletion(reason) {
-          if (reason) {
-            reject(reason);
-          } else {
-            resolve();
-          }
-        },
-      }
-    );
-  });
-}
--- a/browser/modules/BrowserUsageTelemetry.jsm
+++ b/browser/modules/BrowserUsageTelemetry.jsm
@@ -73,17 +73,17 @@ const KNOWN_SEARCH_SOURCES = [
 
 const KNOWN_ONEOFF_SOURCES = [
   "oneoff-urlbar",
   "oneoff-searchbar",
   "unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7).
 ];
 
 /**
- * The buckets used for logging telemetry to the FX_URLBAR_SELECTED_RESULT_TYPE
+ * Buckets used for logging telemetry to the FX_URLBAR_SELECTED_RESULT_TYPE_2
  * histogram.
  */
 const URLBAR_SELECTED_RESULT_TYPES = {
   autofill: 0,
   bookmark: 1,
   history: 2,
   keyword: 3,
   searchengine: 4,
@@ -91,18 +91,18 @@ const URLBAR_SELECTED_RESULT_TYPES = {
   switchtab: 6,
   tag: 7,
   visiturl: 8,
   remotetab: 9,
   extension: 10,
   "preloaded-top-site": 11,
   tip: 12,
   topsite: 13,
-  // There's no more space in this histogram, next addition must define a new
-  // one.
+  formhistory: 14,
+  // n_values = 32, so you'll need to create a new histogram if you need more.
 };
 
 /**
  * This maps the categories used by the FX_URLBAR_SELECTED_RESULT_METHOD and
  * FX_SEARCHBAR_SELECTED_RESULT_METHOD histograms to their indexes in the
  * `labels` array.  This only needs to be used by tests that need to map from
  * category names to indexes in histogram snapshots.  Actual app code can use
  * these category names directly when they add to a histogram.
@@ -422,16 +422,18 @@ let BrowserUsageTelemetry = {
    * @param {String} source
    *        Where the search originated from. See KNOWN_SEARCH_SOURCES for allowed
    *        values.
    * @param {Object} [details] Options object.
    * @param {Boolean} [details.isOneOff=false]
    *        true if this event was generated by a one-off search.
    * @param {Boolean} [details.isSuggestion=false]
    *        true if this event was generated by a suggested search.
+   * @param {Boolean} [details.isFormHistory=false]
+   *        true if this event was generated by a form history result.
    * @param {String} [details.alias=null]
    *        The search engine alias used in the search, if any.
    * @param {Object} [details.type=null]
    *        The object describing the event that triggered the search.
    * @throws if source is not in the known sources list.
    */
   recordSearch(tabbrowser, engine, source, details = {}) {
     if (!shouldRecordSearchCount(tabbrowser)) {
@@ -536,17 +538,21 @@ let BrowserUsageTelemetry = {
       }
 
       // If that's a legit one-off search signal, record it using the relative key.
       this._recordSearch(engine, sourceName, "oneoff");
       return;
     }
 
     // The search was not a one-off. It was a search with the default search engine.
-    if (details.isSuggestion) {
+    if (details.isFormHistory) {
+      // It came from a form history result.
+      this._recordSearch(engine, sourceName, "formhistory");
+      return;
+    } else if (details.isSuggestion) {
       // It came from a suggested search, so count it as such.
       this._recordSearch(engine, sourceName, "suggestion");
       return;
     } else if (details.alias) {
       // This one came from a search that used an alias.
       this._recordSearch(engine, sourceName, "alias");
       return;
     }
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
@@ -144,20 +144,20 @@ add_task(async function setup() {
 add_task(async function test_simpleQuery() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
 
   let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_URLBAR_SELECTED_RESULT_INDEX"
   );
   let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
-    "FX_URLBAR_SELECTED_RESULT_TYPE"
+    "FX_URLBAR_SELECTED_RESULT_TYPE_2"
   );
   let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
-    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
   );
   let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_URLBAR_SELECTED_RESULT_METHOD"
   );
   let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
     "SEARCH_COUNTS"
   );
 
@@ -241,20 +241,20 @@ add_task(async function test_simpleQuery
 add_task(async function test_searchAlias() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
 
   let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_URLBAR_SELECTED_RESULT_INDEX"
   );
   let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
-    "FX_URLBAR_SELECTED_RESULT_TYPE"
+    "FX_URLBAR_SELECTED_RESULT_TYPE_2"
   );
   let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
-    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
   );
   let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_URLBAR_SELECTED_RESULT_METHOD"
   );
   let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
     "SEARCH_COUNTS"
   );
 
@@ -389,20 +389,20 @@ add_task(async function test_internalSea
 add_task(async function test_oneOff_enter() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
 
   let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_URLBAR_SELECTED_RESULT_INDEX"
   );
   let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
-    "FX_URLBAR_SELECTED_RESULT_TYPE"
+    "FX_URLBAR_SELECTED_RESULT_TYPE_2"
   );
   let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
-    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
   );
   let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_URLBAR_SELECTED_RESULT_METHOD"
   );
   let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
     "SEARCH_COUNTS"
   );
 
@@ -555,25 +555,26 @@ add_task(async function test_oneOff_clic
 
   BrowserTestUtils.removeTab(tab);
 });
 
 // Clicks the first suggestion offered by the test search engine.
 add_task(async function test_suggestion_click() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
+  await UrlbarTestUtils.formHistory.clear();
 
   let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_URLBAR_SELECTED_RESULT_INDEX"
   );
   let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
-    "FX_URLBAR_SELECTED_RESULT_TYPE"
+    "FX_URLBAR_SELECTED_RESULT_TYPE_2"
   );
   let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
-    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
   );
   let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
     "FX_URLBAR_SELECTED_RESULT_METHOD"
   );
   let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
     "SEARCH_COUNTS"
   );
 
@@ -744,16 +745,248 @@ add_task(async function test_suggestion_
       URLBAR_SELECTED_RESULT_METHODS.enterSelection,
       1
     );
 
     BrowserTestUtils.removeTab(tab);
   });
 });
 
+// Clicks a form history result.
+add_task(async function test_formHistory_click() {
+  Services.telemetry.clearScalars();
+  Services.telemetry.clearEvents();
+  await UrlbarTestUtils.formHistory.clear();
+  await UrlbarTestUtils.formHistory.add(["foobar"]);
+
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+  });
+
+  let resultIndexHist = TelemetryTestUtils.getAndClearHistogram(
+    "FX_URLBAR_SELECTED_RESULT_INDEX"
+  );
+  let resultTypeHist = TelemetryTestUtils.getAndClearHistogram(
+    "FX_URLBAR_SELECTED_RESULT_TYPE_2"
+  );
+  let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram(
+    "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
+  );
+  let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+    "FX_URLBAR_SELECTED_RESULT_METHOD"
+  );
+  let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+    "SEARCH_COUNTS"
+  );
+
+  await withNewSearchEngine(async engine => {
+    let tab = await BrowserTestUtils.openNewForegroundTab(
+      gBrowser,
+      "about:blank"
+    );
+
+    info("Type a query. There should be form history.");
+    let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+    await searchInAwesomebar("foo");
+    info("Clicking the form history.");
+    await clickURLBarSuggestion("foobar");
+    await p;
+
+    // Check if the scalars contain the expected values.
+    const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+    TelemetryTestUtils.assertKeyedScalar(
+      scalars,
+      SCALAR_URLBAR,
+      "search_formhistory",
+      1
+    );
+    Assert.equal(
+      Object.keys(scalars[SCALAR_URLBAR]).length,
+      1,
+      "This search must only increment one entry in the scalar."
+    );
+
+    // SEARCH_COUNTS should be incremented.
+    let searchEngineId = "other-" + engine.name;
+    TelemetryTestUtils.assertKeyedHistogramSum(
+      search_hist,
+      searchEngineId + ".urlbar",
+      1
+    );
+
+    // Also check events.
+    TelemetryTestUtils.assertEvents(
+      [
+        [
+          "navigation",
+          "search",
+          "urlbar",
+          "formhistory",
+          { engine: searchEngineId },
+        ],
+      ],
+      { category: "navigation", method: "search" }
+    );
+
+    // Check the histograms as well.
+    TelemetryTestUtils.assertHistogram(resultIndexHist, 2, 1);
+
+    TelemetryTestUtils.assertHistogram(
+      resultTypeHist,
+      URLBAR_SELECTED_RESULT_TYPES.formhistory,
+      1
+    );
+
+    TelemetryTestUtils.assertKeyedHistogramValue(
+      resultIndexByTypeHist,
+      "formhistory",
+      2,
+      1
+    );
+
+    TelemetryTestUtils.assertHistogram(
+      resultMethodHist,
+      URLBAR_SELECTED_RESULT_METHODS.click,
+      1
+    );
+
+    BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
+    await SpecialPowers.popPrefEnv();
+  });
+});
+
+// Selects and presses the Return (Enter) key on a form history result.  This
+// only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram since
+// test_formHistory_click covers everything else.
+add_task(async function test_formHistory_arrowEnterSelection() {
+  Services.telemetry.clearScalars();
+  let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+    "FX_URLBAR_SELECTED_RESULT_METHOD"
+  );
+
+  await UrlbarTestUtils.formHistory.clear();
+  await UrlbarTestUtils.formHistory.add(["foobar"]);
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+  });
+
+  await withNewSearchEngine(async function() {
+    let tab = await BrowserTestUtils.openNewForegroundTab(
+      gBrowser,
+      "about:blank"
+    );
+
+    info("Type a query. There should be form history.");
+    let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+    await searchInAwesomebar("foo");
+    info("Select the form history result and press Return.");
+    while (gURLBar.untrimmedValue != "foobar") {
+      EventUtils.synthesizeKey("KEY_ArrowDown");
+    }
+    EventUtils.synthesizeKey("KEY_Enter");
+    await p;
+
+    TelemetryTestUtils.assertHistogram(
+      resultMethodHist,
+      URLBAR_SELECTED_RESULT_METHODS.arrowEnterSelection,
+      1
+    );
+
+    BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
+    await SpecialPowers.popPrefEnv();
+  });
+});
+
+// Selects through tab and presses the Return (Enter) key on a form history
+// result.
+add_task(async function test_formHistory_tabEnterSelection() {
+  Services.telemetry.clearScalars();
+  let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+    "FX_URLBAR_SELECTED_RESULT_METHOD"
+  );
+
+  await UrlbarTestUtils.formHistory.clear();
+  await UrlbarTestUtils.formHistory.add(["foobar"]);
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+  });
+
+  await withNewSearchEngine(async function() {
+    let tab = await BrowserTestUtils.openNewForegroundTab(
+      gBrowser,
+      "about:blank"
+    );
+
+    info("Type a query. There should be form history.");
+    let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+    await searchInAwesomebar("foo");
+    info("Select the form history result and press Return.");
+    while (gURLBar.untrimmedValue != "foobar") {
+      EventUtils.synthesizeKey("KEY_Tab");
+    }
+    EventUtils.synthesizeKey("KEY_Enter");
+    await p;
+
+    TelemetryTestUtils.assertHistogram(
+      resultMethodHist,
+      URLBAR_SELECTED_RESULT_METHODS.tabEnterSelection,
+      1
+    );
+
+    BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
+    await SpecialPowers.popPrefEnv();
+  });
+});
+
+// Selects through code and presses the Return (Enter) key on a form history
+// result.
+add_task(async function test_formHistory_enterSelection() {
+  Services.telemetry.clearScalars();
+  let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+    "FX_URLBAR_SELECTED_RESULT_METHOD"
+  );
+
+  await UrlbarTestUtils.formHistory.clear();
+  await UrlbarTestUtils.formHistory.add(["foobar"]);
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]],
+  });
+
+  await withNewSearchEngine(async function() {
+    let tab = await BrowserTestUtils.openNewForegroundTab(
+      gBrowser,
+      "about:blank"
+    );
+
+    info("Type a query. There should be form history.");
+    let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+    await searchInAwesomebar("foo");
+    info("Select the second result and press Return.");
+    let index = 1;
+    while (gURLBar.untrimmedValue != "foobar") {
+      UrlbarTestUtils.setSelectedRowIndex(window, index++);
+    }
+    EventUtils.synthesizeKey("KEY_Enter");
+    await p;
+
+    TelemetryTestUtils.assertHistogram(
+      resultMethodHist,
+      URLBAR_SELECTED_RESULT_METHODS.enterSelection,
+      1
+    );
+
+    BrowserTestUtils.removeTab(tab);
+    await UrlbarTestUtils.formHistory.clear();
+    await SpecialPowers.popPrefEnv();
+  });
+});
+
 add_task(async function test_privateWindow() {
   // Override the search telemetry search provider info to
   // count in-content SEARCH_COUNTs telemetry for our test engine.
   SearchTelemetry.overrideSearchTelemetryForTests({
     example: {
       regexp: "^http://example\\.com/",
       queryParam: "q",
     },
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar_extension.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar_extension.js
@@ -54,20 +54,20 @@ function assertSearchTelemetryEmpty(sear
 function snapshotHistograms() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
   return {
     resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_INDEX"
     ),
     resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
-      "FX_URLBAR_SELECTED_RESULT_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_TYPE_2"
     ),
     resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
-      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
     ),
     resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_METHOD"
     ),
     search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
   };
 }
 
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar_places.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar_places.js
@@ -69,20 +69,20 @@ function assertSearchTelemetryEmpty(sear
 function snapshotHistograms() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
   return {
     resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_INDEX"
     ),
     resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
-      "FX_URLBAR_SELECTED_RESULT_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_TYPE_2"
     ),
     resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
-      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
     ),
     resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_METHOD"
     ),
     search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
   };
 }
 
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar_remotetab.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar_remotetab.js
@@ -56,20 +56,20 @@ function assertSearchTelemetryEmpty(sear
 function snapshotHistograms() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
   return {
     resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_INDEX"
     ),
     resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
-      "FX_URLBAR_SELECTED_RESULT_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_TYPE_2"
     ),
     resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
-      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
     ),
     resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_METHOD"
     ),
     search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
   };
 }
 
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar_tip.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar_tip.js
@@ -21,20 +21,20 @@ XPCOMUtils.defineLazyModuleGetters(this,
 function snapshotHistograms() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
   return {
     resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_INDEX"
     ),
     resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
-      "FX_URLBAR_SELECTED_RESULT_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_TYPE_2"
     ),
     resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
-      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
     ),
     resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_METHOD"
     ),
     search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
   };
 }
 
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar_topsite.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar_topsite.js
@@ -22,20 +22,20 @@ const EN_US_TOPSITES =
 function snapshotHistograms() {
   Services.telemetry.clearScalars();
   Services.telemetry.clearEvents();
   return {
     resultIndexHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_INDEX"
     ),
     resultTypeHist: TelemetryTestUtils.getAndClearHistogram(
-      "FX_URLBAR_SELECTED_RESULT_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_TYPE_2"
     ),
     resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram(
-      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE"
+      "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2"
     ),
     resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
       "FX_URLBAR_SELECTED_RESULT_METHOD"
     ),
     search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"),
   };
 }
 
--- a/toolkit/components/search/SearchSuggestionController.jsm
+++ b/toolkit/components/search/SearchSuggestionController.jsm
@@ -280,21 +280,16 @@ SearchSuggestionController.prototype = {
    *
    * Note: If there was no remote results fetched, the fetching cannot be stopped and local results
    * will still be returned because stopping relies on aborting the XMLHTTPRequest to reject the
    * promise for Promise.all.
    */
   stop() {
     if (this._request) {
       this._request.abort();
-    } else if (!this.maxRemoteResults) {
-      Cu.reportError(
-        "SearchSuggestionController: Cannot stop fetching if remote results were not " +
-          "requested"
-      );
     }
     this._reset();
   },
 
   // Private methods
 
   _fetchFormHistory(searchTerm) {
     return new Promise(resolve => {
--- a/toolkit/components/telemetry/Events.yaml
+++ b/toolkit/components/telemetry/Events.yaml
@@ -438,17 +438,17 @@ urlbar:
     extra_keys:
       elapsed: engagement time in milliseconds.
       numChars: number of input characters.
       selIndex: index of the selected result in the urlbar panel, or -1.
       selType: >
         type of the selected result in the urlbar panel. One of:
           "autofill", "visit", "bookmark", "history", "keyword", "search",
           "searchsuggestion", "switchtab", "remotetab", "extension", "oneoff",
-          "keywordoffer", "canonized", "tip", "tiphelp", "none"
+          "keywordoffer", "canonized", "tip", "tiphelp", "formhistory", "none"
   abandonment:
     objects: ["blur"]
     release_channel_collection: opt-out
     products:
       - "firefox"
     record_in_processes: ["main"]
     description: >
       This is recorded on urlbar search abandon, that is when the user starts
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -7619,37 +7619,37 @@
     "alert_emails": ["fx-search@mozilla.com"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 17,
     "releaseChannelCollection": "opt-out",
     "bug_numbers": [775825],
     "description": "Firefox: The index of the selected result in the URL bar popup"
   },
-  "FX_URLBAR_SELECTED_RESULT_TYPE": {
+  "FX_URLBAR_SELECTED_RESULT_TYPE_2": {
     "record_in_processes": ["main", "content"],
     "products": ["firefox", "fennec", "geckoview"],
     "alert_emails": ["fx-search@mozilla.com"],
     "expires_in_version": "never",
     "kind": "enumerated",
-    "n_values": 14,
-    "releaseChannelCollection": "opt-out",
-    "bug_numbers": [775825, 1617631],
+    "n_values": 32,
+    "releaseChannelCollection": "opt-out",
+    "bug_numbers": [775825, 1617631, 1398416],
     "description": "Firefox: The type of the selected result in the URL bar popup. See BrowserUsageTelemetry.jsm:URLBAR_SELECTED_RESULT_TYPES for the result types."
   },
-  "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE": {
+  "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2": {
     "record_in_processes": ["main", "content"],
     "products": ["firefox", "fennec", "geckoview"],
     "alert_emails": ["fx-search@mozilla.com"],
     "expires_in_version": "never",
     "kind": "enumerated",
-    "n_values": 14,
-    "keyed": true,
-    "releaseChannelCollection": "opt-out",
-    "bug_numbers": [1345834, 1617631],
+    "n_values": 32,
+    "keyed": true,
+    "releaseChannelCollection": "opt-out",
+    "bug_numbers": [1345834, 1617631, 1398416],
     "description": "Firefox: The index of the selected result in the URL bar popup by the type of the selected result in the URL bar popup. See BrowserUsageTelemetry.jsm:URLBAR_SELECTED_RESULT_TYPES for the result types."
   },
   "FX_URLBAR_SELECTED_RESULT_METHOD": {
     "record_in_processes": ["main", "content"],
     "products": ["firefox", "fennec", "geckoview"],
     "alert_emails": ["dzeber@mozilla.com", "fx-search@mozilla.com"],
     "expires_in_version": "never",
     "releaseChannelCollection": "opt-out",