Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / urlbar / UrlbarProvidersManager.sys.mjs
blob25a84dda7f1b7bbdb93512938004aa84679bf7f9
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * This module exports a component used to register search providers and manage
7  * the connection between such providers and a UrlbarController.
8  */
10 const lazy = {};
12 ChromeUtils.defineESModuleGetters(lazy, {
13   ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
14   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
15   SkippableTimer: "resource:///modules/UrlbarUtils.sys.mjs",
16   UrlbarMuxer: "resource:///modules/UrlbarUtils.sys.mjs",
17   UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
18   UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs",
19   UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
20   UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
21   UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
22 });
24 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
25   lazy.UrlbarUtils.getLogger({ prefix: "ProvidersManager" })
28 // List of available local providers, each is implemented in its own jsm module
29 // and will track different queries internally by queryContext.
30 // When adding new providers please remember to update the list in metrics.yaml.
31 var localProviderModules = {
32   UrlbarProviderAboutPages:
33     "resource:///modules/UrlbarProviderAboutPages.sys.mjs",
34   UrlbarProviderActionsSearchMode:
35     "resource:///modules/UrlbarProviderActionsSearchMode.sys.mjs",
36   UrlbarProviderGlobalActions:
37     "resource:///modules/UrlbarProviderGlobalActions.sys.mjs",
38   UrlbarProviderAliasEngines:
39     "resource:///modules/UrlbarProviderAliasEngines.sys.mjs",
40   UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
41   UrlbarProviderBookmarkKeywords:
42     "resource:///modules/UrlbarProviderBookmarkKeywords.sys.mjs",
43   UrlbarProviderCalculator:
44     "resource:///modules/UrlbarProviderCalculator.sys.mjs",
45   UrlbarProviderClipboard:
46     "resource:///modules/UrlbarProviderClipboard.sys.mjs",
47   UrlbarProviderHeuristicFallback:
48     "resource:///modules/UrlbarProviderHeuristicFallback.sys.mjs",
49   UrlbarProviderHistoryUrlHeuristic:
50     "resource:///modules/UrlbarProviderHistoryUrlHeuristic.sys.mjs",
51   UrlbarProviderInputHistory:
52     "resource:///modules/UrlbarProviderInputHistory.sys.mjs",
53   UrlbarProviderInterventions:
54     "resource:///modules/UrlbarProviderInterventions.sys.mjs",
55   UrlbarProviderOmnibox: "resource:///modules/UrlbarProviderOmnibox.sys.mjs",
56   UrlbarProviderPlaces: "resource:///modules/UrlbarProviderPlaces.sys.mjs",
57   UrlbarProviderPrivateSearch:
58     "resource:///modules/UrlbarProviderPrivateSearch.sys.mjs",
59   UrlbarProviderQuickSuggest:
60     "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
61   UrlbarProviderQuickSuggestContextualOptIn:
62     "resource:///modules/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs",
63   UrlbarProviderRecentSearches:
64     "resource:///modules/UrlbarProviderRecentSearches.sys.mjs",
65   UrlbarProviderRemoteTabs:
66     "resource:///modules/UrlbarProviderRemoteTabs.sys.mjs",
67   UrlbarProviderRestrictKeywords:
68     "resource:///modules/UrlbarProviderRestrictKeywords.sys.mjs",
69   UrlbarProviderRestrictKeywordsAutofill:
70     "resource:///modules/UrlbarProviderRestrictKeywordsAutofill.sys.mjs",
71   UrlbarProviderSearchTips:
72     "resource:///modules/UrlbarProviderSearchTips.sys.mjs",
73   UrlbarProviderSearchSuggestions:
74     "resource:///modules/UrlbarProviderSearchSuggestions.sys.mjs",
75   UrlbarProviderTabToSearch:
76     "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
77   UrlbarProviderTokenAliasEngines:
78     "resource:///modules/UrlbarProviderTokenAliasEngines.sys.mjs",
79   UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.sys.mjs",
80   UrlbarProviderUnitConversion:
81     "resource:///modules/UrlbarProviderUnitConversion.sys.mjs",
84 // List of available local muxers, each is implemented in its own jsm module.
85 var localMuxerModules = {
86   UrlbarMuxerUnifiedComplete:
87     "resource:///modules/UrlbarMuxerUnifiedComplete.sys.mjs",
90 const DEFAULT_MUXER = "UnifiedComplete";
92 /**
93  * Class used to create a manager.
94  * The manager is responsible to keep a list of providers, instantiate query
95  * objects and pass those to the providers.
96  */
97 class ProvidersManager {
98   constructor() {
99     // Tracks the available providers.  This is a sorted array, with HEURISTIC
100     // providers at the front.
101     this.providers = [];
102     this.providersByNotificationType = {
103       onEngagement: new Set(),
104       onImpression: new Set(),
105       onAbandonment: new Set(),
106       onSearchSessionEnd: new Set(),
107     };
108     for (let [symbol, module] of Object.entries(localProviderModules)) {
109       let { [symbol]: provider } = ChromeUtils.importESModule(module);
110       this.registerProvider(provider);
111     }
113     // Tracks ongoing Query instances by queryContext.
114     this.queries = new Map();
116     // Interrupt() allows to stop any running SQL query, some provider may be
117     // running a query that shouldn't be interrupted, and if so it should
118     // bump this through disableInterrupt and enableInterrupt.
119     this.interruptLevel = 0;
121     // This maps muxer names to muxers.
122     this.muxers = new Map();
123     for (let [symbol, module] of Object.entries(localMuxerModules)) {
124       let { [symbol]: muxer } = ChromeUtils.importESModule(module);
125       this.registerMuxer(muxer);
126     }
128     // These can be set by tests to increase or reduce the chunk delays.
129     // See _notifyResultsFromProvider for additional details.
130     // To improve dataflow and reduce UI work, when a result is added we may notify
131     // it to the controller after a delay, so that we can chunk results in that
132     // timeframe into a single call. See _notifyResultsFromProvider for details.
133     this.CHUNK_RESULTS_DELAY_MS = 16;
134   }
136   /**
137    * Registers a provider object with the manager.
138    *
139    * @param {object} provider
140    *   The provider object to register.
141    */
142   registerProvider(provider) {
143     if (!provider || !(provider instanceof lazy.UrlbarProvider)) {
144       throw new Error(`Trying to register an invalid provider`);
145     }
146     if (
147       !Object.values(lazy.UrlbarUtils.PROVIDER_TYPE).includes(provider.type)
148     ) {
149       throw new Error(`Unknown provider type ${provider.type}`);
150     }
151     lazy.logger.info(`Registering provider ${provider.name}`);
152     let index = -1;
153     if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
154       // Keep heuristic providers in order at the front of the array.  Find the
155       // first non-heuristic provider and insert the new provider there.
156       index = this.providers.findIndex(
157         p => p.type != lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC
158       );
159     }
160     if (index < 0) {
161       index = this.providers.length;
162     }
163     this.providers.splice(index, 0, provider);
165     for (const notificationType of Object.keys(
166       this.providersByNotificationType
167     )) {
168       if (typeof provider[notificationType] === "function") {
169         this.providersByNotificationType[notificationType].add(provider);
170       }
171     }
172   }
174   /**
175    * Unregisters a previously registered provider object.
176    *
177    * @param {object} provider
178    *   The provider object to unregister.
179    */
180   unregisterProvider(provider) {
181     lazy.logger.info(`Unregistering provider ${provider.name}`);
182     let index = this.providers.findIndex(p => p.name == provider.name);
183     if (index != -1) {
184       this.providers.splice(index, 1);
185     }
187     Object.values(this.providersByNotificationType).forEach(providers =>
188       providers.delete(provider)
189     );
190   }
192   /**
193    * Returns the provider with the given name.
194    *
195    * @param {string} name
196    *   The provider name.
197    * @returns {UrlbarProvider} The provider.
198    */
199   getProvider(name) {
200     return this.providers.find(p => p.name == name);
201   }
203   /**
204    * Registers a muxer object with the manager.
205    *
206    * @param {object} muxer
207    *   a UrlbarMuxer object
208    */
209   registerMuxer(muxer) {
210     if (!muxer || !(muxer instanceof lazy.UrlbarMuxer)) {
211       throw new Error(`Trying to register an invalid muxer`);
212     }
213     lazy.logger.info(`Registering muxer ${muxer.name}`);
214     this.muxers.set(muxer.name, muxer);
215   }
217   /**
218    * Unregisters a previously registered muxer object.
219    *
220    * @param {object} muxer
221    *   a UrlbarMuxer object or name.
222    */
223   unregisterMuxer(muxer) {
224     let muxerName = typeof muxer == "string" ? muxer : muxer.name;
225     lazy.logger.info(`Unregistering muxer ${muxerName}`);
226     this.muxers.delete(muxerName);
227   }
229   /**
230    * Starts querying.
231    *
232    * @param {object} queryContext
233    *   The query context object
234    * @param {object} [controller]
235    *   a UrlbarController instance
236    */
237   async startQuery(queryContext, controller = null) {
238     lazy.logger.info(`Query start "${queryContext.searchString}"`);
240     // Define the muxer to use.
241     let muxerName = queryContext.muxer || DEFAULT_MUXER;
242     lazy.logger.debug(`Using muxer ${muxerName}`);
243     let muxer = this.muxers.get(muxerName);
244     if (!muxer) {
245       throw new Error(`Muxer with name ${muxerName} not found`);
246     }
248     // If the queryContext specifies a list of providers to use, filter on it,
249     // otherwise just pass the full list of providers.
250     let providers = queryContext.providers
251       ? this.providers.filter(p => queryContext.providers.includes(p.name))
252       : this.providers;
254     queryContext.canceled = false;
255     try {
256       // The tokenizer needs to synchronously check whether the first token is a
257       // keyword, thus here we must ensure the keywords cache is up.
258       await lazy.PlacesUtils.keywords.ensureCacheInitialized();
259     } catch (ex) {
260       lazy.logger.error(
261         "Unable to ensure keyword cache is initialization. A keyword may not be \
262          detected at the beginning of the search string.",
263         ex
264       );
265     }
267     // The query may have been canceled while awaiting for asynchronous work.
268     if (queryContext.canceled) {
269       return;
270     }
272     // Apply tokenization.
273     lazy.UrlbarTokenizer.tokenize(queryContext);
275     // If there's a single source, we are in restriction mode.
276     if (queryContext.sources && queryContext.sources.length == 1) {
277       queryContext.restrictSource = queryContext.sources[0];
278     }
279     // Providers can use queryContext.sources to decide whether they want to be
280     // invoked or not.
281     // The sources may be defined in the context, then the whole search string
282     // can be used for searching. Otherwise sources are extracted from prefs and
283     // restriction tokens, then restriction tokens must be filtered out of the
284     // search string.
285     let restrictToken = updateSourcesIfEmpty(queryContext);
286     if (restrictToken) {
287       queryContext.restrictToken = restrictToken;
288       // If the restriction token has an equivalent source, then set it as
289       // restrictSource.
290       if (lazy.UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(restrictToken.value)) {
291         queryContext.restrictSource = queryContext.sources[0];
292       }
293     }
294     lazy.logger.debug(`Context sources ${queryContext.sources}`);
296     let query = new Query(queryContext, controller, muxer, providers);
297     this.queries.set(queryContext, query);
299     // The muxer and many providers depend on the search service and our search
300     // utils.  Make sure they're initialized now (via UrlbarSearchUtils) so that
301     // all query-related urlbar modules don't need to do it.
302     try {
303       await lazy.UrlbarSearchUtils.init();
304     } catch {
305       // We continue anyway, because we want the user to be able to search their
306       // history and bookmarks even if search engines are not available.
307     }
309     if (query.canceled) {
310       return;
311     }
313     await query.start();
314   }
316   /**
317    * Cancels a running query.
318    *
319    * @param {object} queryContext The query context object
320    */
321   cancelQuery(queryContext) {
322     lazy.logger.info(`Query cancel "${queryContext.searchString}"`);
323     queryContext.canceled = true;
325     let query = this.queries.get(queryContext);
326     if (!query) {
327       // The query object may have not been created yet, if the query was
328       // canceled immediately.
329       return;
330     }
331     query.cancel();
332     if (!this.interruptLevel) {
333       try {
334         let db = lazy.PlacesUtils.promiseLargeCacheDBConnection();
335         db.interrupt();
336       } catch (ex) {}
337     }
338     this.queries.delete(queryContext);
339   }
341   /**
342    * A provider can use this util when it needs to run a SQL query that can't
343    * be interrupted. Otherwise, when a query is canceled any running SQL query
344    * is interrupted abruptly.
345    *
346    * @param {Function} taskFn a Task to execute in the critical section.
347    */
348   async runInCriticalSection(taskFn) {
349     this.interruptLevel++;
350     try {
351       await taskFn();
352     } finally {
353       this.interruptLevel--;
354     }
355   }
357   /**
358    * Notifies all providers about changes in user engagement with the urlbar.
359    * This function centralizes the dispatch of engagement-related events to the
360    * appropriate providers based on the current state of interaction.
361    *
362    * @param {string} state
363    *   The state of the engagement, one of: engagement, abandonment
364    * @param {UrlbarQueryContext} queryContext
365    *   The engagement's query context, if available.
366    * @param {object} details
367    *   An object that describes the search string and the picked result, if any.
368    * @param {UrlbarController} controller
369    *   The controller associated with the engagement
370    */
371   notifyEngagementChange(state, queryContext, details = {}, controller) {
372     if (!["engagement", "abandonment"].includes(state)) {
373       lazy.logger.error(`Unsupported state for engagement change: ${state}`);
374       return;
375     }
377     const visibleResults = controller.view?.visibleResults ?? [];
378     const visibleResultsByProviderName = new Map();
380     visibleResults.forEach((result, index) => {
381       const providerName = result.providerName;
382       let results = visibleResultsByProviderName.get(providerName);
383       if (!results) {
384         results = [];
385         visibleResultsByProviderName.set(providerName, results);
386       }
387       results.push({ index, result });
388     });
390     if (!details.isSessionOngoing) {
391       this.#notifyImpression(
392         this.providersByNotificationType.onImpression,
393         state,
394         queryContext,
395         controller,
396         visibleResultsByProviderName,
397         state == "engagement" && details.result ? details : null
398       );
399     }
401     if (state === "engagement") {
402       if (details.result) {
403         this.#notifyEngagement(
404           this.providersByNotificationType.onEngagement,
405           queryContext,
406           controller,
407           details
408         );
409       }
410     } else {
411       this.#notifyAbandonment(
412         this.providersByNotificationType.onAbandonment,
413         queryContext,
414         controller,
415         visibleResultsByProviderName
416       );
417     }
419     if (!details.isSessionOngoing) {
420       this.#notifySearchSessionEnd(
421         this.providersByNotificationType.onSearchSessionEnd,
422         queryContext,
423         controller,
424         details
425       );
426     }
427   }
429   #notifyEngagement(engagementProviders, queryContext, controller, details) {
430     for (const provider of engagementProviders) {
431       if (details.result.providerName == provider.name) {
432         provider.tryMethod("onEngagement", queryContext, controller, details);
433         break;
434       }
435     }
436   }
438   #notifyImpression(
439     impressionProviders,
440     state,
441     queryContext,
442     controller,
443     visibleResultsByProviderName,
444     details
445   ) {
446     for (const provider of impressionProviders) {
447       const providerVisibleResults =
448         visibleResultsByProviderName.get(provider.name) ?? [];
450       if (providerVisibleResults.length) {
451         provider.tryMethod(
452           "onImpression",
453           state,
454           queryContext,
455           controller,
456           providerVisibleResults,
457           details
458         );
459       }
460     }
461   }
463   #notifyAbandonment(
464     abandomentProviders,
465     queryContext,
466     controller,
467     visibleResultsByProviderName
468   ) {
469     for (const provider of abandomentProviders) {
470       if (visibleResultsByProviderName.has(provider.name)) {
471         provider.tryMethod("onAbandonment", queryContext, controller);
472       }
473     }
474   }
476   #notifySearchSessionEnd(
477     searchSessionEndProviders,
478     queryContext,
479     controller,
480     details
481   ) {
482     for (const provider of searchSessionEndProviders) {
483       provider.tryMethod(
484         "onSearchSessionEnd",
485         queryContext,
486         controller,
487         details
488       );
489     }
490   }
493 export var UrlbarProvidersManager = new ProvidersManager();
496  * Tracks a query status.
497  * Multiple queries can potentially be executed at the same time by different
498  * controllers. Each query has to track its own status and delays separately,
499  * to avoid conflicting with other ones.
500  */
501 class Query {
502   /**
503    * Initializes the query object.
504    *
505    * @param {object} queryContext
506    *        The query context
507    * @param {object} controller
508    *        The controller to be notified
509    * @param {object} muxer
510    *        The muxer to sort results
511    * @param {Array} providers
512    *        Array of all the providers.
513    */
514   constructor(queryContext, controller, muxer, providers) {
515     this.context = queryContext;
516     this.context.results = [];
517     // Clear any state in the context object, since it could be reused by the
518     // caller and we don't want to port previous query state over.
519     this.context.pendingHeuristicProviders.clear();
520     this.context.deferUserSelectionProviders.clear();
521     this.unsortedResults = [];
522     this.muxer = muxer;
523     this.controller = controller;
524     this.providers = providers;
525     this.started = false;
526     this.canceled = false;
528     // This is used as a last safety filter in add(), thus we keep an unmodified
529     // copy of it.
530     this.acceptableSources = queryContext.sources.slice();
531   }
533   /**
534    * Starts querying.
535    */
536   async start() {
537     if (this.started) {
538       throw new Error("This Query has been started already");
539     }
540     this.started = true;
542     // Check which providers should be queried by calling isActive on them.
543     let activeProviders = [];
544     let activePromises = [];
545     let maxPriority = -1;
546     for (let provider of this.providers) {
547       // This can be used by the provider to check the query is still running
548       // after executing async tasks:
549       //   let instance = this.queryInstance;
550       //   await ...
551       //   if (instance != this.queryInstance) {
552       //     // Query was canceled or a new one started.
553       //     return;
554       //   }
555       provider.queryInstance = this;
556       activePromises.push(
557         // Not all isActive implementations are async, so wrap the call in a
558         // promise so we can be sure we can call `then` on it.  Note that
559         // Promise.resolve returns its arg directly if it's already a promise.
560         Promise.resolve(
561           provider.tryMethod("isActive", this.context, this.controller)
562         )
563           .then(isActive => {
564             if (isActive && !this.canceled) {
565               let priority = provider.tryMethod("getPriority", this.context);
566               if (priority >= maxPriority) {
567                 // The provider's priority is at least as high as the max.
568                 if (priority > maxPriority) {
569                   // The provider's priority is higher than the max.  Remove all
570                   // previously added providers, since their priority is
571                   // necessarily lower, by setting length to zero.
572                   activeProviders.length = 0;
573                   maxPriority = priority;
574                 }
575                 activeProviders.push(provider);
576                 if (provider.deferUserSelection) {
577                   this.context.deferUserSelectionProviders.add(provider.name);
578                 }
579               }
580             }
581           })
582           .catch(ex => lazy.logger.error(ex))
583       );
584     }
586     // We have to wait for all isActive calls to finish because we want to query
587     // only the highest priority active providers as determined by the priority
588     // logic above.
589     await Promise.all(activePromises);
591     if (this.canceled) {
592       this.controller = null;
593       return;
594     }
596     // Start querying active providers.
597     let startQuery = async provider => {
598       provider.logger.debug(
599         `Starting query for "${this.context.searchString}"`
600       );
601       let addedResult = false;
602       await provider.tryMethod("startQuery", this.context, (...args) => {
603         addedResult = true;
604         this.add(...args);
605       });
606       if (!addedResult) {
607         this.context.deferUserSelectionProviders.delete(provider.name);
608       }
609     };
611     let queryPromises = [];
612     for (let provider of activeProviders) {
613       // Track heuristic providers. later we'll use this Set to wait for them
614       // before returning results to the user.
615       if (provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC) {
616         this.context.pendingHeuristicProviders.add(provider.name);
617         queryPromises.push(
618           startQuery(provider).finally(() => {
619             this.context.pendingHeuristicProviders.delete(provider.name);
620           })
621         );
622         continue;
623       }
624       if (!this._sleepTimer) {
625         // Tracks the delay timer. We will fire (in this specific case, cancel
626         // would do the same, since the callback is empty) the timer when the
627         // search is canceled, unblocking start().
628         this._sleepTimer = new lazy.SkippableTimer({
629           name: "Query provider timer",
630           time: lazy.UrlbarPrefs.get("delay"),
631           logger: provider.logger,
632         });
633       }
634       queryPromises.push(
635         this._sleepTimer.promise.then(() =>
636           this.canceled ? undefined : startQuery(provider)
637         )
638       );
639     }
641     lazy.logger.info(
642       `Queried ${queryPromises.length} providers: ${activeProviders.map(
643         p => p.name
644       )}`
645     );
647     // Normally we wait for all the queries, but in case this is canceled we can
648     // return earlier.
649     let cancelPromise = new Promise(resolve => {
650       this._cancelQueries = resolve;
651     });
652     await Promise.race([Promise.all(queryPromises), cancelPromise]);
654     // All the providers are done returning results, so we can stop chunking.
655     if (!this.canceled) {
656       await this._chunkTimer?.fire();
657     }
659     // Break cycles with the controller to avoid leaks.
660     this.controller = null;
661   }
663   /**
664    * Cancels this query. Note: Invoking cancel multiple times is a no-op.
665    */
666   cancel() {
667     if (this.canceled) {
668       return;
669     }
670     this.canceled = true;
671     this.context.deferUserSelectionProviders.clear();
672     for (let provider of this.providers) {
673       provider.logger.debug(
674         `Canceling query for "${this.context.searchString}"`
675       );
676       // Mark the instance as no more valid, see start() for details.
677       provider.queryInstance = null;
678       provider.tryMethod("cancelQuery", this.context);
679     }
680     this._chunkTimer?.cancel().catch(ex => lazy.logger.error(ex));
681     this._sleepTimer?.fire().catch(ex => lazy.logger.error(ex));
682     this._cancelQueries?.();
683   }
685   /**
686    * Adds a result returned from a provider to the results set.
687    *
688    * @param {UrlbarProvider} provider The provider that returned the result.
689    * @param {object} result The result object.
690    */
691   add(provider, result) {
692     if (!(provider instanceof lazy.UrlbarProvider)) {
693       throw new Error("Invalid provider passed to the add callback");
694     }
696     // When this set is empty, we can display heuristic results early. We remove
697     // the provider from the list without checking result.heuristic since
698     // heuristic providers don't necessarily have to return heuristic results.
699     // We expect a provider with type HEURISTIC will return its heuristic
700     // result(s) first.
701     this.context.pendingHeuristicProviders.delete(provider.name);
703     // Stop returning results as soon as we've been canceled.
704     if (this.canceled) {
705       return;
706     }
708     // In search mode, don't allow heuristic results in the following cases
709     // since they don't make sense:
710     //   * When the search string is empty, or
711     //   * In local search mode, except for autofill results
712     if (
713       result.heuristic &&
714       this.context.searchMode &&
715       (!this.context.trimmedSearchString ||
716         (!this.context.searchMode.engineName && !result.autofill))
717     ) {
718       return;
719     }
721     // Check if the result source should be filtered out. Pay attention to the
722     // heuristic result though, that is supposed to be added regardless.
723     if (
724       !this.acceptableSources.includes(result.source) &&
725       !result.heuristic &&
726       // Treat form history as searches for the purpose of acceptableSources.
727       (result.type != lazy.UrlbarUtils.RESULT_TYPE.SEARCH ||
728         result.source != lazy.UrlbarUtils.RESULT_SOURCE.HISTORY ||
729         !this.acceptableSources.includes(lazy.UrlbarUtils.RESULT_SOURCE.SEARCH))
730     ) {
731       return;
732     }
734     // Filter out javascript results for safety. The provider is supposed to do
735     // it, but we don't want to risk leaking these out.
736     if (
737       result.type != lazy.UrlbarUtils.RESULT_TYPE.KEYWORD &&
738       result.payload.url &&
739       result.payload.url.startsWith("javascript:") &&
740       !this.context.searchString.startsWith("javascript:") &&
741       lazy.UrlbarPrefs.get("filter.javascript")
742     ) {
743       return;
744     }
746     result.providerName = provider.name;
747     result.providerType = provider.type;
748     this.unsortedResults.push(result);
750     this._notifyResultsFromProvider(provider);
751   }
753   _notifyResultsFromProvider(provider) {
754     // We use a timer to reduce UI flicker, by adding results in chunks.
755     if (!this._chunkTimer || this._chunkTimer.done) {
756       // Either there's no heuristic provider pending at all, or the previous
757       // timer is done, but we're still getting results. Start a short timer
758       // to chunk remaining results.
759       this._chunkTimer = new lazy.SkippableTimer({
760         name: "chunking",
761         callback: () => this._notifyResults(),
762         time: UrlbarProvidersManager.CHUNK_RESULTS_DELAY_MS,
763         logger: provider.logger,
764       });
765     } else if (
766       !this.context.pendingHeuristicProviders.size &&
767       provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.HEURISTIC
768     ) {
769       // All the active heuristic providers have returned results, we can skip
770       // the heuristic chunk timer and start showing results immediately.
771       this._chunkTimer.fire().catch(ex => lazy.logger.error(ex));
772     }
774     // Otherwise some timer is still ongoing and we'll wait for it.
775   }
777   _notifyResults() {
778     this.muxer.sort(this.context, this.unsortedResults);
779     // We don't want to notify consumers if there are no results since they
780     // generally expect at least one result when notified, so bail, but only
781     // after nulling out the chunk timer above so that it will be restarted
782     // the next time results are added.
783     if (!this.context.results.length) {
784       return;
785     }
787     this.context.firstResultChanged = !lazy.ObjectUtils.deepEqual(
788       this.context.firstResult,
789       this.context.results[0]
790     );
791     this.context.firstResult = this.context.results[0];
793     if (this.controller) {
794       this.controller.receiveResults(this.context);
795     }
796   }
800  * Updates in place the sources for a given UrlbarQueryContext.
802  * @param {UrlbarQueryContext} context The query context to examine
803  * @returns {object} The restriction token that was used to set sources, or
804  *          undefined if there's no restriction token.
805  */
806 function updateSourcesIfEmpty(context) {
807   if (context.sources && context.sources.length) {
808     return false;
809   }
810   let acceptedSources = [];
811   // There can be only one restrict token per query.
812   let restrictToken = context.tokens.find(t =>
813     [
814       lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY,
815       lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK,
816       lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG,
817       lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE,
818       lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH,
819       lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE,
820       lazy.UrlbarTokenizer.TYPE.RESTRICT_URL,
821       lazy.UrlbarTokenizer.TYPE.RESTRICT_ACTION,
822     ].includes(t.type)
823   );
825   // RESTRICT_TITLE and RESTRICT_URL do not affect query sources.
826   let restrictTokenType =
827     restrictToken &&
828     restrictToken.type != lazy.UrlbarTokenizer.TYPE.RESTRICT_TITLE &&
829     restrictToken.type != lazy.UrlbarTokenizer.TYPE.RESTRICT_URL
830       ? restrictToken.type
831       : undefined;
833   for (let source of Object.values(lazy.UrlbarUtils.RESULT_SOURCE)) {
834     // Skip sources that the context doesn't care about.
835     if (context.sources && !context.sources.includes(source)) {
836       continue;
837     }
838     // Check prefs and restriction tokens.
839     switch (source) {
840       case lazy.UrlbarUtils.RESULT_SOURCE.BOOKMARKS:
841         if (
842           restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK ||
843           restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_TAG ||
844           (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.bookmark"))
845         ) {
846           acceptedSources.push(source);
847         }
848         break;
849       case lazy.UrlbarUtils.RESULT_SOURCE.HISTORY:
850         if (
851           restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_HISTORY ||
852           (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.history"))
853         ) {
854           acceptedSources.push(source);
855         }
856         break;
857       case lazy.UrlbarUtils.RESULT_SOURCE.SEARCH:
858         if (
859           restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_SEARCH ||
860           !restrictTokenType
861         ) {
862           // We didn't check browser.urlbar.suggest.searches here, because it
863           // just controls search suggestions. If a search suggestion arrives
864           // here, we lost already, because we broke user's privacy by hitting
865           // the network. Thus, it's better to leave things go through and
866           // notice the bug, rather than hiding it with a filter.
867           acceptedSources.push(source);
868         }
869         break;
870       case lazy.UrlbarUtils.RESULT_SOURCE.TABS:
871         if (
872           restrictTokenType === lazy.UrlbarTokenizer.TYPE.RESTRICT_OPENPAGE ||
873           (!restrictTokenType && lazy.UrlbarPrefs.get("suggest.openpage"))
874         ) {
875           acceptedSources.push(source);
876         }
877         break;
878       case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK:
879         if (!context.isPrivate && !restrictTokenType) {
880           acceptedSources.push(source);
881         }
882         break;
883       case lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL:
884       case lazy.UrlbarUtils.RESULT_SOURCE.ADDON:
885       default:
886         if (!restrictTokenType) {
887           acceptedSources.push(source);
888         }
889         break;
890     }
891   }
892   context.sources = acceptedSources;
893   return restrictToken;