Bug 1502385 - Filter matches and providers in the Quantum Bar manager. r=adw
authorMarco Bonardo <mbonardo@mozilla.com>
Mon, 05 Nov 2018 21:54:09 +0000
changeset 444487 e36fa00c208967777b85d23194bc1900ca654cb1
parent 444486 10d5db1fc33fa0ef81202e9a6fdd1771a85c1af9
child 444488 f6f47d5b05b68597739e059cc707573a3d1cea01
push id34996
push userrgurzau@mozilla.com
push dateTue, 06 Nov 2018 09:53:23 +0000
treeherdermozilla-central@e160f0a60e4f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1502385
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 1502385 - Filter matches and providers in the Quantum Bar manager. r=adw Differential Revision: https://phabricator.services.mozilla.com/D10348
browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
browser/base/content/test/urlbar/browser_urlbarDecode.js
browser/components/urlbar/UrlbarController.jsm
browser/components/urlbar/UrlbarMatch.jsm
browser/components/urlbar/UrlbarProviderOpenTabs.jsm
browser/components/urlbar/UrlbarProvidersManager.jsm
browser/components/urlbar/UrlbarUtils.jsm
browser/components/urlbar/UrlbarView.jsm
browser/components/urlbar/tests/browser/browser_UrlbarController_resultOpening.js
browser/components/urlbar/tests/unit/head.js
browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
browser/components/urlbar/tests/unit/test_providersManager.js
browser/components/urlbar/tests/unit/test_providersManager_filtering.js
browser/components/urlbar/tests/unit/xpcshell.ini
browser/docs/AddressBar.rst
--- a/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
+++ b/browser/base/content/test/urlbar/browser_autocomplete_enter_race.js
@@ -56,17 +56,17 @@ add_task(taskWithNewTab(async function t
 
 add_task(taskWithNewTab(async function test_disabled_ac() {
   // Disable autocomplete.
   let suggestHistory = Preferences.get("browser.urlbar.suggest.history");
   Preferences.set("browser.urlbar.suggest.history", false);
   let suggestBookmarks = Preferences.get("browser.urlbar.suggest.bookmark");
   Preferences.set("browser.urlbar.suggest.bookmark", false);
   let suggestOpenPages = Preferences.get("browser.urlbar.suggest.openpage");
-  Preferences.set("browser.urlbar.suggest.openpages", false);
+  Preferences.set("browser.urlbar.suggest.openpage", false);
 
   Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
                                        "http://example.com/?q={searchTerms}");
   let engine = Services.search.getEngineByName("MozSearch");
   let originalEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
 
   function cleanup() {
--- a/browser/base/content/test/urlbar/browser_urlbarDecode.js
+++ b/browser/base/content/test/urlbar/browser_urlbarDecode.js
@@ -28,17 +28,19 @@ add_task(async function injectJSON() {
   gURLBar.handleRevert();
   gURLBar.blur();
 });
 
 add_task(function losslessDecode() {
   let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa";
   let url = "http://" + urlNoScheme;
   if (Services.prefs.getBoolPref("browser.urlbar.quantumbar", true)) {
-    const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, {url});
+    const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                                   UrlbarUtils.MATCH_SOURCE.TABS,
+                                   { url });
     gURLBar.setValueFromResult(result);
   } else {
     gURLBar.textValue = url;
   }
   // Since this is directly setting textValue, it is expected to be trimmed.
   Assert.equal(gURLBar.inputField.value, urlNoScheme,
                "The string displayed in the textbox should not be escaped");
   gURLBar.value = "";
--- a/browser/components/urlbar/UrlbarController.jsm
+++ b/browser/components/urlbar/UrlbarController.jsm
@@ -74,16 +74,17 @@ class QueryContext {
  * results and returns them to the UI for display.
  *
  * Listeners may be added to listen for the results. They must support the
  * following methods which may be called when a query is run:
  *
  * - onQueryStarted(queryContext)
  * - onQueryResults(queryContext)
  * - onQueryCancelled(queryContext)
+ * - onQueryFinished(queryContext)
  */
 class UrlbarController {
   /**
    * Initialises the class. The manager may be overridden here, this is for
    * test purposes.
    *
    * @param {object} options
    *   The initial options for UrlbarController.
@@ -114,16 +115,18 @@ class UrlbarController {
    * @param {QueryContext} queryContext The query details.
    */
   async startQuery(queryContext) {
     queryContext.autoFill = Services.prefs.getBoolPref("browser.urlbar.autoFill", true);
 
     this._notify("onQueryStarted", queryContext);
 
     await this.manager.startQuery(queryContext, this);
+
+    this._notify("onQueryFinished", queryContext);
   }
 
   /**
    * Cancels an in-progress query. Note, queries may continue running if they
    * can't be canceled.
    *
    * @param {QueryContext} queryContext The query details.
    */
--- a/browser/components/urlbar/UrlbarMatch.jsm
+++ b/browser/components/urlbar/UrlbarMatch.jsm
@@ -20,23 +20,42 @@ XPCOMUtils.defineLazyModuleGetters(this,
 });
 
 /**
  * Class used to create a match.
  */
 class UrlbarMatch {
   /**
    * Creates a match.
-   * @param {integer} matchType one of UrlbarUtils.MATCHTYPE.* values
+   * @param {integer} matchType one of UrlbarUtils.MATCH_TYPE.* values
+   * @param {integer} matchSource one of UrlbarUtils.MATCH_SOURCE.* values
    * @param {object} payload data for this match. A payload should always
    *        contain a way to extract a final url to visit. The url getter
    *        should have a case for each of the types.
    */
-  constructor(matchType, payload) {
+  constructor(matchType, matchSource, payload) {
+    // Type describes the payload and visualization that should be used for
+    // this match.
+    if (!Object.values(UrlbarUtils.MATCH_TYPE).includes(matchType)) {
+      throw new Error("Invalid match type");
+    }
     this.type = matchType;
+
+    // Source describes which data has been used to derive this match. In case
+    // 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) {
+      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.
--- a/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
+++ b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
@@ -87,16 +87,22 @@ class ProviderOpenTabs {
   /**
    * Returns the type of this provider.
    * @returns {integer} one of the types from UrlbarProvidersManager.TYPE.*
    */
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
   }
 
+  get sources() {
+    return [
+      UrlbarUtils.MATCH_SOURCE.TABS,
+    ];
+  }
+
   /**
    * Registers a tab as open.
    * @param {string} url Address of the tab
    * @param {integer} userContextId Containers user context id
    */
   registerOpenTab(url, userContextId = 0) {
     if (!this.openTabs.has(userContextId)) {
       this.openTabs.set(userContextId, []);
@@ -144,17 +150,18 @@ class ProviderOpenTabs {
     await conn.executeCached(`
       SELECT url, userContextId
       FROM moz_openpages_temp
     `, {}, (row, cancel) => {
       if (!this.queries.has(queryContext)) {
         cancel();
         return;
       }
-      addCallback(this, new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, {
+      addCallback(this, new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                                        UrlbarUtils.MATCH_SOURCE.TABS, {
         url: row.getResultByName("url"),
         userContextId: row.getResultByName("userContextId"),
       }));
     });
     // We are done.
     this.queries.delete(queryContext);
   }
 
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -151,58 +151,72 @@ class Query {
   constructor(queryContext, controller, providers) {
     this.context = queryContext;
     this.context.results = [];
     this.controller = controller;
     this.providers = providers;
     this.started = false;
     this.canceled = false;
     this.complete = false;
+    // Array of acceptable MATCH_SOURCE values for this query. Providers not
+    // returning any of these will be skipped, as well as matches not part of
+    // this subset (Note we still expect the provider to do its own internal
+    // filtering, our additional filtering will be for sanity).
+    this.acceptableSources = [];
   }
 
   /**
    * Starts querying.
    */
   async start() {
     if (this.started) {
       throw new Error("This Query has been started already");
     }
     this.started = true;
     UrlbarTokenizer.tokenize(this.context);
+    this.acceptableSources = getAcceptableMatchSources(this.context);
+    logger.debug(`Acceptable sources ${this.acceptableSources}`);
 
     let promises = [];
     for (let provider of this.providers.get(UrlbarUtils.PROVIDER_TYPE.IMMEDIATE).values()) {
       if (this.canceled) {
         break;
       }
-      promises.push(provider.startQuery(this.context, this.add));
+      if (this._providerHasAcceptableSources(provider)) {
+        promises.push(provider.startQuery(this.context, this.add));
+      }
     }
 
     // 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;
 
     for (let providerType of [UrlbarUtils.PROVIDER_TYPE.NETWORK,
                               UrlbarUtils.PROVIDER_TYPE.PROFILE,
                               UrlbarUtils.PROVIDER_TYPE.EXTENSION]) {
       for (let provider of this.providers.get(providerType).values()) {
         if (this.canceled) {
           break;
         }
-        promises.push(provider.startQuery(this.context, this.add.bind(this)));
+        if (this._providerHasAcceptableSources(provider)) {
+          promises.push(provider.startQuery(this.context, this.add.bind(this)));
+        }
       }
     }
 
-    await Promise.all(promises.map(p => p.catch(Cu.reportError)));
+    logger.info(`Queried ${promises.length} providers`);
+    if (promises.length) {
+      await Promise.all(promises.map(p => p.catch(Cu.reportError)));
 
-    if (this._chunkTimer) {
-      // All the providers are done returning results, so we can stop chunking.
-      await this._chunkTimer.fire();
+      if (this._chunkTimer) {
+        // All the providers are done returning results, so we can stop chunking.
+        await this._chunkTimer.fire();
+      }
     }
 
     // Nothing should be failing above, since we catch all the promises, thus
     // this is not in a finally for now.
     this.complete = true;
   }
 
   /**
@@ -229,22 +243,22 @@ class Query {
 
   /**
    * Adds a match returned from a provider to the results set.
    * @param {object} provider
    * @param {object} match
    */
   add(provider, match) {
     // Stop returning results as soon as we've been canceled.
-    if (this.canceled) {
+    if (this.canceled || !this.acceptableSources.includes(match.source)) {
       return;
     }
+
     this.context.results.push(match);
 
-
     let notifyResults = () => {
       if (this._chunkTimer) {
         this._chunkTimer.cancel().catch(Cu.reportError);
         delete this._chunkTimer;
       }
       // TODO:
       //  * pass results to a muxer before sending them back to the controller.
       this.controller.receiveResults(this.context);
@@ -253,16 +267,25 @@ class Query {
     // If the provider is not of immediate type, chunk results, to improve the
     // dataflow and reduce UI flicker.
     if (provider.type == UrlbarUtils.PROVIDER_TYPE.IMMEDIATE) {
       notifyResults();
     } else if (!this._chunkTimer) {
       this._chunkTimer = new SkippableTimer(notifyResults, CHUNK_MATCHES_DELAY_MS);
     }
   }
+
+  /**
+   * Returns whether a provider's sources are acceptable for this query.
+   * @param {object} provider A provider object.
+   * @returns {boolean}whether the provider sources are acceptable.
+   */
+  _providerHasAcceptableSources(provider) {
+    return provider.sources.some(s => this.acceptableSources.includes(s));
+  }
 }
 
 /**
  * Class used to create a timer that can be manually fired, to immediately
  * invoke the callback, or canceled, as necessary.
  * Examples:
  *   let timer = new SkippableTimer();
  *   // Invokes the callback immediately without waiting for the delay.
@@ -312,8 +335,68 @@ class SkippableTimer {
    */
   cancel() {
     logger.debug(`Canceling timer for ${this._timer.delay}ms`);
     this._timer.cancel();
     delete this._timer;
     return this.fire();
   }
 }
+
+/**
+ * Gets an array of the provider sources accepted for a given QueryContext.
+ * @param {object} context The QueryContext to examine
+ * @returns {array} Array of accepted sources
+ */
+function getAcceptableMatchSources(context) {
+  let acceptedSources = [];
+  // There can be only one restrict token about sources.
+  let restrictToken = context.tokens.find(t => [ UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
+                                                 UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
+                                                 UrlbarTokenizer.TYPE.RESTRICT_TAG,
+                                                 UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE,
+                                                 UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
+                                               ].includes(t.type));
+  let restrictTokenType = restrictToken ? restrictToken.type : undefined;
+  for (let source of Object.values(UrlbarUtils.MATCH_SOURCE)) {
+    switch (source) {
+      case UrlbarUtils.MATCH_SOURCE.BOOKMARKS:
+        if (UrlbarPrefs.get("suggest.bookmark") &&
+            (!restrictTokenType ||
+             restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK ||
+             restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_TAG)) {
+          acceptedSources.push(source);
+        }
+        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:
+        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") &&
+            (!restrictTokenType ||
+             restrictTokenType === UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE)) {
+          acceptedSources.push(source);
+        }
+        break;
+      case UrlbarUtils.MATCH_SOURCE.OTHER_NETWORK:
+        if (!context.isPrivate) {
+          acceptedSources.push(source);
+        }
+        break;
+      case UrlbarUtils.MATCH_SOURCE.OTHER_LOCAL:
+      default:
+        acceptedSources.push(source);
+        break;
+    }
+  }
+  return acceptedSources;
+}
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -62,16 +62,29 @@ var UrlbarUtils = {
 
   // Defines UrlbarMatch types.
   MATCH_TYPE: {
     // Indicates an open tab.
     // The payload is: { url, userContextId }
     TAB_SWITCH: 1,
   },
 
+  // 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,
+    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.
    *
    * @param {string} url The url to add to history.
    * @param {nsIDomWindow} window The window from where the url is being added.
    */
   addToUrlbarHistory(url, window) {
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -78,16 +78,24 @@ class UrlbarView {
     this.panel.hidePopup();
   }
 
   // UrlbarController listener methods.
   onQueryStarted(queryContext) {
     this._rows.textContent = "";
   }
 
+  onQueryCancelled(queryContext) {
+    // Nothing.
+  }
+
+  onQueryFinished(queryContext) {
+    // Nothing.
+  }
+
   onQueryResults(queryContext) {
     // XXX For now, clear the results for each set received. We should really
     // be updating the existing list.
     this._rows.textContent = "";
     this._queryContext = queryContext;
     for (let resultIndex in queryContext.results) {
       this._addRow(resultIndex);
     }
--- a/browser/components/urlbar/tests/browser/browser_UrlbarController_resultOpening.js
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarController_resultOpening.js
@@ -52,17 +52,19 @@ add_task(function test_handleEnteredText
 
 add_task(function test_resultSelected_switchtab() {
   sandbox.stub(window, "switchToTabHavingURI").returns(true);
   sandbox.stub(window.gBrowser.selectedTab, "isEmpty").returns(false);
   sandbox.stub(window.gBrowser, "removeTab");
 
   const event = new MouseEvent("click", {button: 0});
   const url = "https://example.com/1";
-  const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, {url});
+  const result = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                                 UrlbarUtils.MATCH_SOURCE.TABS,
+                                 { url });
 
   Assert.equal(gURLBar.value, "", "urlbar input is empty before selecting a result");
   if (Services.prefs.getBoolPref("browser.urlbar.quantumbar", true)) {
     gURLBar.resultSelected(event, result);
     Assert.equal(gURLBar.value, url, "urlbar value updated for selected result");
   } else {
     controller.resultSelected(event, result);
   }
--- a/browser/components/urlbar/tests/unit/head.js
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -49,37 +49,41 @@ function createContext(searchString = "f
   });
 }
 
 /**
  * Waits for the given notification from the supplied controller.
  *
  * @param {UrlbarController} controller The controller to wait for a response from.
  * @param {string} notification The name of the notification to wait for.
+ * @param {boolean} expected Wether the notification is expected.
  * @returns {Promise} A promise that is resolved with the arguments supplied to
  *   the notification.
  */
-function promiseControllerNotification(controller, notification) {
-  return new Promise(resolve => {
+function promiseControllerNotification(controller, notification, expected = true) {
+  return new Promise((resolve, reject) => {
     let proxifiedObserver = new Proxy({}, {
       get: (target, name) => {
         if (name == notification) {
           return (...args) => {
             controller.removeQueryListener(proxifiedObserver);
-            resolve(args);
+            if (expected) {
+              resolve(args);
+            } else {
+              reject();
+            }
           };
         }
         return () => false;
       },
     });
     controller.addQueryListener(proxifiedObserver);
   });
 }
 
-
 /**
  * Helper function to clear the existing providers and register a basic provider
  * that returns only the results given.
  *
  * @param {array} results The results for the provider to return.
  * @param {function} [cancelCallback] Optional, called when the query provider
  *                                    receives a cancel instruction.
  */
@@ -96,16 +100,19 @@ function registerBasicTestProvider(resul
   }
   UrlbarProvidersManager.registerProvider({
     get name() {
       return "TestProvider";
     },
     get type() {
       return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     },
+    get sources() {
+      return results.map(r => r.source);
+    },
     async startQuery(context, add) {
       Assert.ok(context, "context is passed-in");
       Assert.equal(typeof add, "function", "add is a callback");
       this._context = context;
       for (const result of results) {
         add(this, result);
       }
     },
--- a/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js
@@ -5,17 +5,19 @@
  * These tests test the UrlbarController in association with the model.
  */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 
 const TEST_URL = "http://example.com";
-const match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, { url: TEST_URL });
+const match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                              UrlbarUtils.MATCH_SOURCE.TABS,
+                              { url: TEST_URL });
 let controller;
 
 /**
  * Asserts that the query context has the expected values.
  *
  * @param {QueryContext} context
  * @param {object} expectedValues The expected values for the QueryContext.
  */
--- a/browser/components/urlbar/tests/unit/test_providersManager.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager.js
@@ -1,15 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 add_task(async function test_providers() {
-  let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH, { url: "http://mozilla.org/foo/" });
+  let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                              UrlbarUtils.MATCH_SOURCE.TABS,
+                              { url: "http://mozilla.org/foo/" });
   registerBasicTestProvider([match]);
 
   let context = createContext();
   let controller = new UrlbarController({
     browserWindow: {
       location: {
         href: AppConstants.BROWSER_CHROME_URL,
       },
new file mode 100644
--- /dev/null
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_providers() {
+  let match = new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                              UrlbarUtils.MATCH_SOURCE.TABS,
+                              { url: "http://mozilla.org/foo/" });
+  registerBasicTestProvider([match]);
+
+  let context = createContext();
+  let controller = new UrlbarController({
+    browserWindow: {
+      location: {
+        href: AppConstants.BROWSER_CHROME_URL,
+      },
+    },
+  });
+
+  info("Disable the only available source, should get no matches");
+  Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+  let promise = Promise.race([
+    promiseControllerNotification(controller, "onQueryResults", false),
+    promiseControllerNotification(controller, "onQueryFinished"),
+  ]);
+  await controller.startQuery(context);
+  await promise;
+  Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
+
+  let matches = [
+    match,
+    new UrlbarMatch(UrlbarUtils.MATCH_TYPE.TAB_SWITCH,
+                    UrlbarUtils.MATCH_SOURCE.HISTORY,
+                    { url: "http://mozilla.org/foo/" }),
+  ];
+  registerBasicTestProvider(matches);
+
+  info("Disable one of the sources, should get a single match");
+  Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
+  promise = Promise.all([
+    promiseControllerNotification(controller, "onQueryResults"),
+    promiseControllerNotification(controller, "onQueryFinished"),
+  ]);
+  await controller.startQuery(context, controller);
+  await promise;
+  Assert.deepEqual(context.results, [match]);
+  Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+
+  info("Use a restriction character, should get a single match");
+  context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`);
+  promise = Promise.all([
+    promiseControllerNotification(controller, "onQueryResults"),
+    promiseControllerNotification(controller, "onQueryFinished"),
+  ]);
+  await controller.startQuery(context, controller);
+  await promise;
+  Assert.deepEqual(context.results, [match]);
+});
--- a/browser/components/urlbar/tests/unit/xpcshell.ini
+++ b/browser/components/urlbar/tests/unit/xpcshell.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
 head = head.js
 firefox-appdir = browser
 
 [test_providerOpenTabs.js]
 [test_providersManager.js]
+[test_providersManager_filtering.js]
 [test_QueryContext.js]
 [test_tokenizer.js]
 [test_UrlbarController_unit.js]
 [test_UrlbarController_integration.js]
 [test_UrlbarUtils_addToUrlbarHistory.js]
 [test_UrlbarUtils_getShortcutOrURIAndPostData.js]
--- a/browser/docs/AddressBar.rst
+++ b/browser/docs/AddressBar.rst
@@ -63,16 +63,17 @@ It is augmented as it progresses through
                 // may actually return more results than expected, so that the
                 // 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.
   }
 
 
 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.
@@ -138,16 +139,18 @@ implementation details may vary deeply a
   *PlacesUtils.promiseLargeCacheDBConnection* utility.
 
 .. highlight:: JavaScript
 .. code::
 
   UrlbarProvider {
     name; // {string} A simple name to track the provider.
     type; // {integer} One of UrlbarUtils.PROVIDER_TYPE.
+    sources; // {array} List of UrlbarUtils.MATCH_SOURCE, representing the
+             // data sources used by this provider.
     // The returned promise should be resolved when the provider is done
     // searching AND returning matches.
     // Each new UrlbarMatch should be passed to the AddCallback function.
     async startQuery(QueryContext, AddCallback);
     // Any cleaning/resetting task should happen here.
     cancelQuery(QueryContext);
   }
 
@@ -267,16 +270,20 @@ Represents the base *View* implementatio
   UrlbarView {
     // Manage View visibility.
     open();
     close();
     // Invoked when the query starts.
     onQueryStarted(queryContext);
     // Invoked when new matches are available.
     onQueryResults(queryContext);
+    // Invoked when the query has been canceled.
+    onQueryCancelled(queryContext);
+    // Invoked when the query is done.
+    onQueryFinished(queryContext);
   }
 
 UrlbarMatch
 ===========
 
 An `UrlbarMatch <https://dxr.mozilla.org/mozilla-central/source/browser/components/urlbar/UrlbarMatch.jsm>`_
 instance represents a single match (search result) with a match type, that
 identifies specific kind of results.
@@ -289,16 +296,18 @@ properties, supported by all of the matc
 
 .. 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.
   }
 
 
 Shared Modules
 ==============