Bug 1502879 - Wrap UnifiedComplete in a Provider. r=adw
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 21 Nov 2018 15:29:44 +0000
changeset 503919 ce090b99cf416d6a8b91fb9e4467b4d389d311b1
parent 503918 df7e7d55004ddbd34b26e1eab09bb0faa7500f5d
child 503920 71901cdb0b857b65f627aa7caf9b6bd68aa0d2e1
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1502879
milestone65.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 1502879 - Wrap UnifiedComplete in a Provider. r=adw Differential Revision: https://phabricator.services.mozilla.com/D12321
browser/components/urlbar/UrlbarInput.jsm
browser/components/urlbar/UrlbarMatch.jsm
browser/components/urlbar/UrlbarProviderOpenTabs.jsm
browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
browser/components/urlbar/UrlbarProvidersManager.jsm
browser/components/urlbar/UrlbarUtils.jsm
browser/components/urlbar/UrlbarView.jsm
browser/components/urlbar/moz.build
browser/components/urlbar/tests/unit/test_providerOpenTabs.js
browser/docs/AddressBar.rst
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -12,16 +12,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   QueryContext: "resource:///modules/UrlbarController.jsm",
   Services: "resource://gre/modules/Services.jsm",
   UrlbarController: "resource:///modules/UrlbarController.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
   UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.jsm",
   UrlbarView: "resource:///modules/UrlbarView.jsm",
