Bug 1551898 - Replace UrlbarProvider.sources with a more flexible solution. r=adw
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 17 May 2019 14:25:47 +0000
changeset 474335 2bc53ddb0a2b142662265355e54c6deb3ab716ad
parent 474334 b8ad543d54474d5c7dd5faaf714be7d77165c805
child 474336 c7a169c8670c79253241ed244a51e0927346bf15
push id36030
push userrgurzau@mozilla.com
push dateFri, 17 May 2019 21:41:01 +0000
treeherdermozilla-central@7c540586aedb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1551898
milestone68.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 1551898 - Replace UrlbarProvider.sources with a more flexible solution. r=adw Differential Revision: https://phabricator.services.mozilla.com/D31272
browser/components/urlbar/UrlbarInput.jsm
browser/components/urlbar/UrlbarProviderOpenTabs.jsm
browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
browser/components/urlbar/UrlbarProvidersManager.jsm
browser/components/urlbar/UrlbarUtils.jsm
browser/components/urlbar/tests/unit/head.js
browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
browser/components/urlbar/tests/unit/test_providersManager_filtering.js
browser/docs/AddressBar.rst
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -638,17 +638,16 @@ class UrlbarInput {
     // TODO (Bug 1522902): This promise is necessary for tests, because some
     // tests are not listening for completion when starting a query through
     // other methods than startQuery (input events for example).
     this.lastQueryContextPromise = this.controller.startQuery(new UrlbarQueryContext({
       allowAutofill,
       isPrivate: this.isPrivate,
       maxResults: UrlbarPrefs.get("maxRichResults"),
       muxer: "UnifiedComplete",
-      providers: ["UnifiedComplete"],
       searchString,
       userContextId: this.window.gBrowser.selectedBrowser.getAttribute("usercontextid"),
     }));
   }
 
   /**
    * Sets the input's value, starts a search, and opens the popup.
    *
--- a/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
+++ b/browser/components/urlbar/UrlbarProviderOpenTabs.jsm
@@ -90,23 +90,37 @@ class ProviderOpenTabs extends UrlbarPro
    * Returns the type of this provider.
    * @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.RESULT_SOURCE.*
+   * Whether this provider should be invoked for the given context.
+   * If this method returns false, the providers manager won't start a query
+   * with this provider, to save on resources.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider should be invoked for the search.
    */
