Bug 1496814 - Show only search suggestion results when only an engine alias and query are typed in the address bar r=mak
authorDrew Willcoxon <adw@mozilla.com>
Thu, 18 Oct 2018 15:23:55 +0000
changeset 500407 665fecc387403fd9336daaaa6bfa619dc5b7baed
parent 500406 88aa6ae72305247b0520e0d698e751749fb8d60b
child 500408 26c0053b13cb7ea1cce71210709fc30472f68671
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1496814, 1496811
milestone64.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 1496814 - Show only search suggestion results when only an engine alias and query are typed in the address bar r=mak This is based on the patch in bug 1496811. This patch looks a little worse than it probably is. (Maybe not by much.) Some of this is indentation changes, moving code around, renaming, and adding jsdocs. It looks like we missed the boat on uplifting this (and the other couple of bugs) to 63, so there's not a super-pressing need to keep the patch minimal. PlacesSearchAutocompleteProvider assumes you're fetching suggestions from the current engine, so I had to modify it to take an engine. While I was doing that, I got a little frustrated with some of its implementation, naming, and interface. It seems like it was written to be a little more generic than it ended up being? There doesn't seem to be any need for it to return generic "match" objects instead of simply engines and `{ suggestion, historical }` objects, for example. The "defaultMatch" concept also doesn't make much sense IMO, especially with the aforementioned changes. So I made some improvements, hopefully, and I also added some jsdocs. Differential Revision: https://phabricator.services.mozilla.com/D8818
toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js
--- a/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
+++ b/toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
@@ -14,29 +14,28 @@ ChromeUtils.import("resource://gre/modul
 
 ChromeUtils.defineModuleGetter(this, "SearchSuggestionController",
   "resource://gre/modules/SearchSuggestionController.jsm");
 
 const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
 
 const SearchAutocompleteProviderInternal = {
   /**
-   * Array of objects in the format returned by findMatchByToken.
+   * {Map} domain string => nsISearchEngine with that domain.  If more than one
+   * engine has the same domain, the last one passed to _addEngine will be the
+   * one in this map.
    */
-  priorityMatches: null,
+  enginesByDomain: new Map(),
 
   /**
-   * Array of objects in the format returned by findMatchByAlias.
+   * {Map} lowercased alias string => nsISearchEngine with that alias.  If more
+   * than one engine has the same alias, the last one passed to _addEngine will
+   * be the one in this map.
    */
-  aliasMatches: null,
-
-  /**
-   * Object for the default search match.
-   **/
-  defaultMatch: null,
+  enginesByAlias: new Map(),
 
   initialize() {
     return new Promise((resolve, reject) => {
       Services.search.init(status => {
         if (!Components.isSuccessCode(status)) {
           reject(new Error("Unable to initialize search service."));
         }
 
@@ -63,150 +62,133 @@ const SearchAutocompleteProviderInternal
       case "engine-changed":
       case "engine-removed":
       case "engine-current":
         this._refresh();
     }
   },
 
   _refresh() {
-    this.priorityMatches = [];
-    this.aliasMatches = [];
-    this.defaultMatch = null;
-
-    let currentEngine = Services.search.currentEngine;
-    // This can be null in XCPShell.
-    if (currentEngine) {
-      this.defaultMatch = {
-        engineName: currentEngine.name,
-        iconUrl: currentEngine.iconURI ? currentEngine.iconURI.spec : null,
-      };
-    }
+    this.enginesByDomain.clear();
+    this.enginesByAlias.clear();
 
     // The search engines will always be processed in the order returned by the
     // search service, which can be defined by the user.
     Services.search.getEngines().forEach(e => this._addEngine(e));
   },
 
   _addEngine(engine) {
     let domain = engine.getResultDomain();
-
     if (domain && !engine.hidden) {
-      this.priorityMatches.push({
-        token: domain,
-        // The searchForm property returns a simple URL for the search engine, but
-        // we may need an URL which includes an affiliate code (bug 990799).
-        url: engine.searchForm,
-        engineName: engine.name,
-        iconUrl: engine.iconURI ? engine.iconURI.spec : null,
-      });
+      this.enginesByDomain.set(domain, engine);
     }
 
     let aliases = [];
-
     if (engine.alias) {
       aliases.push(engine.alias);
     }
     aliases.push(...engine.wrappedJSObject._internalAliases);
-
-    if (aliases.length) {
-      this.aliasMatches.push({
-        aliases,
-        engineName: engine.name,
-        iconUrl: engine.iconURI ? engine.iconURI.spec : null,
-        resultDomain: domain,
-      });
+    for (let alias of aliases) {
+      this.enginesByAlias.set(alias.toLocaleLowerCase(), engine);
     }
   },
 
-  getSuggestionController(searchToken, inPrivateContext, maxLocalResults,
-                          maxRemoteResults, userContextId) {
-    let engine = Services.search.currentEngine;
-    if (!engine) {
-      return null;
-    }
-    return new SearchSuggestionControllerWrapper(engine, searchToken,
-                                                 inPrivateContext,
-                                                 maxLocalResults, maxRemoteResults,
-                                                 userContextId);
-  },
-
   QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver,
                                           Ci.nsISupportsWeakReference]),
 };
 
-function SearchSuggestionControllerWrapper(engine, searchToken,
-                                           inPrivateContext,
-                                           maxLocalResults, maxRemoteResults,
-                                           userContextId) {
-  this._controller = new SearchSuggestionController();
-  this._controller.maxLocalResults = maxLocalResults;
-  this._controller.maxRemoteResults = maxRemoteResults;
-  let promise = this._controller.fetch(searchToken, inPrivateContext, engine, userContextId);
-  this._suggestions = [];
-  this._success = false;
-  this._promise = promise.then(results => {
-    this._success = true;
+class SuggestionsFetch {
+  /**
+   * Create a new instance of this class for each new suggestions fetch.
+   *
+   * @param {nsISearchEngine} engine
+   *        The engine from which suggestions will be fetched.
+   * @param {string} searchToken
+   *        The search query string.
+   * @param {bool} inPrivateContext
+   *        Pass true if the fetch is being done in a private window.
+   * @param {int} maxLocalResults
+   *        The maximum number of results to fetch from the user's local
+   *        history.
+   * @param {int} maxRemoteResults
+   *        The maximum number of results to fetch from the search engine.
+   * @param {int} userContextId
+   *        The user context ID in which the fetch is being performed.
+   */
+  constructor(engine,
+              searchToken,
+              inPrivateContext,
+              maxLocalResults,
+              maxRemoteResults,
+              userContextId) {
+    this._controller = new SearchSuggestionController();
+    this._controller.maxLocalResults = maxLocalResults;
+    this._controller.maxRemoteResults = maxRemoteResults;
+    this._engine = engine;
     this._suggestions = [];
-    if (results) {
-      this._suggestions = this._suggestions.concat(
-        results.local.map(r => ({ suggestion: r, historical: true }))
-      );
-      this._suggestions = this._suggestions.concat(
-        results.remote.map(r => ({ suggestion: r, historical: false }))
-      );
-    }
-  }).catch(err => {
-    // fetch() rejects its promise if there's a pending request.
-  });
-}
-
-SearchSuggestionControllerWrapper.prototype = {
+    this._success = false;
+    this._promise = this._controller.fetch(searchToken, inPrivateContext, engine, userContextId).then(results => {
+      this._success = true;
+      if (results) {
+        this._suggestions.push(
+          ...results.local.map(r => ({ suggestion: r, historical: true })),
+          ...results.remote.map(r => ({ suggestion: r, historical: false }))
+        );
+      }
+    }).catch(err => {
+      // fetch() rejects its promise if there's a pending request.
+    });
+  }
 
   /**
-   * Resolved when all suggestions have been fetched.
+   * {nsISearchEngine} The engine from which suggestions are being fetched.
+   */
+  get engine() {
+    return this._engine;
+  }
+
+  /**
+   * {promise} Resolved when all suggestions have been fetched.
    */
   get fetchCompletePromise() {
     return this._promise;
-  },
+  }
 
   /**
    * Returns one suggestion, if any are available, otherwise returns null.
    * Note that may be multiple reasons why suggestions are not available:
    *  - all suggestions have already been consumed
    *  - the fetch failed
    *  - the fetch didn't complete yet (should have awaited the promise)
    *
-   * @return An object {match, suggestion, historical}.
+   * @returns {object} An object { suggestion, historical } or null if no
+   *          suggestions are available.
+   *          - suggestion {string} The suggestion.
+   *          - historical {bool} True if the suggestion comes from the user's
+   *            local history (instead of the search engine).
    */
   consume() {
-    if (!this._suggestions.length)
-      return null;
-    let { suggestion, historical } = this._suggestions.shift();
-    return { match: SearchAutocompleteProviderInternal.defaultMatch,
-             suggestion,
-             historical,
-           };
-  },
+    return this._suggestions.shift() || null;
+  }
 
   /**
    * Returns the number of fetched suggestions, or -1 if the fetching was
    * incomplete or failed.
    */
   get resultsCount() {
     return this._success ? this._suggestions.length : -1;
-  },
+  }
 
   /**
    * Stops the fetch.
    */
   stop() {
     this._controller.stop();
-  },
-};
+  }
+}
 
 var gInitializationPromise = null;
 
 var PlacesSearchAutocompleteProvider = Object.freeze({
   /**
    * Starts initializing the component and returns a promise that is resolved or
    * rejected when initialization finished.  The same promise is returned if
    * this function is called multiple times.
@@ -214,71 +196,60 @@ var PlacesSearchAutocompleteProvider = O
   ensureInitialized() {
     if (!gInitializationPromise) {
       gInitializationPromise = SearchAutocompleteProviderInternal.initialize();
     }
     return gInitializationPromise;
   },
 
   /**
-   * Matches a given string to an item that should be included by URL search
-   * components, like autocomplete in the address bar.
-   *
-   * @param searchToken
-   *        String containing the first part of the matching domain name.
+   * Gets the engine whose domain matches a given prefix.
    *
-   * @return An object with the following properties, or undefined if the token
-   *         does not match any relevant URL:
-   *         {
-   *           token: The full string used to match the search term to the URL.
-   *           url: The URL to navigate to if the match is selected.
-   *           engineName: The display name of the search engine.
-   *           iconUrl: Icon associated to the match, or null if not available.
-   *         }
+   * @param   {string} prefix
+   *          String containing the first part of the matching domain name.
+   * @returns {nsISearchEngine} The matching engine or null if there isn't one.
    */
-  async findMatchByToken(searchToken) {
+  async engineForDomainPrefix(prefix) {
     await this.ensureInitialized();
 
     // Match at the beginning for now.  In the future, an "options" argument may
     // allow the matching behavior to be tuned.
-    return SearchAutocompleteProviderInternal.priorityMatches.find(m => {
-      return m.token.startsWith(searchToken) ||
-             m.token.startsWith("www." + searchToken);
-    });
+    let tuples = SearchAutocompleteProviderInternal.enginesByDomain.entries();
+    for (let [domain, engine] of tuples) {
+      if (domain.startsWith(prefix) || domain.startsWith("www." + prefix)) {
+        return engine;
+      }
+    }
+    return null;
   },
 
   /**
-   * Matches a given search string to an item that should be included by
-   * components wishing to search using search engine aliases, like
-   * autocomple.
-   *
-   * @param searchToken
-   *        Search string to match exactly a search engine alias.
+   * Gets the engine with a given alias.
    *
-   * @return An object with the following properties, or undefined if the token
-   *         does not match any relevant URL:
-   *         {
-   *           alias: The matched search engine's alias.
-   *           engineName: The display name of the search engine.
-   *           iconUrl: Icon associated to the match, or null if not available.
-   *           resultDomain: The domain name for the search engine's results;
-   *                         see nsISearchEngine::getResultDomain.
-   *         }
+   * @param   {string} alias
+   *          A search engine alias.
+   * @returns {nsISearchEngine} The matching engine or null if there isn't one.
    */
-  async findMatchByAlias(searchToken) {
+  async engineForAlias(alias) {
     await this.ensureInitialized();
 
-    return SearchAutocompleteProviderInternal.aliasMatches
-             .find(m => m.aliases.some(a => a.toLocaleLowerCase() == searchToken.toLocaleLowerCase()));
+    return SearchAutocompleteProviderInternal
+           .enginesByAlias.get(alias.toLocaleLowerCase()) || null;
   },
 
-  async getDefaultMatch() {
+  /**
+   * Use this to get the current engine rather than Services.search.currentEngine
+   * directly.  This method makes sure that the service is first initialized.
+   *
+   * @returns {nsISearchEngine} The current search engine.
+   */
+  async currentEngine() {
     await this.ensureInitialized();
 
-    return SearchAutocompleteProviderInternal.defaultMatch;
+    return Services.search.currentEngine;
   },
 
   /**
    * Synchronously determines if the provided URL represents results from a
    * search engine, and provides details about the match.
    *
    * @param url
    *        String containing the URL to parse.
@@ -303,18 +274,44 @@ var PlacesSearchAutocompleteProvider = O
 
     let parseUrlResult = Services.search.parseSubmissionURL(url);
     return parseUrlResult.engine && {
       engineName: parseUrlResult.engine.name,
       terms: parseUrlResult.terms,
     };
   },
 
-  getSuggestionController(searchToken, inPrivateContext, maxLocalResults,
-                          maxRemoteResults, userContextId) {
+  /**
+   * Starts a new suggestions fetch.
+   *
+   * @param   {nsISearchEngine} engine
+   *          The engine from which suggestions will be fetched.
+   * @param   {string} searchToken
+   *          The search query string.
+   * @param   {bool} inPrivateContext
+   *          Pass true if the fetch is being done in a private window.
+   * @param   {int} maxLocalResults
+   *          The maximum number of results to fetch from the user's local
+   *          history.
+   * @param   {int} maxRemoteResults
+   *          The maximum number of results to fetch from the search engine.
+   * @param   {int} userContextId
+   *          The user context ID in which the fetch is being performed.
+   * @returns {SuggestionsFetch} A new suggestions fetch object you should use
+   *          to track the fetch.
+   */
+  newSuggestionsFetch(engine,
+                      searchToken,
+                      inPrivateContext,
+                      maxLocalResults,
+                      maxRemoteResults,
+                      userContextId) {
     if (!SearchAutocompleteProviderInternal.initialized) {
       throw new Error("The component has not been initialized.");
     }
-    return SearchAutocompleteProviderInternal.getSuggestionController(
-      searchToken, inPrivateContext, maxLocalResults, maxRemoteResults,
-      userContextId);
+    if (!engine) {
+      throw new Error("`engine` is null");
+    }
+    return new SuggestionsFetch(engine, searchToken, inPrivateContext,
+                                maxLocalResults, maxRemoteResults,
+                                userContextId);
   },
 });
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -740,19 +740,19 @@ Search.prototype = {
       this._notifyTimer.cancel();
     this._notifyDelaysCount = 0;
     if (this._sleepTimer)
       this._sleepTimer.cancel();
     if (this._sleepResolve) {
       this._sleepResolve();
       this._sleepResolve = null;
     }
-    if (this._searchSuggestionController) {
-      this._searchSuggestionController.stop();
-      this._searchSuggestionController = null;
+    if (this._suggestionsFetch) {
+      this._suggestionsFetch.stop();
+      this._suggestionsFetch = null;
     }
     if (typeof this.interrupt == "function") {
       this.interrupt();
     }
     this.pending = false;
   },
 
   /**
@@ -830,54 +830,71 @@ Search.prototype = {
     if (hasHeuristic) {
       await this._sleep(UrlbarPrefs.get("delay"));
       if (!this.pending)
         return;
 
       // If the heuristic result is a search engine result with an alias and an
       // empty query, then we're done.  We want to show only that single result
       // as a clear hint that the user can continue typing to search.
-      if (this._searchEngineAliasHasEmptyQuery) {
+      if (this._searchEngineAliasMatch && !this._searchEngineAliasMatch.query) {
         this._cleanUpNonCurrentMatches(null, false);
         this._autocompleteSearch.finishSearch(true);
         return;
       }
     }
 
     // Only add extension suggestions if the first token is a registered keyword
     // and the search string has characters after the first token.
     let extensionsCompletePromise = Promise.resolve();
     if (this._searchTokens.length > 0 &&
         ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
-        this._originalSearchString.length > this._searchTokens[0].length) {
+        this._originalSearchString.length > this._searchTokens[0].length &&
+        !this._searchEngineAliasMatch) {
       // Do not await on this, since extensions cannot notify when they are done
       // adding results, it may take too long.
       extensionsCompletePromise = this._matchExtensionSuggestions();
     } else if (ExtensionSearchHandler.hasActiveInputSession()) {
       ExtensionSearchHandler.handleInputCancelled();
     }
 
     let searchSuggestionsCompletePromise = Promise.resolve();
-    if (this._enableActions && this._searchTokens.length > 0) {
-      // Limit the string sent for search suggestions to a maximum length.
-      let searchString = this._searchTokens.join(" ")
-                             .substr(0, UrlbarPrefs.get("maxCharsForSearchSuggestions"));
-      // Avoid fetching suggestions if they are not required, private browsing
-      // mode is enabled, or the search string may expose sensitive information.
-      if (this.hasBehavior("searches") && !this._inPrivateWindow &&
-          !this._prohibitSearchSuggestionsFor(searchString)) {
-        searchSuggestionsCompletePromise = this._matchSearchSuggestions(searchString);
-        if (this.hasBehavior("restrict")) {
-          // Wait for the suggestions to be added.
-          await searchSuggestionsCompletePromise;
-          this._cleanUpNonCurrentMatches(UrlbarUtils.MATCH_GROUP.SUGGESTION);
-          // We're done if we're restricting to search suggestions.
-          // Notify the result completion then stop the search.
-          this._autocompleteSearch.finishSearch(true);
-          return;
+    if (this._enableActions) {
+      let query =
+        this._searchEngineAliasMatch ? this._searchEngineAliasMatch.query :
+        this._searchTokens.join(" ");
+      if (query) {
+        // Limit the string sent for search suggestions to a maximum length.
+        query = query.substr(0, UrlbarPrefs.get("maxCharsForSearchSuggestions"));
+        // Avoid fetching suggestions if they are not required, private browsing
+        // mode is enabled, or the query may expose sensitive information.
+        if (this.hasBehavior("searches") &&
+            !this._inPrivateWindow &&
+            !this._prohibitSearchSuggestionsFor(query)) {
+          let engine;
+          if (this._searchEngineAliasMatch) {
+            engine = this._searchEngineAliasMatch.engine;
+          } else {
+            engine = await PlacesSearchAutocompleteProvider.currentEngine();
+            if (!this.pending) {
+              return;
+            }
+          }
+          searchSuggestionsCompletePromise =
+            this._matchSearchSuggestions(engine, query);
+          // If the user has used a search engine alias, then the only results
+          // we want to show are suggestions from that engine, so we're done.
+          // We're also done if we're restricting results to suggestions.
+          if (this._searchEngineAliasMatch || this.hasBehavior("restrict")) {
+            // Wait for the suggestions to be added.
+            await searchSuggestionsCompletePromise;
+            this._cleanUpNonCurrentMatches(UrlbarUtils.MATCH_GROUP.SUGGESTION);
+            this._autocompleteSearch.finishSearch(true);
+            return;
+          }
         }
       }
     }
     // In any case, clear previous suggestions.
     searchSuggestionsCompletePromise.then(() => {
       this._cleanUpNonCurrentMatches(UrlbarUtils.MATCH_GROUP.SUGGESTION);
     });
 
@@ -1152,52 +1169,63 @@ Search.prototype = {
       if (matched) {
         return true;
       }
     }
 
     return false;
   },
 
-  _matchSearchSuggestions(searchString) {
-    this._searchSuggestionController =
-      PlacesSearchAutocompleteProvider.getSuggestionController(
+  _matchSearchSuggestions(engine, searchString) {
+    this._suggestionsFetch =
+      PlacesSearchAutocompleteProvider.newSuggestionsFetch(
+        engine,
         searchString,
         this._inPrivateWindow,
         UrlbarPrefs.get("maxHistoricalSearchSuggestions"),
         UrlbarPrefs.get("maxRichResults") - UrlbarPrefs.get("maxHistoricalSearchSuggestions"),
         this._userContextId
       );
-    return this._searchSuggestionController.fetchCompletePromise.then(() => {
-      // The search has been canceled already.
-      if (!this._searchSuggestionController)
+    return this._suggestionsFetch.fetchCompletePromise.then(() => {
+      // The fetch has been canceled already.
+      if (!this._suggestionsFetch) {
         return;
-      if (this._searchSuggestionController.resultsCount >= 0 &&
-          this._searchSuggestionController.resultsCount < 2) {
-        // The original string is used to properly compare with the next search.
+      }
+      if (this._suggestionsFetch.resultsCount >= 0 &&
+          this._suggestionsFetch.resultsCount < 2) {
+        // The original string is used to properly compare with the next fetch.
         this._lastLowResultsSearchSuggestion = this._originalSearchString;
       }
       while (this.pending) {
-        let result = this._searchSuggestionController.consume();
+        let result = this._suggestionsFetch.consume();
         if (!result)
           break;
-        let { match, suggestion, historical } = result;
+        let { suggestion, historical } = result;
         if (!looksLikeUrl(suggestion)) {
-          // Don't include the restrict token, if present.
-          let searchString = this._searchTokens.join(" ");
-          this._addSearchEngineMatch(match, searchString, suggestion, historical);
+          this._addSearchEngineMatch({
+            engine,
+            query: searchString,
+            suggestion,
+            historical,
+          });
         }
       }
     }).catch(Cu.reportError);
   },
 
   _prohibitSearchSuggestionsFor(searchString) {
     if (this._prohibitSearchSuggestions)
       return true;
 
+    // Never prohibit suggestions when the user has used a search engine alias.
+    // We want "@engine query" to return suggestions from the engine.
+    if (this._searchEngineAliasMatch) {
+      return false;
+    }
+
     // Suggestions for a single letter are unlikely to be useful.
     if (searchString.length < 2)
       return true;
 
     // The first token may be a whitelisted host.
     if (this._searchTokens.length == 1 &&
         REGEXP_SINGLEWORD_HOST.test(this._searchTokens[0]) &&
         Services.uriFixup.isDomainWhitelisted(this._searchTokens[0], -1)) {
@@ -1314,81 +1342,89 @@ Search.prototype = {
     if (searchStr.indexOf("/") == searchStr.length - 1) {
       searchStr = searchStr.slice(0, -1);
     }
     // If the search string looks more like a url than a domain, bail out.
     if (!looksLikeOrigin(searchStr)) {
       return false;
     }
 
-    let match =
-      await PlacesSearchAutocompleteProvider.findMatchByToken(searchStr);
+    let engine =
+      await PlacesSearchAutocompleteProvider.engineForDomainPrefix(searchStr);
+    if (!engine) {
+      return false;
+    }
+    let url = engine.searchForm;
+    let domain = engine.getResultDomain();
     // Verify that the match we got is acceptable. Autofilling "example/" to
     // "example.com/" would not be good.
-    if (!match ||
-        (this._strippedPrefix && !match.url.startsWith(this._strippedPrefix)) ||
-        !(match.token + "/").includes(this._searchString)) {
+    if ((this._strippedPrefix && !url.startsWith(this._strippedPrefix)) ||
+        !(domain + "/").includes(this._searchString)) {
       return false;
     }
 
     // The value that's autofilled in the input is the prefix the user typed, if
     // any, plus the portion of the engine domain that the user typed.  Append a
     // trailing slash too, as is usual with autofill.
     let value =
-      this._strippedPrefix +
-      match.token.substr(match.token.indexOf(searchStr)) +
-      "/";
+      this._strippedPrefix + domain.substr(domain.indexOf(searchStr)) + "/";
 
-    let finalCompleteValue = match.url;
+    let finalCompleteValue = url;
     try {
-      let fixupInfo = Services.uriFixup.getFixupURIInfo(match.url, 0);
+      let fixupInfo = Services.uriFixup.getFixupURIInfo(url, 0);
       if (fixupInfo.fixedURI) {
         finalCompleteValue = fixupInfo.fixedURI.spec;
       }
     } catch (ex) {}
 
     this._result.setDefaultIndex(0);
     this._addMatch({
       value,
       finalCompleteValue,
-      comment: match.engineName,
-      icon: match.iconUrl,
+      comment: engine.name,
+      icon: engine.iconURI ? engine.iconURI.spec : null,
       style: "priority-search",
       frecency: Infinity,
     });
     return true;
   },
 
   async _matchSearchEngineAlias() {
-    if (this._searchTokens.length < 1)
+    if (this._searchTokens.length < 1) {
       return false;
+    }
 
     let alias = this._searchTokens[0];
-    let match = await PlacesSearchAutocompleteProvider.findMatchByAlias(alias);
-    if (!match)
+    let engine = await PlacesSearchAutocompleteProvider.engineForAlias(alias);
+    if (!engine) {
       return false;
+    }
 
-    match.engineAlias = alias;
     let query = this._trimmedOriginalSearchString.substr(alias.length + 1);
-
-    this._addSearchEngineMatch(match, query);
-    this._searchEngineAliasHasEmptyQuery = !query;
+    this._searchEngineAliasMatch = {
+      engine,
+      query,
+      alias,
+    };
+    this._addSearchEngineMatch(this._searchEngineAliasMatch);
     if (!this._keywordSubstitute) {
-      this._keywordSubstitute = match.resultDomain;
+      this._keywordSubstitute = engine.getResultDomain();
     }
     return true;
   },
 
   async _matchCurrentSearchEngine() {
-    let match = await PlacesSearchAutocompleteProvider.getDefaultMatch();
-    if (!match)
+    let engine = await PlacesSearchAutocompleteProvider.currentEngine();
+    if (!engine || !this.pending) {
       return false;
-
-    let query = this._originalSearchString;
-    this._addSearchEngineMatch(match, query);
+    }
+    this._addSearchEngineMatch({
+      engine,
+      query: this._originalSearchString,
+    });
     return true;
   },
 
   _addExtensionMatch(content, comment) {
     let count = this._counts[UrlbarUtils.MATCH_GROUP.EXTENSION] +
                 this._counts[UrlbarUtils.MATCH_GROUP.HEURISTIC];
     if (count >= UrlbarUtils.MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
       return;
@@ -1402,32 +1438,50 @@ Search.prototype = {
       comment,
       icon: "chrome://browser/content/extension.svg",
       style: "action extension",
       frecency: Infinity,
       type: UrlbarUtils.MATCH_GROUP.EXTENSION,
     });
   },
 
-  _addSearchEngineMatch(searchMatch, query, suggestion = "", historical = false) {
+  /**
+   * Adds a search engine match.
+   *
+   * @param {nsISearchEngine} engine
+   *        The search engine associated with the match.
+   * @param {string} query
+   *        The search query string.
+   * @param {string} [alias]
+   *        The search engine alias associated with the match.
+   * @param {string} [suggestion]
+   *        The suggestion from the search engine.
+   * @param {bool} [historical]
+   *        True if the suggestion is from the user's local history.
+   */
+  _addSearchEngineMatch({engine,
+                         query,
+                         alias,
+                         suggestion,
+                         historical}) {
     let actionURLParams = {
-      engineName: searchMatch.engineName,
+      engineName: engine.name,
       input: suggestion || this._originalSearchString,
       searchQuery: query,
     };
     if (suggestion)
       actionURLParams.searchSuggestion = suggestion;
-    if (searchMatch.engineAlias) {
-      actionURLParams.alias = searchMatch.engineAlias;
+    if (alias) {
+      actionURLParams.alias = alias;
     }
     let value = PlacesUtils.mozActionURI("searchengine", actionURLParams);
     let match = {
       value,
-      comment: searchMatch.engineName,
-      icon: searchMatch.iconUrl,
+      comment: engine.name,
+      icon: engine.iconURI ? engine.iconURI.spec : null,
       style: "action searchengine",
       frecency: FRECENCY_DEFAULT,
     };
     if (suggestion) {
       match.style += " suggestion";
       match.type = UrlbarUtils.MATCH_GROUP.SUGGESTION;
     }
 
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -378,16 +378,20 @@ function makeSearchMatch(input, extra = 
   }
   let style = [ "action", "searchengine" ];
   if ("style" in extra && Array.isArray(extra.style)) {
     style.push(...extra.style);
   }
   if (extra.heuristic) {
     style.push("heuristic");
   }
+  if ("searchSuggestion" in extra) {
+    params.searchSuggestion = extra.searchSuggestion;
+    style.push("suggestion");
+  }
   return {
     uri: makeActionURI("searchengine", params),
     title: params.engineName,
     style,
   };
 }
 
 // Creates a full "match" entry for a search result, suitable for passing as
@@ -462,16 +466,43 @@ function addTestEngine(basename, httpSer
       resolve(engine);
     }, "browser-search-engine-modified");
 
     info("Adding engine from URL: " + dataUrl + basename);
     Services.search.addEngine(dataUrl + basename, null, false);
   });
 }
 
+/**
+ * Sets up a search engine that provides some suggestions by appending strings
+ * onto the search query.
+ *
+ * @param   {function} suggestionsFn
+ *          A function that returns an array of suggestion strings given a
+ *          search string.  If not given, a default function is used.
+ * @returns {nsISearchEngine} The new engine.
+ */
+async function addTestSuggestionsEngine(suggestionsFn = null) {
+  // This port number should match the number in engine-suggestions.xml.
+  let server = makeTestServer(9000);
+  server.registerPathHandler("/suggest", (req, resp) => {
+    // URL query params are x-www-form-urlencoded, which converts spaces into
+    // plus signs, so un-convert any plus signs back to spaces.
+    let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
+    let suggestions =
+      suggestionsFn ? suggestionsFn(searchStr) :
+      ["foo", "bar"].map(s => searchStr + " " + s);
+    let data = [searchStr, suggestions];
+    resp.setHeader("Content-Type", "application/json", false);
+    resp.write(JSON.stringify(data));
+  });
+  let engine = await addTestEngine("engine-suggestions.xml", server);
+  return engine;
+}
+
 // Ensure we have a default search engine and the keyword.enabled preference
 // set.
 add_task(async function ensure_search_engine() {
   // keyword.enabled is necessary for the tests to see keyword searches.
   Services.prefs.setBoolPref("keyword.enabled", true);
 
   // Initialize the search service, but first set this geo IP pref to a dummy
   // string.  When the search service is initialized, it contacts the URI named
--- a/toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_PlacesSearchAutocompleteProvider.js
@@ -13,98 +13,125 @@ add_task(async function() {
    Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
 
    Services.search.restoreDefaultEngines();
    Services.search.resetToOriginalDefaultEngine();
 });
 
 add_task(async function search_engine_match() {
   let engine = await promiseDefaultSearchEngine();
-  let token = engine.getResultDomain();
-  let match = await PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1));
-  Assert.equal(match.url, engine.searchForm);
-  Assert.equal(match.engineName, engine.name);
-  Assert.equal(match.iconUrl, engine.iconURI ? engine.iconURI.spec : null);
+  let domain = engine.getResultDomain();
+  let token = domain.substr(0, 1);
+  let matchedEngine =
+    await PlacesSearchAutocompleteProvider.engineForDomainPrefix(token);
+  Assert.equal(matchedEngine, engine);
 });
 
 add_task(async function no_match() {
-  Assert.equal(null, await PlacesSearchAutocompleteProvider.findMatchByToken("test"));
+  Assert.equal(
+    null,
+    await PlacesSearchAutocompleteProvider.engineForDomainPrefix("test")
+  );
 });
 
 add_task(async function hide_search_engine_nomatch() {
   let engine = await promiseDefaultSearchEngine();
-  let token = engine.getResultDomain();
+  let domain = engine.getResultDomain();
+  let token = domain.substr(0, 1);
   let promiseTopic = promiseSearchTopic("engine-changed");
   Services.search.removeEngine(engine);
   await promiseTopic;
   Assert.ok(engine.hidden);
-  let match = await PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1));
-  Assert.ok(!match || match.token != token);
+  let matchedEngine =
+    await PlacesSearchAutocompleteProvider.engineForDomainPrefix(token);
+  Assert.ok(!matchedEngine || matchedEngine.getResultDomain() != domain);
 });
 
 add_task(async function add_search_engine_match() {
   let promiseTopic = promiseSearchTopic("engine-added");
-  Assert.equal(null, await PlacesSearchAutocompleteProvider.findMatchByToken("bacon"));
+  Assert.equal(
+    null,
+    await PlacesSearchAutocompleteProvider.engineForDomainPrefix("bacon")
+  );
   Services.search.addEngineWithDetails("bacon", "", "pork", "Search Bacon",
                                        "GET", "http://www.bacon.moz/?search={searchTerms}");
   await promiseTopic;
-  let match = await PlacesSearchAutocompleteProvider.findMatchByToken("bacon");
-  Assert.equal(match.url, "http://www.bacon.moz");
-  Assert.equal(match.engineName, "bacon");
-  Assert.equal(match.iconUrl, null);
+  let matchedEngine =
+    await PlacesSearchAutocompleteProvider.engineForDomainPrefix("bacon");
+  Assert.ok(matchedEngine);
+  Assert.equal(matchedEngine.searchForm, "http://www.bacon.moz");
+  Assert.equal(matchedEngine.name, "bacon");
+  Assert.equal(matchedEngine.iconURI, null);
 });
 
 add_task(async function test_aliased_search_engine_match() {
-  Assert.equal(null, await PlacesSearchAutocompleteProvider.findMatchByAlias("sober"));
+  Assert.equal(
+    null,
+    await PlacesSearchAutocompleteProvider.engineForAlias("sober")
+  );
   // Lower case
-  let match = await PlacesSearchAutocompleteProvider.findMatchByAlias("pork");
-  Assert.equal(match.engineName, "bacon");
-  Assert.equal(match.aliases[0], "pork");
-  Assert.equal(match.iconUrl, null);
+  let matchedEngine =
+    await PlacesSearchAutocompleteProvider.engineForAlias("pork");
+  Assert.ok(matchedEngine);
+  Assert.equal(matchedEngine.name, "bacon");
+  Assert.equal(matchedEngine.alias, "pork");
+  Assert.equal(matchedEngine.iconURI, null);
   // Upper case
-  let match1 = await PlacesSearchAutocompleteProvider.findMatchByAlias("PORK");
-  Assert.equal(match1.engineName, "bacon");
-  Assert.equal(match1.aliases[0], "pork");
-  Assert.equal(match1.iconUrl, null);
+  matchedEngine = await PlacesSearchAutocompleteProvider.engineForAlias("PORK");
+  Assert.ok(matchedEngine);
+  Assert.equal(matchedEngine.name, "bacon");
+  Assert.equal(matchedEngine.alias, "pork");
+  Assert.equal(matchedEngine.iconURI, null);
   // Cap case
-  let match2 = await PlacesSearchAutocompleteProvider.findMatchByAlias("Pork");
-  Assert.equal(match2.engineName, "bacon");
-  Assert.equal(match2.aliases[0], "pork");
-  Assert.equal(match2.iconUrl, null);
+  matchedEngine = await PlacesSearchAutocompleteProvider.engineForAlias("Pork");
+  Assert.ok(matchedEngine);
+  Assert.equal(matchedEngine.name, "bacon");
+  Assert.equal(matchedEngine.alias, "pork");
+  Assert.equal(matchedEngine.iconURI, null);
 });
 
 add_task(async function test_aliased_search_engine_match_upper_case_alias() {
   let promiseTopic = promiseSearchTopic("engine-added");
-  Assert.equal(null, await PlacesSearchAutocompleteProvider.findMatchByToken("patch"));
+  Assert.equal(
+    null,
+    await PlacesSearchAutocompleteProvider.engineForDomainPrefix("patch")
+  );
   Services.search.addEngineWithDetails("patch", "", "PR", "Search Patch",
                                        "GET", "http://www.patch.moz/?search={searchTerms}");
   await promiseTopic;
   // lower case
-  let match = await PlacesSearchAutocompleteProvider.findMatchByAlias("pr");
-  Assert.equal(match.engineName, "patch");
-  Assert.equal(match.aliases[0], "PR");
-  Assert.equal(match.iconUrl, null);
+  let matchedEngine =
+    await PlacesSearchAutocompleteProvider.engineForAlias("pr");
+  Assert.ok(matchedEngine);
+  Assert.equal(matchedEngine.name, "patch");
+  Assert.equal(matchedEngine.alias, "PR");
+  Assert.equal(matchedEngine.iconURI, null);
   // Upper case
-  let match1 = await PlacesSearchAutocompleteProvider.findMatchByAlias("PR");
-  Assert.equal(match1.engineName, "patch");
-  Assert.equal(match1.aliases[0], "PR");
-  Assert.equal(match1.iconUrl, null);
+  matchedEngine = await PlacesSearchAutocompleteProvider.engineForAlias("PR");
+  Assert.ok(matchedEngine);
+  Assert.equal(matchedEngine.name, "patch");
+  Assert.equal(matchedEngine.alias, "PR");
+  Assert.equal(matchedEngine.iconURI, null);
   // Cap case
-  let match2 = await PlacesSearchAutocompleteProvider.findMatchByAlias("Pr");
-  Assert.equal(match2.engineName, "patch");
-  Assert.equal(match2.aliases[0], "PR");
-  Assert.equal(match2.iconUrl, null);
+  matchedEngine = await PlacesSearchAutocompleteProvider.engineForAlias("Pr");
+  Assert.ok(matchedEngine);
+  Assert.equal(matchedEngine.name, "patch");
+  Assert.equal(matchedEngine.alias, "PR");
+  Assert.equal(matchedEngine.iconURI, null);
 });
 
 add_task(async function remove_search_engine_nomatch() {
   let engine = Services.search.getEngineByName("bacon");
   let promiseTopic = promiseSearchTopic("engine-removed");
   Services.search.removeEngine(engine);
   await promiseTopic;
-  Assert.equal(null, await PlacesSearchAutocompleteProvider.findMatchByToken("bacon"));
+  Assert.equal(
+    null,
+    await PlacesSearchAutocompleteProvider.engineForDomainPrefix("bacon")
+  );
 });
 
 add_task(async function test_parseSubmissionURL_basic() {
   // Most of the logic of parseSubmissionURL is tested in the search service
   // itself, thus we only do a sanity check of the wrapper here.
   let engine = await promiseDefaultSearchEngine();
   let submissionURL = engine.getSubmission("terms").uri.spec;
 
@@ -112,18 +139,20 @@ add_task(async function test_parseSubmis
   Assert.equal(result.engineName, engine.name);
   Assert.equal(result.terms, "terms");
 
   result = PlacesSearchAutocompleteProvider.parseSubmissionURL("http://example.org/");
   Assert.equal(result, null);
 });
 
 add_task(async function test_builtin_aliased_search_engine_match() {
-  let match = await PlacesSearchAutocompleteProvider.findMatchByAlias("@google");
-  Assert.equal(match.engineName, "Google");
+  let matchedEngine =
+    await PlacesSearchAutocompleteProvider.engineForAlias("@google");
+  Assert.ok(matchedEngine);
+  Assert.equal(matchedEngine.name, "Google");
 });
 
 function promiseDefaultSearchEngine() {
   return new Promise(resolve => {
     Services.search.init( () => {
       resolve(Services.search.defaultEngine);
     });
   });
--- a/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
@@ -1,64 +1,168 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+const SUGGESTIONS_ENGINE_NAME = "engine-suggestions.xml";
 
-add_task(async function() {
+add_task(async function init() {
+  // This history result would match some of the searches below were it not for
+  // the fact that don't include history results when the user has used an
+  // engine alias.  Therefore, this result should never appear below.
+  await PlacesTestUtils.addVisits("http://s.example.com/search?q=firefox");
+});
+
+
+// Basic test that uses two engines, a GET engine and a POST engine, neither
+// providing search suggestions.
+add_task(async function getPost() {
   // Note that head_autocomplete.js has already added a MozSearch engine.
   // Here we add another engine with a search alias.
   Services.search.addEngineWithDetails("AliasedGETMozSearch", "", "get", "",
                                        "GET", "http://s.example.com/search");
   Services.search.addEngineWithDetails("AliasedPOSTMozSearch", "", "post", "",
                                        "POST", "http://s.example.com/search");
-  let histURI = NetUtil.newURI("http://s.example.com/search?q=firefox");
-  await PlacesTestUtils.addVisits([{ uri: histURI, title: "History entry" }]);
 
   for (let alias of ["get", "post"]) {
     await check_autocomplete({
       search: alias,
       searchParam: "enable-actions",
-      matches: [ makeSearchMatch(alias, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
-                                          searchQuery: "", alias, heuristic: true }),
+      matches: [
+        makeSearchMatch(alias, {
+          engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+          searchQuery: "",
+          alias,
+          heuristic: true,
+        }),
       ],
     });
 
     await check_autocomplete({
       search: `${alias} `,
       searchParam: "enable-actions",
-      matches: [ makeSearchMatch(`${alias} `, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
-                                                searchQuery: "", alias, heuristic: true }),
+      matches: [
+        makeSearchMatch(`${alias} `, {
+          engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+          searchQuery: "",
+          alias,
+          heuristic: true,
+        }),
       ],
     });
 
     await check_autocomplete({
       search: `${alias} fire`,
       searchParam: "enable-actions",
-      matches: [ makeSearchMatch(`${alias} fire`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
-                                                    searchQuery: "fire", alias, heuristic: true }),
-        { uri: histURI, title: "History entry" },
+      matches: [
+        makeSearchMatch(`${alias} fire`, {
+          engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+          searchQuery: "fire",
+          alias,
+          heuristic: true,
+        }),
       ],
     });
 
     await check_autocomplete({
       search: `${alias} mozilla`,
       searchParam: "enable-actions",
-          matches: [ makeSearchMatch(`${alias} mozilla`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
-                                                           searchQuery: "mozilla", alias, heuristic: true }) ],
+      matches: [
+        makeSearchMatch(`${alias} mozilla`, {
+          engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+          searchQuery: "mozilla",
+          alias,
+          heuristic: true,
+        }),
+      ],
     });
 
     await check_autocomplete({
       search: `${alias} MoZiLlA`,
       searchParam: "enable-actions",
-          matches: [ makeSearchMatch(`${alias} MoZiLlA`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
-                                                           searchQuery: "MoZiLlA", alias, heuristic: true }) ],
+      matches: [
+        makeSearchMatch(`${alias} MoZiLlA`, {
+          engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+          searchQuery: "MoZiLlA",
+          alias,
+          heuristic: true,
+        }),
+      ],
     });
 
     await check_autocomplete({
       search: `${alias} mozzarella mozilla`,
       searchParam: "enable-actions",
-          matches: [ makeSearchMatch(`${alias} mozzarella mozilla`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
-                                                                      searchQuery: "mozzarella mozilla", alias, heuristic: true }) ],
+      matches: [
+        makeSearchMatch(`${alias} mozzarella mozilla`, {
+          engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+          searchQuery: "mozzarella mozilla",
+          alias,
+          heuristic: true,
+        }),
+      ],
     });
   }
 
   await cleanup();
 });
+
+
+// Uses an engine that provides search suggestions.
+add_task(async function engineWithSuggestions() {
+  let engine = await addTestSuggestionsEngine();
+
+  // Use a normal alias and then one with an "@", the latter to simulate the
+  // built-in "@" engine aliases (e.g., "@google").
+  for (let alias of ["moz", "@moz"]) {
+    engine.alias = alias;
+
+    await check_autocomplete({
+      search: `${alias}`,
+      searchParam: "enable-actions",
+      matches: [
+        makeSearchMatch(alias, {
+          engineName: SUGGESTIONS_ENGINE_NAME,
+          alias,
+          searchQuery: "",
+          heuristic: true,
+        }),
+      ],
+    });
+
+    await check_autocomplete({
+      search: `${alias} `,
+      searchParam: "enable-actions",
+      matches: [
+        makeSearchMatch(`${alias} `, {
+          engineName: SUGGESTIONS_ENGINE_NAME,
+          alias,
+          searchQuery: "",
+          heuristic: true,
+        }),
+      ],
+    });
+
+    await check_autocomplete({
+      search: `${alias} fire`,
+      searchParam: "enable-actions",
+      matches: [
+        makeSearchMatch(`${alias} fire`, {
+          engineName: SUGGESTIONS_ENGINE_NAME,
+          alias,
+          searchQuery: "fire",
+          heuristic: true,
+        }),
+        makeSearchMatch(`fire foo`, {
+          engineName: SUGGESTIONS_ENGINE_NAME,
+          searchQuery: "fire",
+          searchSuggestion: "fire foo",
+        }),
+        makeSearchMatch(`fire bar`, {
+          engineName: SUGGESTIONS_ENGINE_NAME,
+          searchQuery: "fire",
+          searchSuggestion: "fire bar",
+        }),
+      ],
+    });
+  }
+
+  await cleanup();
+});
--- a/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_suggestions.js
@@ -22,37 +22,27 @@ async function cleanUpSuggestions() {
     previousSuggestionsFn = null;
   }
 }
 
 add_task(async function setup() {
   Services.prefs.setCharPref("browser.urlbar.matchBuckets", "general:5,suggestion:Infinity");
   Services.prefs.setBoolPref("browser.urlbar.geoSpecificDefaults", false);
 
-  // Set up a server that provides some suggestions by appending strings onto
-  // the search query.
-  let server = makeTestServer(SERVER_PORT);
-  server.registerPathHandler("/suggest", (req, resp) => {
-    // URL query params are x-www-form-urlencoded, which converts spaces into
-    // plus signs, so un-convert any plus signs back to spaces.
-    let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " "));
-    let suggestions = suggestionsFn(searchStr);
-    let data = [searchStr, suggestions];
-    resp.setHeader("Content-Type", "application/json", false);
-    resp.write(JSON.stringify(data));
+  let engine = await addTestSuggestionsEngine(searchStr => {
+    return suggestionsFn(searchStr);
   });
   setSuggestionsFn(searchStr => {
     let suffixes = ["foo", "bar"];
     return suffixes.map(s => searchStr + " " + s);
   });
 
   // Install the test engine.
   let oldCurrentEngine = Services.search.currentEngine;
   registerCleanupFunction(() => Services.search.currentEngine = oldCurrentEngine);
-  let engine = await addTestEngine(ENGINE_NAME, server);
   Services.search.currentEngine = engine;
 
   // 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!");