+  ExtensionSearchHandler: "resource://gre/modules/ExtensionSearchHandler.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper",
                                    "@mozilla.org/widget/clipboardhelper;1",
                                    "nsIClipboardHelper");
 
 /**
  * Represents the urlbar <textbox>.
@@ -267,53 +268,63 @@ class UrlbarInput {
     // BrowserUsageTelemetry.recordUrlbarSelectedResultMethod(
     //   event, this.userSelectionBehavior);
 
     let where = this._whereToOpen(event);
     let openParams = {
       postData: null,
       allowInheritPrincipal: false,
     };
-    let url = result.url;
 
     switch (result.type) {
       case UrlbarUtils.MATCH_TYPE.TAB_SWITCH: {
         // TODO: Implement handleRevert or equivalent on the input.
         // this.input.handleRevert();
-        let prevTab = this.browserWindow.gBrowser.selectedTab;
+        let prevTab = this.window.gBrowser.selectedTab;
         let loadOpts = {
           adoptIntoActiveWindow: UrlbarPrefs.get("switchTabs.adoptIntoActiveWindow"),
         };
 
-        if (this.browserWindow.switchToTabHavingURI(url, false, loadOpts) &&
+        if (this.window.switchToTabHavingURI(result.payload.url, false, loadOpts) &&
             prevTab.isEmpty) {
-          this.browserWindow.gBrowser.removeTab(prevTab);
+          this.window.gBrowser.removeTab(prevTab);
         }
         return;
 
         // TODO: How to handle meta chars?
         // Once we get here, we got a TAB_SWITCH match but the user
         // bypassed it by pressing shift/meta/ctrl. Those modifiers
         // might otherwise affect where we open - we always want to
         // open in the current tab.
         // where = "current";
-
       }
+      case UrlbarUtils.MATCH_TYPE.SEARCH:
+        // TODO: port _parseAndRecordSearchEngineLoad.
+        return;
+      case UrlbarUtils.MATCH_TYPE.OMNIBOX:
+        // Give the extension control of handling the command.
+        ExtensionSearchHandler.handleInputEntered(result.payload.keyword,
+                                                  result.payload.content,
+                                                  where);
+        return;
     }
 
-    this._loadURL(url, where, openParams);
+    this._loadURL(result.payload.url, where, openParams);
   }
 
   /**
    * Called by the view when moving through results with the keyboard.
    *
    * @param {UrlbarMatch} result The result that was selected.
    */
   setValueFromResult(result) {
-    let val = result.url;
+    // FIXME: This is wrong, not all the matches have a url. For example
+    // extension matches will call into the extension code rather than loading
+    // a url. That means we likely can't use the url as our value.
+    let val = result.payload.url;
     let uri;
     try {
       uri = Services.io.newURI(val);
     } catch (ex) {}
     if (uri) {
       val = this.window.losslessDecodeURI(uri);
     }
     this.value = val;
@@ -408,16 +419,18 @@ class UrlbarInput {
     if (!selectedVal.includes("/")) {
       let remainder = inputVal.replace(selectedVal, "");
       if (remainder != "" && remainder[0] != "/") {
         return selectedVal;
       }
     }
 
     // If the value was filled by a search suggestion, just return it.
+    // FIXME: This is wrong, the new system doesn't return action urls, it
+    // should instead build this based on MATCH_TYPE.
     let action = this._parseActionUrl(this.value);
     if (action && action.type == "searchengine") {
       return selectedVal;
     }
 
     let uri;
     if (this.getAttribute("pageproxystate") == "valid") {
       uri = this.window.gBrowser.currentURI;
@@ -506,22 +519,22 @@ class UrlbarInput {
    * @param {object} params.triggeringPrincipal
    *   The principal that the action was triggered from.
    * @param {nsIInputStream} [params.postData]
    *   The POST data associated with a search submission.
    * @param {boolean} [params.allowInheritPrincipal]
    *   If the principal may be inherited
    */
   _loadURL(url, openUILinkWhere, params) {
-    let browser = this.browserWindow.gBrowser.selectedBrowser;
+    let browser = this.window.gBrowser.selectedBrowser;
 
     // TODO: These should probably be set by the input field.
     // this.value = url;
     // browser.userTypedValue = url;
-    if (this.browserWindow.gInitialPages.includes(url)) {
+    if (this.window.gInitialPages.includes(url)) {
       browser.initialPageLoadedFromURLBar = url;
     }
     try {
       UrlbarUtils.addToUrlbarHistory(url);
     } catch (ex) {
       // Things may go wrong when adding url to session history,
       // but don't let that interfere with the loading of the url.
       Cu.reportError(ex);
@@ -530,31 +543,31 @@ class UrlbarInput {
     params.allowThirdPartyFixup = true;
 
     if (openUILinkWhere == "current") {
       params.targetBrowser = browser;
       params.indicateErrorPageLoad = true;
       params.allowPinnedTabHostChange = true;
       params.allowPopups = url.startsWith("javascript:");
     } else {
-      params.initiatingDoc = this.browserWindow.document;
+      params.initiatingDoc = this.window.document;
     }
 
     // Focus the content area before triggering loads, since if the load
     // occurs in a new tab, we want focus to be restored to the content
     // area when the current tab is re-selected.
     browser.focus();
 
     if (openUILinkWhere != "current") {
       // TODO: Implement handleRevert or equivalent on the input.
       // this.input.handleRevert();
     }
 
     try {
-      this.browserWindow.openTrustedLinkIn(url, openUILinkWhere, params);
+      this.window.openTrustedLinkIn(url, openUILinkWhere, params);
     } catch (ex) {
       // This load can throw an exception in certain cases, which means
       // we'll want to replace the URL with the loaded URL:
       if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
         // TODO: Implement handleRevert or equivalent on the input.
         // this.input.handleRevert();
       }
     }
@@ -578,29 +591,29 @@ class UrlbarInput {
       // We support using 'alt' to open in a tab, because ctrl/shift
       // might be used for canonizing URLs:
       where = event.shiftKey ? "tabshifted" : "tab";
     } else if (!isMouseEvent && this._ctrlCanonizesURLs && event && event.ctrlKey) {
       // If we're allowing canonization, and this is a key event with ctrl
       // pressed, open in current tab to allow ctrl-enter to canonize URL.
       where = "current";
     } else {
-      where = this.browserWindow.whereToOpenLink(event, false, false);
+      where = this.window.whereToOpenLink(event, false, false);
     }
     if (this.openInTab) {
       if (where == "current") {
         where = "tab";
       } else if (where == "tab") {
         where = "current";
       }
       reuseEmpty = true;
     }
     if (where == "tab" &&
         reuseEmpty &&
-        this.browserWindow.gBrowser.selectedTab.isEmpty) {
+        this.window.gBrowser.selectedTab.isEmpty) {
       where = "current";
     }
     return where;
   }
 
   // Event handlers below.
 
   _on_blur(event) {
--- a/browser/components/urlbar/UrlbarMatch.jsm
+++ b/browser/components/urlbar/UrlbarMatch.jsm
@@ -43,36 +43,40 @@ class UrlbarMatch {
     // multiple sources are involved, use the more privacy restricted.
     if (!Object.values(UrlbarUtils.MATCH_SOURCE).includes(matchSource)) {
       throw new Error("Invalid match source");
     }
     this.source = matchSource;
 
     // The payload contains match data. Some of the data is common across
     // multiple types, but most of it will vary.
-    if (!payload || (typeof payload != "object") || !payload.url) {
+    if (!payload || (typeof payload != "object")) {
       throw new Error("Invalid match payload");
     }
     this.payload = payload;
   }
 
   /**
-   * Returns a final destination for this match.
-   * Different kind of matches may have different ways to express this value,
-   * and this is a common getter for all of them.
-   * @returns {string} a url to load when this match is confirmed byt the user.
-   */
-  get url() {
-    switch (this.type) {
-      case UrlbarUtils.MATCH_TYPE.TAB_SWITCH:
-        return this.payload.url;
-    }
-    return "";
-  }
-
-  /**
    * Returns a title that could be used as a label for this match.
    * @returns {string} The label to show in a simplified title / url view.
    */
   get title() {
-    return "";
+    switch (this.type) {
+      case UrlbarUtils.MATCH_TYPE.TAB_SWITCH:
+      case UrlbarUtils.MATCH_TYPE.URL:
+      case UrlbarUtils.MATCH_TYPE.OMNIBOX:
+      case UrlbarUtils.MATCH_TYPE.REMOTE_TAB:
+        return this.payload.title || "";
+      case UrlbarUtils.MATCH_TYPE.SEARCH:
+        return this.payload.engine;
+      default:
+        return "";
+    }
+  }
+
+  /**
+   * Returns an icon url.
+   * @returns {string} url of the icon.
+   */
+  get icon() {
+    return this.payload.icon;
   }
 }
--- a/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
+++ b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
@@ -81,22 +81,26 @@ class ProviderOpenTabs {
    * @returns {string} the name of this provider.
    */
   get name() {
     return "OpenTabs";
   }
 
   /**
    * Returns the type of this provider.
-   * @returns {integer} one of the types from UrlbarProvidersManager.TYPE.*
+   * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
    */
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
   }
 
+  /**
+   * Returns the sources returned by this provider.
+   * @returns {array} one or multiple types from UrlbarUtils.MATCH_SOURCE.*
+   */
   get sources() {
     return [
       UrlbarUtils.MATCH_SOURCE.TABS,
     ];
   }
 
   /**
    * Registers a tab as open.
@@ -134,20 +138,21 @@ class ProviderOpenTabs {
   /**
    * Starts querying.
    * @param {object} queryContext The query context object
    * @param {function} addCallback Callback invoked by the provider to add a new
    *        match.
    * @returns {Promise} resolved when the query stops.
    */
   async startQuery(queryContext, addCallback) {
+    // Note: this is not actually expected to be used as an internal provider,
+    // because normal history search will already coalesce with the open tabs
+    // temp table to return proper frecency.
     // TODO:
     //  * properly search and handle tokens, this is just a mock for now.
-    //  * we won't search openTabs like this usually, the history search will
-    //  * coalesce the temp table with its data.
     logger.info(`Starting query for ${queryContext.searchString}`);
     let instance = {};
     this.queries.set(queryContext, instance);
     let conn = await this.promiseDb();
     await conn.executeCached(`
       SELECT url, userContextId
       FROM moz_openpages_temp
     `, {}, (row, cancel) => {
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
@@ -0,0 +1,301 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * This module exports a provider that wraps the existing UnifiedComplete
+ * component, it is supposed to be used as an interim solution while we rewrite
+ * the model providers in a more modular way.
+ */
+
+var EXPORTED_SYMBOLS = ["UrlbarProviderUnifiedComplete"];
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Log: "resource://gre/modules/Log.jsm",
+  PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
+  UrlbarMatch: "resource:///modules/UrlbarMatch.jsm",
+  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "unifiedComplete",
+  "@mozilla.org/autocomplete/search;1?name=unifiedcomplete",
+  "nsIAutoCompleteSearch");
+
+XPCOMUtils.defineLazyGetter(this, "logger",
+  () => Log.repository.getLogger("Places.Urlbar.Provider.UnifiedComplete"));
+
+// See UnifiedComplete.
+const TITLE_TAGS_SEPARATOR = " \u2013 ";
+
+/**
+ * Class used to create the provider.
+ */
+class ProviderUnifiedComplete {
+  constructor() {
+    // Maps the running queries by queryContext.
+    this.queries = new Map();
+  }
+
+  /**
+   * Returns the name of this provider.
+   * @returns {string} the name of this provider.
+   */
+  get name() {
+    return "UnifiedComplete";
+  }
+
+  /**
+   * Returns the type of this provider.
+   * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
+   */
+  get type() {
+    return UrlbarUtils.PROVIDER_TYPE.IMMEDIATE;
+  }
+
+  /**
+   * Returns the sources returned by this provider.
+   * @returns {array} one or multiple types from UrlbarUtils.MATCH_SOURCE.*
+   */
+  get sources() {
+    return [
+      UrlbarUtils.MATCH_SOURCE.TABS,
+    ];
+  }
+
+  /**
+   * Starts querying.
+   * @param {object} queryContext The query context object
+   * @param {function} addCallback Callback invoked by the provider to add a new
+   *        match.
+   * @returns {Promise} resolved when the query stops.
+   */
+  async startQuery(queryContext, addCallback) {
+    logger.info(`Starting query for ${queryContext.searchString}`);
+    let instance = {};
+    this.queries.set(queryContext, instance);
+
+    // Supported search params are:
+    //  * "enable-actions": default to true.
+    //  * "disable-private-actions": set for private windows, if not in permanent
+    //    private browsing mode. ()
+    //  * "private-window": the search is taking place in a private window.
+    //  * "user-context-id:#": the userContextId to use.
+    let params = ["enable-actions"];
+    if (queryContext.isPrivate) {
+      params.push("private-window");
+      if (!PrivateBrowsingUtils.permanentPrivateBrowsing) {
+        params.push("disable-private-actions");
+      }
+    }
+    if (queryContext.userContextId) {
+      params.push(`user-context-id:${queryContext.userContextId}}`);
+    }
+
+    let urls = new Set();
+    await new Promise(resolve => {
+      let listener = {
+        onSearchResult(_, result) {
+          let {done, matches} = convertResultToMatches(queryContext, result, urls);
+          for (let match of matches) {
+            addCallback(this, match);
+          }
+          if (done) {
+            resolve();
+          }
+        },
+      };
+      unifiedComplete.startSearch(queryContext.searchString,
+                                  params.join(" "),
+                                  null, // previousResult
+                                  listener);
+    });
+
+    // We are done.
+    this.queries.delete(queryContext);
+  }
+
+  /**
+   * Cancels a running query.
+   * @param {object} queryContext The query context object
+   */
+  cancelQuery(queryContext) {
+    logger.info(`Canceling query for ${queryContext.searchString}`);
+    // This doesn't properly support being used concurrently by multiple fields.
+    this.queries.delete(queryContext);
+    unifiedComplete.stopSearch();
+  }
+}
+
+var UrlbarProviderUnifiedComplete = new ProviderUnifiedComplete();
+
+/**
+ * Convert from a nsIAutocompleteResult to a list of new matches.
+ * Note that at every call we get the full set of matches, included the
+ * previously returned ones, and new matches may be inserted in the middle.
+ * This means we could sort these wrongly, the muxer should take care of it.
+ * In any case at least we're sure there's just one heuristic result and it
+ * comes first.
+ *
+ * @param {object} context the QueryContext
+ * @param {object} result an nsIAutocompleteResult
+ * @param {set} urls a Set containing all the found urls, used to discard
+ *        already added matches.
+ * @returns {object} { matches: {array}, done: {boolean} }
+ */
+function convertResultToMatches(context, result, urls) {
+  let matches = [];
+  let done = [
+    Ci.nsIAutoCompleteResult.RESULT_IGNORED,
+    Ci.nsIAutoCompleteResult.RESULT_FAILURE,
+    Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
+    Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
+  ].includes(result.searchResult) || result.errorDescription;
+
+  for (let i = 0; i < result.matchCount; ++i) {
+    // First, let's check if we already added this match.
+    // nsIAutocompleteResult always contains all of the matches, includes ones
+    // we may have added already. This means we'll end up adding things in the
+    // wrong order here, but that's a task for the UrlbarMuxer.
+    let url = result.getFinalCompleteValueAt(i);
+    if (urls.has(url)) {
+      continue;
+    }
+    urls.add(url);
+    // Not used yet: result.getValueAt(i), result.getLabelAt(i)
+    let style = result.getStyleAt(i);
+    let match = makeUrlbarMatch({
+      url,
+      icon: result.getImageAt(i),
+      style,
+      comment: result.getCommentAt(i),
+      firstToken: context.tokens[0],
+    });
+    // Should not happen, but better safe than sorry.
+    if (!match) {
+      continue;
+    }
+    matches.push(match);
+    // Manage autofill and preselected properties for the first match.
+    if (i == 0 && result.defaultIndex == 0) {
+      if (style.includes("autofill")) {
+        context.autofill = true;
+      }
+      if (style.includes("heuristic")) {
+        context.preselected = true;
+      }
+    }
+  }
+  return {matches, done};
+}
+
+/**
+ * Creates a new UrlbarMatch from the provided data.
+ * @param {object} info includes properties from the legacy match.
+ * @returns {object} an UrlbarMatch
+ */
+function makeUrlbarMatch(info) {
+  let action = PlacesUtils.parseActionUrl(info.url);
+  if (action) {
+    switch (action.type) {
+      case "searchengine":
+        return new UrlbarMatch(
+          UrlbarUtils.MATCH_TYPE.SEARCH,
+          UrlbarUtils.MATCH_SOURCE.SEARCH,
+          {
+            engine: action.params.engineName,
+            suggestion: action.params.searchSuggestion,
+            keyword: action.params.alias,
+            query: action.params.searchQuery,
+            icon: info.icon,
+          }
+        );
+      case "keyword":
+        return new UrlbarMatch(
+          UrlbarUtils.MATCH_TYPE.KEYWORD,
+          UrlbarUtils.MATCH_SOURCE.BOOKMARKS,
+          {
+            url: action.params.url,
+            keyword: info.firstToken,
+            postData: action.params.postData,
+            icon: info.icon,
+          }
+        );
+      case "extension":
+        return new UrlbarMatch(
+          UrlbarUtils.MATCH_TYPE.OMNIBOX,
+          UrlbarUtils.MATCH_SOURCE.OTHER_NETWORK,
+          {
+            title: info.comment,
+            content: action.params.content,
+            keyword: action.params.keyword,
+            icon: info.icon,
+          }
+        );
+      case "remotetab":
+        return new UrlbarMatch(
+          UrlbarUtils.MATCH_TYPE.REMOTE_TAB,
+          UrlbarUtils.MATCH_SOURCE.TABS,
+          {
+            url: action.params.url,
+            title: info.comment,
+            device: action.params.deviceName,
+            icon: info.icon,
+          }
+        );
+      case "visiturl":
+        return new UrlbarMatch(
+          UrlbarUtils.MATCH_TYPE.URL,
+          UrlbarUtils.MATCH_SOURCE.OTHER_LOCAL,
+          {
+            title: info.comment,
+            url: action.params.url,
+            icon: info.icon,
+          }
+        );
+      default:
+        Cu.reportError("Unexpected action type");
+        return null;
+    }
+  }
+
+  if (info.style.includes("priority-search")) {
+    return new UrlbarMatch(
+      UrlbarUtils.MATCH_TYPE.SEARCH,
+      UrlbarUtils.MATCH_SOURCE.SEARCH,
+      {
+        engine: info.comment,
+        icon: info.icon,
+      }
+    );
+  }
+
+  // This is a normal url/title tuple.
+  let source, tags, comment;
+  let hasTags = info.style.includes("tag");
+  if (info.style.includes("bookmark") || hasTags) {
+    source = UrlbarUtils.MATCH_SOURCE.BOOKMARKS;
+    if (info.style.includes("tag")) {
+      // Split title and tags.
+      [comment, tags] = info.comment.split(TITLE_TAGS_SEPARATOR);
+      tags = tags.split(",").map(t => t.trim());
+    }
+  } else if (info.style.includes("preloaded-top-sites")) {
+    source = UrlbarUtils.MATCH_SOURCE.OTHER_LOCAL;
+  } else {
+    source = UrlbarUtils.MATCH_SOURCE.HISTORY;
+  }
+  return new UrlbarMatch(
+    UrlbarUtils.MATCH_TYPE.URL,
+    source,
+    {
+      url: info.url,
+      icon: info.icon,
+      title: comment,
+      tags,
+    }
+  );
+}
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -21,17 +21,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
 });
 
 XPCOMUtils.defineLazyGetter(this, "logger", () =>
   Log.repository.getLogger("Places.Urlbar.ProvidersManager"));
 
 // List of available local providers, each is implemented in its own jsm module
 // and will track different queries internally by queryContext.
 var localProviderModules = {
-  UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
+  UrlbarProviderUnifiedComplete: "resource:///modules/UrlbarProviderUnifiedComplete.jsm",
 };
 
 // To improve dataflow and reduce UI work, when a match is added by a
 // non-immediate provider, we notify it to the controller after a delay, so
 // that we can chunk matches coming in that timeframe into a single call.
 const CHUNK_MATCHES_DELAY_MS = 16;
 
 /**
@@ -176,17 +176,17 @@ class Query {
     logger.debug(`Acceptable sources ${this.acceptableSources}`);
 
     let promises = [];
     for (let provider of this.providers.get(UrlbarUtils.PROVIDER_TYPE.IMMEDIATE).values()) {
       if (this.canceled) {
         break;
       }
       if (this._providerHasAcceptableSources(provider)) {
-        promises.push(provider.startQuery(this.context, this.add));
+        promises.push(provider.startQuery(this.context, this.add.bind(this)));
       }
     }
 
     // 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(() => {}, UrlbarPrefs.get("delay"));
     await this._sleepTimer.promise;
@@ -249,17 +249,17 @@ class Query {
   add(provider, match) {
     // Stop returning results as soon as we've been canceled.
     if (this.canceled || !this.acceptableSources.includes(match.source)) {
       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 (match.url.startsWith("javascript:") &&
+    if (match.payload.url && match.payload.url.startsWith("javascript:") &&
         !this.context.searchString.startsWith("javascript:") &&
         UrlbarPrefs.get("filter.javascript")) {
       return;
     }
 
     this.context.results.push(match);
 
     let notifyResults = () => {
@@ -376,17 +376,17 @@ function getAcceptableMatchSources(conte
         break;
       case UrlbarUtils.MATCH_SOURCE.HISTORY:
         if (UrlbarPrefs.get("suggest.history") &&
             (!restrictTokenType ||
              restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_HISTORY)) {
           acceptedSources.push(source);
         }
         break;
-      case UrlbarUtils.MATCH_SOURCE.SEARCHENGINE:
+      case UrlbarUtils.MATCH_SOURCE.SEARCH:
         if (UrlbarPrefs.get("suggest.searches") &&
             (!restrictTokenType ||
              restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_SEARCH)) {
           acceptedSources.push(source);
         }
         break;
       case UrlbarUtils.MATCH_SOURCE.TABS:
         if (UrlbarPrefs.get("suggest.openpage") &&
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -57,29 +57,44 @@ var UrlbarUtils = {
     // Can be delayed, contains results coming from the network.
     NETWORK: 3,
     // Can be delayed, contains results coming from unknown sources.
     EXTENSION: 4,
   },
 
   // Defines UrlbarMatch types.
   MATCH_TYPE: {
-    // Indicates an open tab.
-    // The payload is: { url, userContextId }
+    // An open tab.
+    // Payload: { icon, url, userContextId }
     TAB_SWITCH: 1,
+    // A search suggestion or engine.
+    // Payload: { icon, suggestion, keyword, query }
+    SEARCH: 2,
+    // A common url/title tuple, may be a bookmark with tags.
+    // Payload: { icon, url, title, tags }
+    URL: 3,
+    // A bookmark keyword.
+    // Payload: { icon, url, keyword, postData }
+    KEYWORD: 4,
+    // A WebExtension Omnibox match.
+    // Payload: { icon, keyword, title, content }
+    OMNIBOX: 5,
+    // A tab from another synced device.
+    // Payload: { url, icon, device, title }
+    REMOTE_TAB: 6,
   },
 
   // This defines the source of matches returned by a provider. Each provider
   // can return matches from more than one source. This is used by the
   // ProvidersManager to decide which providers must be queried and which
   // matches can be returned.
   MATCH_SOURCE: {
     BOOKMARKS: 1,
     HISTORY: 2,
-    SEARCHENGINE: 3,
+    SEARCH: 3,
     TABS: 4,
     OTHER_LOCAL: 5,
     OTHER_NETWORK: 6,
   },
 
   /**
    * Adds a url to history as long as it isn't in a private browsing window,
    * and it is valid.
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -141,27 +141,27 @@ class UrlbarView {
     content.appendChild(actionIcon);
 
     let favicon = this._createElement("span");
     favicon.className = "urlbarView-favicon";
     content.appendChild(favicon);
 
     let title = this._createElement("span");
     title.className = "urlbarView-title";
-    title.textContent = result.title || result.url;
+    title.textContent = result.title || result.payload.url;
     content.appendChild(title);
 
     let secondary = this._createElement("span");
     secondary.className = "urlbarView-secondary";
     if (result.type == UrlbarUtils.MATCH_TYPE.TAB_SWITCH) {
       secondary.classList.add("urlbarView-action");
       secondary.textContent = "Switch to Tab";
     } else {
       secondary.classList.add("urlbarView-url");
-      secondary.textContent = result.url;
+      secondary.textContent = result.payload.url;
     }
     content.appendChild(secondary);
 
     this._rows.appendChild(item);
   }
 
   /**
    * Passes DOM events for the view to the _on_<event type> methods.
--- a/browser/components/urlbar/moz.build
+++ b/browser/components/urlbar/moz.build
@@ -7,16 +7,17 @@ with Files("**"):
 
 EXTRA_JS_MODULES += [
     'UrlbarController.jsm',
     'UrlbarInput.jsm',
     'UrlbarMatch.jsm',
     'UrlbarPrefs.jsm',
     'UrlbarProviderOpenTabs.jsm',
     'UrlbarProvidersManager.jsm',
+    'UrlbarProviderUnifiedComplete.jsm',
     'UrlbarTokenizer.jsm',
     'UrlbarUtils.jsm',
     'UrlbarValueFormatter.jsm',
     'UrlbarView.jsm',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
--- a/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
+++ b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js
@@ -16,17 +16,17 @@ add_task(async function test_openTabs() 
 
   let context = createContext();
   let matchCount = 0;
   let callback = function(provider, match) {
     matchCount++;
     Assert.equal(provider, UrlbarProviderOpenTabs, "Got the expected provider");
     Assert.equal(match.type, UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
                  "Got the expected match type");
-    Assert.equal(match.url, url, "Got the expected url");
+    Assert.equal(match.payload.url, url, "Got the expected url");
     Assert.equal(match.title, "", "Got the expected title");
   };
 
   await UrlbarProviderOpenTabs.startQuery(context, callback);
   Assert.equal(matchCount, 1, "Found the expected number of matches");
   // Sanity check that this doesn't throw.
   UrlbarProviderOpenTabs.cancelQuery(context);
   Assert.equal(UrlbarProviderOpenTabs.queries.size, 0,
--- a/browser/docs/AddressBar.rst
+++ b/browser/docs/AddressBar.rst
@@ -64,16 +64,18 @@ It is augmented as it progresses through
                 // View and the Controller can do additional filtering.
     isPrivate; // {boolean} Whether the search started in a private context.
     userContextId; // {integer} The user context ID (containers feature).
 
     // Properties added by the Model.
     tokens; // {array} tokens extracted from the searchString, each token is an
             // object in the form {type, value}.
     results; // {array} list of UrlbarMatch objects.
+    preselected; // {boolean} whether the first match should be preselected.
+    autofill; // {boolean} whether the first match is an autofill match.
   }
 
 
 The Model
 =========
 
 The *Model* is the component responsible for retrieving search results based on
 the user's input, and sorting them accordingly to their importance.
@@ -295,23 +297,40 @@ properties, supported by all of the matc
   Match types are also enumerated by *UrlbarUtils.MATCH_TYPE*.
 
 .. highlight:: JavaScript
 .. code::
 
   UrlbarMatch {
     constructor(matchType, payload);
 
-    // Common properties:
     type: {integer} One of UrlbarUtils.MATCH_TYPE.
     source: {integer} One of UrlbarUtils.MATCH_SOURCE.
-    url: {string} The url pointed by this match.
     title: {string} A title that may be used as a label for this match.
+    icon: {string} Url of an icon for this match.
+    payload: {object} Object containing properties for the specific MATCH_TYPE.
   }
 
+The following MATCH_TYPEs are supported:
+
+.. highlight:: JavaScript
+.. code::
+
+    // Payload: { icon, url, userContextId }
+    TAB_SWITCH: 1,
+    // Payload: { icon, suggestion, keyword, query }
+    SEARCH: 2,
+    // Payload: { icon, url, title, tags }
+    URL: 3,
+    // Payload: { icon, url, keyword, postData }
+    KEYWORD: 4,
+    // Payload: { icon, keyword, title, content }
+    OMNIBOX: 5,
+    // Payload: { icon, url, device, title }
+    REMOTE_TAB: 6,
 
 Shared Modules
 ==============
 
 Various modules provide shared utilities to the other components:
 
 `UrlbarPrefs.jsm <https://dxr.mozilla.org/mozilla-central/source/browser/components/urlbar/UrlbarPrefs.jsm>`_
 ----------------