-  get sources() {
-    return [
-      UrlbarUtils.RESULT_SOURCE.TABS,
-    ];
+  isActive(queryContext) {
+    // For now we don't actually use this provider to query open tabs, instead
+    // we join the temp table in UnifiedComplete.
+    return false;
+  }
+
+  /**
+   * Whether this provider wants to restrict results to just itself.
+   * Other providers won't be invoked, unless this provider doesn't
+   * support the current query.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider wants to restrict results.
+   */
+  isRestricting(queryContext) {
+    return false;
   }
 
   /**
    * Registers a tab as open.
    * @param {string} url Address of the tab
    * @param {integer} userContextId Containers user context id
    */
   registerOpenTab(url, userContextId = 0) {
--- a/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
+++ b/browser/components/urlbar/UrlbarProviderUnifiedComplete.jsm
@@ -58,28 +58,35 @@ class ProviderUnifiedComplete extends Ur
    * 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.RESULT_SOURCE.*
+   * Whether this provider should be invoked for the given context.
+   * If this method returns false, the providers manager won't start a query
+   * with this provider, to save on resources.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider should be invoked for the search.
    */
-  get sources() {
-    return [
-      UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
-      UrlbarUtils.RESULT_SOURCE.HISTORY,
-      UrlbarUtils.RESULT_SOURCE.SEARCH,
-      UrlbarUtils.RESULT_SOURCE.TABS,
-      UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
-      UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
-    ];
+  isActive(queryContext) {
+    return true;
+  }
+
+  /**
+   * Whether this provider wants to restrict results to just itself.
+   * Other providers won't be invoked, unless this provider doesn't
+   * support the current query.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider wants to restrict results.
+   */
+  isRestricting(queryContext) {
+    return false;
   }
 
   /**
    * 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.
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -36,33 +36,28 @@ var localMuxerModules = {
   UrlbarMuxerUnifiedComplete: "resource:///modules/UrlbarMuxerUnifiedComplete.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;
 
-const DEFAULT_PROVIDERS = ["UnifiedComplete"];
 const DEFAULT_MUXER = "UnifiedComplete";
 
 /**
  * Class used to create a manager.
  * The manager is responsible to keep a list of providers, instantiate query
  * objects and pass those to the providers.
  */
 class ProvidersManager {
   constructor() {
     // Tracks the available providers.
-    // This is a double map, first it maps by PROVIDER_TYPE, then
-    // registerProvider maps by provider.name: { type: { name: provider }}
-    this.providers = new Map();
-    for (let type of Object.values(UrlbarUtils.PROVIDER_TYPE)) {
-      this.providers.set(type, new Map());
-    }
+    // This is a sorted array, with IMMEDIATE providers at the top.
+    this.providers = [];
     for (let [symbol, module] of Object.entries(localProviderModules)) {
       let {[symbol]: provider} = ChromeUtils.import(module, {});
       this.registerProvider(provider);
     }
     // Tracks ongoing Query instances by queryContext.
     this.queries = new Map();
 
     // Interrupt() allows to stop any running SQL query, some provider may be
@@ -85,26 +80,33 @@ class ProvidersManager {
   registerProvider(provider) {
     if (!provider || !(provider instanceof UrlbarProvider)) {
       throw new Error(`Trying to register an invalid provider`);
     }
     if (!Object.values(UrlbarUtils.PROVIDER_TYPE).includes(provider.type)) {
       throw new Error(`Unknown provider type ${provider.type}`);
     }
     logger.info(`Registering provider ${provider.name}`);
-    this.providers.get(provider.type).set(provider.name, provider);
+    if (provider.type == UrlbarUtils.PROVIDER_TYPE.IMMEDIATE) {
+      this.providers.unshift(provider);
+    } else {
+      this.providers.push(provider);
+    }
   }
 
   /**
    * Unregisters a previously registered provider object.
    * @param {object} provider
    */
   unregisterProvider(provider) {
     logger.info(`Unregistering provider ${provider.name}`);
-    this.providers.get(provider.type).delete(provider.name);
+    let index = this.providers.indexOf(provider);
+    if (index != -1) {
+      this.providers.splice(index, 1);
+    }
   }
 
   /**
    * Registers a muxer object with the manager.
    * @param {object} muxer a UrlbarMuxer object
    */
   registerMuxer(muxer) {
     if (!muxer || !(muxer instanceof UrlbarMuxer)) {
@@ -134,19 +136,22 @@ class ProvidersManager {
 
     // Define the muxer to use.
     let muxerName = queryContext.muxer || DEFAULT_MUXER;
     logger.info(`Using muxer ${muxerName}`);
     let muxer = this.muxers.get(muxerName);
     if (!muxer) {
       throw new Error(`Muxer with name ${muxerName} not found`);
     }
-    // Define the list of providers to use.
-    let providers = queryContext.providers || DEFAULT_PROVIDERS;
-    providers = filterProviders(this.providers, providers);
+
+    // If the queryContext specifies a list of providers to use, filter on it,
+    // otherwise just pass the full list of providers.
+    let providers = queryContext.providers ?
+                      this.providers.filter(p => queryContext.providers.includes(p.name)) :
+                      this.providers;
 
     let query = new Query(queryContext, controller, muxer, providers);
     this.queries.set(queryContext, query);
     await query.start();
   }
 
   /**
    * Cancels a running query.
@@ -208,65 +213,65 @@ class Query {
     this.context = queryContext;
     this.context.results = [];
     this.muxer = muxer;
     this.controller = controller;
     this.providers = providers;
     this.started = false;
     this.canceled = false;
     this.complete = false;
-    // Array of acceptable RESULT_SOURCE values for this query. Providers not
-    // returning any of these will be skipped, as well as results not part of
-    // this subset (Note we still expect the provider to do its own internal
-    // filtering, our additional filtering will be for sanity).
+
+    // Array of acceptable RESULT_SOURCE values for this query. Providers can
+    // use queryContext.acceptableSources to decide whether they want to be
+    // invoked or not.
+    // This is also used to filter results in add().
     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}`);
+    // Pass a copy so the provider can't modify our local version.
+    this.context.acceptableSources = this.acceptableSources.slice();
 
+    // Check which providers should be queried.
+    let providers = this.providers.filter(p => p.isActive(this.context));
+    // Check if any of the remaining providers wants to restrict the search.
+    let restrictProviders = providers.filter(p => p.isRestricting(this.context));
+    if (restrictProviders.length) {
+      providers = restrictProviders;
+    }
+
+    // Start querying providers.
     let promises = [];
-    for (let provider of this.providers.get(UrlbarUtils.PROVIDER_TYPE.IMMEDIATE).values()) {
+    let delayStarted = false;
+    for (let provider of providers) {
       if (this.canceled) {
         break;
       }
-      // Immediate type providers may return heuristic results, that usually can
-      // bypass suggest.* preferences, so we always execute them, regardless of
-      // this.acceptableSources, and filter results in add().
+      if (provider.type != UrlbarUtils.PROVIDER_TYPE.IMMEDIATE && !delayStarted) {
+        delayStarted = true;
+        // 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;
+      }
       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;
-
-    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;
-        }
-        if (this._providerHasAcceptableSources(provider)) {
-          promises.push(provider.startQuery(this.context, this.add.bind(this)));
-        }
-      }
-    }
-
     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();
       }
@@ -281,20 +286,18 @@ class Query {
    * Cancels this query.
    * @note Invoking cancel multiple times is a no-op.
    */
   cancel() {
     if (this.canceled) {
       return;
     }
     this.canceled = true;
-    for (let providers of this.providers.values()) {
-      for (let provider of providers.values()) {
-        provider.cancelQuery(this.context);
-      }
+    for (let provider of this.providers) {
+      provider.cancelQuery(this.context);
     }
     if (this._chunkTimer) {
       this._chunkTimer.cancel().catch(Cu.reportError);
     }
     if (this._sleepTimer) {
       this._sleepTimer.fire().catch(Cu.reportError);
     }
   }
@@ -344,25 +347,16 @@ 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.
@@ -475,25 +469,8 @@ function getAcceptableMatchSources(conte
         if (!restrictTokenType) {
           acceptedSources.push(source);
         }
         break;
     }
   }
   return acceptedSources;
 }
-
-/* Given a providers Map and a list of provider names, produces a filtered
- * Map containing only the provided names.
- * @param providersMap {Map} providers mapped by type and name
- * @param names {array} list of provider names to retain
- * @returns {Map} a new filtered providers Map
- */
-function filterProviders(providersMap, names) {
-  let providers = new Map();
-  for (let [type, providersByName] of providersMap) {
-    providers.set(type, new Map());
-    for (let name of Array.from(providersByName.keys()).filter(n => names.includes(n))) {
-      providers.get(type).set(name, providersByName.get(name));
-    }
-  }
-  return providers;
-}
--- a/browser/components/urlbar/UrlbarUtils.jsm
+++ b/browser/components/urlbar/UrlbarUtils.jsm
@@ -481,21 +481,35 @@ class UrlbarProvider {
   /**
    * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
    * @abstract
    */
   get type() {
     throw new Error("Trying to access the base class, must be overridden");
   }
   /**
-   * List of UrlbarUtils.RESULT_SOURCE, representing the data sources used by
-   * the provider.
+   * Whether this provider should be invoked for the given context.
+   * If this method returns false, the providers manager won't start a query
+   * with this provider, to save on resources.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider should be invoked for the search.
    * @abstract
    */
-  get sources() {
+  isActive(queryContext) {
+    throw new Error("Trying to access the base class, must be overridden");
+  }
+  /**
+   * Whether this provider wants to restrict results to just itself.
+   * Other providers won't be invoked, unless this provider doesn't
+   * support the current query.
+   * @param {UrlbarQueryContext} queryContext The query context object
+   * @returns {boolean} Whether this provider wants to restrict results.
+   * @abstract
+   */
+  isRestricting(queryContext) {
     throw new Error("Trying to access the base class, must be overridden");
   }
   /**
    * Starts querying.
    * @param {UrlbarQueryContext} queryContext The query context object
    * @param {function} addCallback Callback invoked by the provider to add a new
    *        result. A UrlbarResult should be passed to it.
    * @note Extended classes should return a Promise resolved when the provider
--- a/browser/components/urlbar/tests/unit/head.js
+++ b/browser/components/urlbar/tests/unit/head.js
@@ -72,61 +72,68 @@ function promiseControllerNotification(c
     controller.addQueryListener(proxifiedObserver);
   });
 }
 
 /**
  * A basic test provider, returning all the provided matches.
  */
 class TestProvider extends UrlbarProvider {
-  constructor(matches, cancelCallback) {
+  constructor(matches, cancelCallback, type = UrlbarUtils.PROVIDER_TYPE.PROFILE) {
     super();
     this._name = "TestProvider" + Math.floor(Math.random() * 100000);
     this._cancelCallback = cancelCallback;
     this._matches = matches;
+    this._type = type;
   }
   get name() {
     return this._name;
   }
   get type() {
-    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    return this._type;
   }
-  get sources() {
-    return this._matches.map(r => r.source);
+  isActive(context) {
+    Assert.ok(context, "context is passed-in");
+    return true;
+  }
+  isRestricting(context) {
+    Assert.ok(context, "context is passed-in");
+    return false;
   }
   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 match of this._matches) {
       add(this, match);
     }
   }
   cancelQuery(context) {
     // If the query was created but didn't run, this_context will be undefined.
     if (this._context) {
-      Assert.equal(this._context, context, "context is the same");
+      Assert.equal(this._context, context, "cancelQuery: context is the same");
     }
     if (this._cancelCallback) {
       this._cancelCallback();
     }
   }
 }
 
 /**
  * Helper function to clear the existing providers and register a basic provider
  * that returns only the results given.
  *
  * @param {array} matches The matches for the provider to return.
  * @param {function} [cancelCallback] Optional, called when the query provider
  *                                    receives a cancel instruction.
+ * @param {UrlbarUtils.PROVIDER_TYPE} type The provider type.
  * @returns {string} name of the registered provider
  */
-function registerBasicTestProvider(matches, cancelCallback) {
-  let provider = new TestProvider(matches, cancelCallback);
+function registerBasicTestProvider(matches = [], cancelCallback, type) {
+  let provider = new TestProvider(matches, cancelCallback, type);
   UrlbarProvidersManager.registerProvider(provider);
   return provider.name;
 }
 
 // Creates an HTTP server for the test.
 function makeTestServer(port = -1) {
   let httpServer = new HttpServer();
   httpServer.start(port);
--- a/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
+++ b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js
@@ -28,18 +28,21 @@ class DelayedProvider extends UrlbarProv
     this._name = "TestProvider" + Math.floor(Math.random() * 100000);
   }
   get name() {
     return this._name;
   }
   get type() {
     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
   }
-  get sources() {
-    return [UrlbarUtils.RESULT_SOURCE.TABS];
+  isActive(context) {
+    return true;
+  }
+  isRestricting(context) {
+    return false;
   }
   async startQuery(context, add) {
     Assert.ok(context, "context is passed-in");
     Assert.equal(typeof add, "function", "add is a callback");
     this._add = add;
     await new Promise(resolve => {
       this._resultsAdded = resolve;
     });
--- a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
+++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js
@@ -1,85 +1,109 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-add_task(async function test_filtering() {
+/**
+ * A test controller.
+ */
+class TestUrlbarController extends UrlbarController {
+  constructor() {
+    super({
+      browserWindow: {
+        location: {
+          href: AppConstants.BROWSER_CHROME_URL,
+        },
+      },
+    });
+  }
+}
+
+add_task(async function test_filtering_disable_only_source() {
   let match = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                                UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: "http://mozilla.org/foo/" });
   let providerName = registerBasicTestProvider([match]);
   let context = createContext(undefined, {providers: [providerName]});
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
+  let controller = new TestUrlbarController();
 
   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");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
+});
 
+add_task(async function test_filtering_disable_one_source() {
   let matches = [
-    match,
+    new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+                     UrlbarUtils.RESULT_SOURCE.TABS,
+                     { url: "http://mozilla.org/foo/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/foo/" }),
   ];
-  providerName = registerBasicTestProvider(matches);
-  context = createContext(undefined, {providers: [providerName]});
+  let providerName = registerBasicTestProvider(matches);
+  let context = createContext(undefined, {providers: [providerName]});
+  let controller = new TestUrlbarController();
 
   info("Disable one of the sources, should get a single match");
   Services.prefs.setBoolPref("browser.urlbar.suggest.history", false);
-  promise = Promise.all([
+  let promise = Promise.all([
     promiseControllerNotification(controller, "onQueryResults"),
     promiseControllerNotification(controller, "onQueryFinished"),
   ]);
   await controller.startQuery(context, controller);
   await promise;
-  Assert.deepEqual(context.results, [match]);
+  Assert.deepEqual(context.results, matches.slice(0, 1));
   Services.prefs.clearUserPref("browser.urlbar.suggest.history");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
+});
+
+add_task(async function test_filtering_restriction_token() {
+  let matches = [
+    new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+                     UrlbarUtils.RESULT_SOURCE.TABS,
+                     { url: "http://mozilla.org/foo/" }),
+    new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+                     UrlbarUtils.RESULT_SOURCE.HISTORY,
+                     { url: "http://mozilla.org/foo/" }),
+  ];
+  let providerName = registerBasicTestProvider(matches);
+  let context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`,
+                              {providers: [providerName]});
+  let controller = new TestUrlbarController();
 
   info("Use a restriction character, should get a single match");
-  context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`,
-                          {providers: [providerName]});
-  promise = Promise.all([
+  let promise = Promise.all([
     promiseControllerNotification(controller, "onQueryResults"),
     promiseControllerNotification(controller, "onQueryFinished"),
   ]);
   await controller.startQuery(context, controller);
   await promise;
-  Assert.deepEqual(context.results, [match]);
+  Assert.deepEqual(context.results, matches.slice(0, 1));
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
 });
 
 add_task(async function test_filter_javascript() {
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
   let match = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                                UrlbarUtils.RESULT_SOURCE.TABS,
                                { url: "http://mozilla.org/foo/" });
   let jsMatch = new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                                  UrlbarUtils.RESULT_SOURCE.HISTORY,
                                  { url: "javascript:foo" });
   let providerName = registerBasicTestProvider([match, jsMatch]);
   let context = createContext(undefined, {providers: [providerName]});
+  let controller = new TestUrlbarController();
 
   info("By default javascript should be filtered out");
   let promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Assert.deepEqual(context.results, [match]);
 
   info("Except when the user explicitly starts the search with javascript:");
@@ -93,242 +117,275 @@ add_task(async function test_filter_java
   info("Disable javascript filtering");
   Services.prefs.setBoolPref("browser.urlbar.filter.javascript", false);
   context = createContext(undefined, {providers: [providerName]});
   promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Assert.deepEqual(context.results, [match, jsMatch]);
   Services.prefs.clearUserPref("browser.urlbar.filter.javascript");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
 });
 
-add_task(async function test_filter_sources() {
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
-
+add_task(async function test_filter_isActive() {
   let goodMatches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
                      UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/foo/" }),
   ];
-  /**
-   * A test provider that should be invoked.
-   */
-  class TestProvider extends UrlbarProvider {
-    get name() {
-      return "GoodProvider";
-    }
-    get type() {
-      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
-    }
-    get sources() {
-      return [
-        UrlbarUtils.RESULT_SOURCE.TABS,
-        UrlbarUtils.RESULT_SOURCE.HISTORY,
-      ];
-    }
-    async startQuery(context, add) {
-      Assert.ok(true, "expected provider was invoked");
-      for (const match of goodMatches) {
-        add(this, match);
-      }
-    }
-    cancelQuery(context) {}
-  }
-  UrlbarProvidersManager.registerProvider(new TestProvider());
+  let providerName = registerBasicTestProvider(goodMatches);
 
   let badMatches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
                      UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
                      { url: "http://mozilla.org/foo/" }),
   ];
-
   /**
    * A test provider that should not be invoked.
    */
   class NoInvokeProvider extends UrlbarProvider {
     get name() {
       return "BadProvider";
     }
     get type() {
       return UrlbarUtils.PROVIDER_TYPE.PROFILE;
     }
-    get sources() {
-      return [UrlbarUtils.RESULT_SOURCE.BOOKMARKS];
+    isActive(context) {
+      info("Acceptable sources: " + context.acceptableSources);
+      return context.acceptableSources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS);
+    }
+    isRestricting(context) {
+      return false;
     }
     async startQuery(context, add) {
       Assert.ok(false, "Provider should no be invoked");
       for (const match of badMatches) {
         add(this, match);
       }
     }
     cancelQuery(context) {}
   }
-
   UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
 
   let context = createContext(undefined, {
     sources: [UrlbarUtils.RESULT_SOURCE.TABS],
-    providers: ["GoodProvider", "BadProvider"],
+    providers: [providerName, "BadProvider"],
   });
+  let controller = new TestUrlbarController();
 
   info("Only tabs should be returned");
   let promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Assert.deepEqual(context.results.length, 1, "Should find only one match");
   Assert.deepEqual(context.results[0].source, UrlbarUtils.RESULT_SOURCE.TABS,
                    "Should find only a tab match");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
+  UrlbarProvidersManager.unregisterProvider({name: "BadProvider"});
+});
+
+add_task(async function test_filter_queryContext() {
+  let providerName = registerBasicTestProvider();
+
+  /**
+   * A test provider that should not be invoked because of queryContext.providers.
+   */
+  class NoInvokeProvider extends UrlbarProvider {
+    get name() {
+      return "BadProvider";
+    }
+    get type() {
+      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    }
+    isActive(context) {
+      return true;
+    }
+    isRestricting(context) {
+      return false;
+    }
+    async startQuery(context, add) {
+      Assert.ok(false, "Provider should no be invoked");
+    }
+    cancelQuery(context) {}
+  }
+  UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
+
+  let context = createContext(undefined, {
+    providers: [providerName],
+  });
+  let controller = new TestUrlbarController();
+
+  await controller.startQuery(context, controller);
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
+  UrlbarProvidersManager.unregisterProvider({name: "BadProvider"});
 });
 
 add_task(async function test_nofilter_immediate() {
   // Checks that even if a provider returns a result that should be filtered out
   // it will still be invoked if it's of type immediate, and only the heuristic
   // result is returned.
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
-
   let matches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo2/" }),
   ];
   matches[0].heuristic = true;
-
-  /**
-   * A test provider that should be invoked.
-   */
-  class TestProvider extends UrlbarProvider {
-    get name() {
-      return "GoodProvider";
-    }
-    get type() {
-      return UrlbarUtils.PROVIDER_TYPE.IMMEDIATE;
-    }
-    get sources() {
-      return [
-        UrlbarUtils.RESULT_SOURCE.TABS,
-      ];
-    }
-    async startQuery(context, add) {
-      Assert.ok(true, "expected provider was invoked");
-      for (let match of matches) {
-        add(this, match);
-      }
-    }
-    cancelQuery(context) {}
-  }
-  UrlbarProvidersManager.registerProvider(new TestProvider());
+  let providerName = registerBasicTestProvider(matches, undefined,
+    UrlbarUtils.PROVIDER_TYPE.IMMEDIATE);
 
   let context = createContext(undefined, {
     sources: [UrlbarUtils.RESULT_SOURCE.SEARCH],
-    providers: ["GoodProvider"],
+    providers: [providerName],
   });
+  let controller = new TestUrlbarController();
+
   // Disable search matches through prefs.
   Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
-
   info("Only 1 heuristic tab result should be returned");
   let promise = promiseControllerNotification(controller, "onQueryResults");
   await controller.startQuery(context, controller);
   await promise;
   Services.prefs.clearUserPref("browser.urlbar.suggest.openpage");
   Assert.deepEqual(context.results.length, 1, "Should find only one match");
   Assert.deepEqual(context.results[0].source, UrlbarUtils.RESULT_SOURCE.TABS,
                    "Should find only a tab match");
+  UrlbarProvidersManager.unregisterProvider({name: providerName});
 });
 
 add_task(async function test_nofilter_restrict() {
   // Checks that even if a pref is disabled, we still return results on a
   // restriction token.
-  let controller = new UrlbarController({
-    browserWindow: {
-      location: {
-        href: AppConstants.BROWSER_CHROME_URL,
-      },
-    },
-  });
-
   let matches = [
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
                      UrlbarUtils.RESULT_SOURCE.TABS,
                      { url: "http://mozilla.org/foo_tab/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
                      UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
                      { url: "http://mozilla.org/foo_bookmark/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.URL,
                      UrlbarUtils.RESULT_SOURCE.HISTORY,
                      { url: "http://mozilla.org/foo_history/" }),
     new UrlbarResult(UrlbarUtils.RESULT_TYPE.SEARCH,
                      UrlbarUtils.RESULT_SOURCE.SEARCH,
                      { engine: "noengine" }),
   ];
-
   /**
    * A test provider.
    */
   class TestProvider extends UrlbarProvider {
     get name() {
       return "MyProvider";
     }
     get type() {
       return UrlbarUtils.PROVIDER_TYPE.IMMEDIATE;
     }
-    get sources() {
-      return [
-        UrlbarUtils.RESULT_SOURCE.TABS,
-        UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
-        UrlbarUtils.RESULT_SOURCE.HISTORY,
-        UrlbarUtils.RESULT_SOURCE.SEARCH,
-      ];
+    isActive(context) {
+      Assert.equal(context.acceptableSources.length, 1,
+                   "Check acceptableSources");
+      return true;
+    }
+    isRestricting(context) {
+      return false;
     }
     async startQuery(context, add) {
       Assert.ok(true, "expected provider was invoked");
       for (let match of matches) {
         add(this, match);
       }
     }
-    cancelQuery(context) {}
+    cancelQuery(context) {
+    }
   }
-  UrlbarProvidersManager.registerProvider(new TestProvider());
+  let provider = new TestProvider();
+  UrlbarProvidersManager.registerProvider(provider);
 
   let typeToPropertiesMap = new Map([
     ["HISTORY", {source: "HISTORY", pref: "history"}],
     ["BOOKMARK", {source: "BOOKMARKS", pref: "bookmark"}],
     ["OPENPAGE", {source: "TABS", pref: "openpage"}],
     ["SEARCH", {source: "SEARCH", pref: "searches"}],
   ]);
   for (let [type, token] of Object.entries(UrlbarTokenizer.RESTRICT)) {
     let properties = typeToPropertiesMap.get(type);
     if (!properties) {
       continue;
     }
     info("Restricting on " + type);
     let context = createContext(token + " foo", {
       providers: ["MyProvider"],
     });
+    let controller = new TestUrlbarController();
     // Disable the corresponding pref.
     const pref = "browser.urlbar.suggest." + properties.pref;
     info("Disabling " + pref);
     Services.prefs.setBoolPref(pref, false);
     await controller.startQuery(context, controller);
     Assert.equal(context.results.length, 1, "Should find one result");
     Assert.equal(context.results[0].source,
                  UrlbarUtils.RESULT_SOURCE[properties.source],
                  "Check result source");
     Services.prefs.clearUserPref(pref);
   }
+  UrlbarProvidersManager.unregisterProvider(provider);
 });
+
+add_task(async function test_filter_isRestricting() {
+  /**
+   * A test provider that should be invoked and is restricting.
+   */
+  class TestProvider extends UrlbarProvider {
+    get name() {
+      return "GoodProvider";
+    }
+    get type() {
+      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    }
+    isActive(context) {
+      return true;
+    }
+    isRestricting(context) {
+      return true;
+    }
+    async startQuery(context, add) {
+      Assert.ok(true, "expected provider was invoked");
+    }
+    cancelQuery(context) {}
+  }
+  UrlbarProvidersManager.registerProvider(new TestProvider());
+
+  /**
+   * A test provider that should not be invoked because the other one is restricting.
+   */
+  class NoInvokeProvider extends UrlbarProvider {
+    get name() {
+      return "BadProvider";
+    }
+    get type() {
+      return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+    }
+    isActive(context) {
+      return true;
+    }
+    isRestricting(context) {
+      return false;
+    }
+    async startQuery(context, add) {
+      Assert.ok(false, "Provider should no be invoked");
+    }
+    cancelQuery(context) {}
+  }
+  UrlbarProvidersManager.registerProvider(new NoInvokeProvider());
+
+  let context = createContext(undefined, {
+    providers: ["GoodProvider", "BadProvider"],
+  });
+  let controller = new TestUrlbarController();
+
+  await controller.startQuery(context, controller);
+  UrlbarProvidersManager.unregisterProvider({name: "GoodProvider"});
+  UrlbarProvidersManager.unregisterProvider({name: "BadProvider"});
+});
--- a/browser/docs/AddressBar.rst
+++ b/browser/docs/AddressBar.rst
@@ -74,16 +74,18 @@ It is augmented as it progresses through
     sources; // {array} If provided is the list of sources, as defined by
              // RESULT_SOURCE.*, that can be returned by the model.
 
     // Properties added by the Model.
     preselected; // {boolean} whether the first result should be preselected.
     results; // {array} list of UrlbarResult objects.
     tokens; // {array} tokens extracted from the searchString, each token is an
             // object in the form {type, value, lowerCaseValue}.
+    acceptableSources; // {array} list of UrlbarUtils.RESULT_SOURCE that the
+                       // model will accept for this context.
   }
 
 
 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.
@@ -151,56 +153,70 @@ implementation details may vary deeply a
   Internal providers can access the Places database through the
   *PlacesUtils.promiseLargeCacheDBConnection* utility.
 
 .. highlight:: JavaScript
 .. code::
 
   class UrlbarProvider {
     /**
-    * Unique name for the provider, used by the context to filter on providers.
-    * Not using a unique name will cause the newest registration to win.
-    * @abstract
-    */
+     * Unique name for the provider, used by the context to filter on providers.
+     * Not using a unique name will cause the newest registration to win.
+     * @abstract
+     */
     get name() {
       return "UrlbarProviderBase";
     }
     /**
-    * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
-    * @abstract
-    */
+     * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
+     * @abstract
+     */
     get type() {
       throw new Error("Trying to access the base class, must be overridden");
     }
     /**
-    * List of UrlbarUtils.RESULT_SOURCE, representing the data sources used by
-    * the provider.
-    * @abstract
-    */
-    get sources() {
+     * Whether this provider should be invoked for the given context.
+     * If this method returns false, the providers manager won't start a query
+     * with this provider, to save on resources.
+     * @param {UrlbarQueryContext} queryContext The query context object
+     * @returns {boolean} Whether this provider should be invoked for the search.
+     * @abstract
+     */
+    isActive(queryContext) {
       throw new Error("Trying to access the base class, must be overridden");
     }
     /**
-    * Starts querying.
-    * @param {UrlbarQueryContext} queryContext The query context object
-    * @param {function} addCallback Callback invoked by the provider to add a new
-    *        result. A UrlbarResult should be passed to it.
-    * @note Extended classes should return a Promise resolved when the provider
-    *       is done searching AND returning results.
-    * @abstract
-    */
+     * Whether this provider wants to restrict results to just itself.
+     * Other providers won't be invoked, unless this provider doesn't
+     * support the current query.
+     * @param {UrlbarQueryContext} queryContext The query context object
+     * @returns {boolean} Whether this provider wants to restrict results.
+     * @abstract
+     */
+    isRestricting(queryContext) {
+      throw new Error("Trying to access the base class, must be overridden");
+    }
+    /**
+     * Starts querying.
+     * @param {UrlbarQueryContext} queryContext The query context object
+     * @param {function} addCallback Callback invoked by the provider to add a new
+     *        result. A UrlbarResult should be passed to it.
+     * @note Extended classes should return a Promise resolved when the provider
+     *       is done searching AND returning results.
+     * @abstract
+     */
     startQuery(queryContext, addCallback) {
       throw new Error("Trying to access the base class, must be overridden");
     }
     /**
-    * Cancels a running query,
-    * @param {UrlbarQueryContext} queryContext The query context object to cancel
-    *        query for.
-    * @abstract
-    */
+     * Cancels a running query,
+     * @param {UrlbarQueryContext} queryContext The query context object to cancel
+     *        query for.
+     * @abstract
+     */
     cancelQuery(queryContext) {
       throw new Error("Trying to access the base class, must be overridden");
     }
   }
 
 UrlbarMuxer
 -----------
 
@@ -213,28 +229,28 @@ indicated by the UrlbarQueryContext.muxe
   The Muxer is a replaceable component, as such what is described here is a
   reference for the default View, but may not be valid for other implementations.
 
 .. highlight:: JavaScript
 .. code::
 
   class UrlbarMuxer {
     /**
-    * Unique name for the muxer, used by the context to sort results.
-    * Not using a unique name will cause the newest registration to win.
-    * @abstract
-    */
+     * Unique name for the muxer, used by the context to sort results.
+     * Not using a unique name will cause the newest registration to win.
+     * @abstract
+     */
     get name() {
       return "UrlbarMuxerBase";
     }
     /**
-    * Sorts UrlbarQueryContext results in-place.
-    * @param {UrlbarQueryContext} queryContext the context to sort results for.
-    * @abstract
-    */
+     * Sorts UrlbarQueryContext results in-place.
+     * @param {UrlbarQueryContext} queryContext the context to sort results for.
+     * @abstract
+     */
     sort(queryContext) {
       throw new Error("Trying to access the base class, must be overridden");
     }
   }
 
 
 The Controller
 ==============