Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / urlbar / UrlbarMuxerUnifiedComplete.sys.mjs
blob1657437427a26bf0f835b08921fe5d41858699a7
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 sort results in a UrlbarQueryContext.
7  */
9 import {
10   UrlbarMuxer,
11   UrlbarUtils,
12 } from "resource:///modules/UrlbarUtils.sys.mjs";
14 const lazy = {};
16 ChromeUtils.defineESModuleGetters(lazy, {
17   QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
18   UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
19   UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
20   UrlbarProviderQuickSuggest:
21     "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
22   UrlbarProviderTabToSearch:
23     "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
24   UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
25 });
27 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
28   UrlbarUtils.getLogger({ prefix: "MuxerUnifiedComplete" })
31 const MS_PER_DAY = 1000 * 60 * 60 * 24;
33 /**
34  * Constructs the map key by joining the url with the userContextId if
35  * 'browser.urlbar.switchTabs.searchAllContainers' is set to true.
36  * Otherwise, just the url is used.
37  *
38  * @param   {UrlbarResult} result The result object.
39  * @returns {string} map key
40  */
41 function makeMapKeyForTabResult(result) {
42   return UrlbarUtils.tupleString(
43     result.payload.url,
44     lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
45       result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
46       lazy.UrlbarProviderOpenTabs.isNonPrivateUserContextId(
47         result.payload.userContextId
48       )
49       ? result.payload.userContextId
50       : undefined
51   );
54 /**
55  * Class used to create a muxer.
56  * The muxer receives and sorts results in a UrlbarQueryContext.
57  */
58 class MuxerUnifiedComplete extends UrlbarMuxer {
59   constructor() {
60     super();
61   }
63   get name() {
64     return "UnifiedComplete";
65   }
67   /**
68    * Sorts results in the given UrlbarQueryContext.
69    *
70    * @param {UrlbarQueryContext} context
71    *   The query context.
72    * @param {Array} unsortedResults
73    *   The array of UrlbarResult that is not sorted yet.
74    */
75   sort(context, unsortedResults) {
76     // This method is called multiple times per keystroke, so it should be as
77     // fast and efficient as possible.  We do two passes through the results:
78     // one to collect state for the second pass, and then a second to build the
79     // sorted list of results.  If you find yourself writing something like
80     // context.results.find(), filter(), sort(), etc., modify one or both passes
81     // instead.
83     // Global state we'll use to make decisions during this sort.
84     let state = {
85       context,
86       // RESULT_GROUP => array of results belonging to the group, excluding
87       // group-relative suggestedIndex results
88       resultsByGroup: new Map(),
89       // RESULT_GROUP => array of group-relative suggestedIndex results
90       // belonging to the group
91       suggestedIndexResultsByGroup: new Map(),
92       // This is analogous to `maxResults` except it's the total available
93       // result span instead of the total available result count. We'll add
94       // results until `usedResultSpan` would exceed `availableResultSpan`.
95       availableResultSpan: context.maxResults,
96       // The total result span taken up by all global (non-group-relative)
97       // suggestedIndex results.
98       globalSuggestedIndexResultSpan: 0,
99       // The total span of results that have been added so far.
100       usedResultSpan: 0,
101       strippedUrlToTopPrefixAndTitle: new Map(),
102       baseAndTitleToTopRef: new Map(),
103       urlToTabResultType: new Map(),
104       addedRemoteTabUrls: new Set(),
105       addedSwitchTabUrls: new Set(),
106       addedResultUrls: new Set(),
107       canShowPrivateSearch: unsortedResults.length > 1,
108       canShowTailSuggestions: true,
109       // Form history and remote suggestions added so far.  Used for deduping
110       // suggestions.  Also includes the heuristic query string if the heuristic
111       // is a search result.  All strings in the set are lowercased.
112       suggestions: new Set(),
113       canAddTabToSearch: true,
114       hasUnitConversionResult: false,
115       maxHeuristicResultSpan: 0,
116       maxTabToSearchResultSpan: 0,
117       // When you add state, update _copyState() as necessary.
118     };
120     // Do the first pass over all results to build some state.
121     for (let result of unsortedResults) {
122       // Add each result to the appropriate `resultsByGroup` map.
123       let group = UrlbarUtils.getResultGroup(result);
124       let resultsByGroup =
125         result.hasSuggestedIndex && result.isSuggestedIndexRelativeToGroup
126           ? state.suggestedIndexResultsByGroup
127           : state.resultsByGroup;
128       let results = resultsByGroup.get(group);
129       if (!results) {
130         results = [];
131         resultsByGroup.set(group, results);
132       }
133       results.push(result);
135       // Update pre-add state.
136       this._updateStatePreAdd(result, state);
137     }
139     // Now that the first pass is done, adjust the available result span. More
140     // than one tab-to-search result may be present but only one will be shown;
141     // add the max TTS span to the total span of global suggestedIndex results.
142     state.globalSuggestedIndexResultSpan += state.maxTabToSearchResultSpan;
144     // Leave room for global suggestedIndex results at the end of the sort, by
145     // subtracting their total span from the total available span. For very
146     // small values of `maxRichResults`, their total span may be larger than
147     // `state.availableResultSpan`.
148     let globalSuggestedIndexAvailableSpan = Math.min(
149       state.availableResultSpan,
150       state.globalSuggestedIndexResultSpan
151     );
152     state.availableResultSpan -= globalSuggestedIndexAvailableSpan;
154     if (state.maxHeuristicResultSpan) {
155       if (lazy.UrlbarPrefs.get("experimental.hideHeuristic")) {
156         // The heuristic is hidden. The muxer will include it but the view will
157         // hide it. Increase the available span to compensate so that the total
158         // visible span accurately reflects `context.maxResults`.
159         state.availableResultSpan += state.maxHeuristicResultSpan;
160       } else if (context.maxResults > 0) {
161         // `context.maxResults` is positive. Make sure there's room for the
162         // heuristic even if it means exceeding `context.maxResults`.
163         state.availableResultSpan = Math.max(
164           state.availableResultSpan,
165           state.maxHeuristicResultSpan
166         );
167       }
168     }
170     // Show Top Sites above trending results.
171     let showSearchSuggestionsFirst =
172       context.searchString ||
173       (!lazy.UrlbarPrefs.get("suggest.trending") &&
174         !lazy.UrlbarPrefs.get("suggest.recentsearches"));
176     // Determine the result groups to use for this sort.  In search mode with
177     // an engine, show search suggestions first.
178     let rootGroup =
179       context.searchMode?.engineName || !showSearchSuggestionsFirst
180         ? lazy.UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst })
181         : lazy.UrlbarPrefs.resultGroups;
182     lazy.logger.debug("Root groups", rootGroup);
184     // Fill the root group.
185     let [sortedResults] = this._fillGroup(
186       rootGroup,
187       { availableSpan: state.availableResultSpan, maxResultCount: Infinity },
188       state
189     );
191     // Add global suggestedIndex results.
192     let globalSuggestedIndexResults = state.resultsByGroup.get(
193       UrlbarUtils.RESULT_GROUP.SUGGESTED_INDEX
194     );
195     if (globalSuggestedIndexResults) {
196       this._addSuggestedIndexResults(
197         globalSuggestedIndexResults,
198         sortedResults,
199         {
200           availableSpan: globalSuggestedIndexAvailableSpan,
201           maxResultCount: Infinity,
202         },
203         state
204       );
205     }
207     context.results = sortedResults;
208   }
210   /**
211    * Returns a *deep* copy of state (except for `state.context`, which is
212    * shallow copied).  i.e., any Maps, Sets, and arrays in the state should be
213    * recursively copied so that the original state is not modified when the copy
214    * is modified.
215    *
216    * @param {object} state
217    *   The muxer state to copy.
218    * @returns {object}
219    *   A deep copy of the state.
220    */
221   _copyState(state) {
222     let copy = Object.assign({}, state, {
223       resultsByGroup: new Map(),
224       suggestedIndexResultsByGroup: new Map(),
225       strippedUrlToTopPrefixAndTitle: new Map(
226         state.strippedUrlToTopPrefixAndTitle
227       ),
228       baseAndTitleToTopRef: new Map(state.baseAndTitleToTopRef),
229       urlToTabResultType: new Map(state.urlToTabResultType),
230       addedRemoteTabUrls: new Set(state.addedRemoteTabUrls),
231       addedSwitchTabUrls: new Set(state.addedSwitchTabUrls),
232       suggestions: new Set(state.suggestions),
233       addedResultUrls: new Set(state.addedResultUrls),
234     });
236     // Deep copy the `resultsByGroup` maps.
237     for (let key of ["resultsByGroup", "suggestedIndexResultsByGroup"]) {
238       for (let [group, results] of state[key]) {
239         copy[key].set(group, [...results]);
240       }
241     }
243     return copy;
244   }
246   /**
247    * Recursively fills a result group and its children.
248    *
249    * There are two ways to limit the number of results in a group:
250    *
251    * (1) By max total result span using the `availableSpan` property. The group
252    * will be filled so that the total span of its results does not exceed this
253    * value.
254    *
255    * (2) By max total result count using the `maxResultCount` property. The
256    * group will be filled so that the total number of its results does not
257    * exceed this value.
258    *
259    * Both `availableSpan` and `maxResultCount` may be defined, and the group's
260    * results will be capped to whichever limit is reached first. If either is
261    * not defined, then the group inherits that limit from its parent group.
262    *
263    * In addition to limiting their total number of results, groups can also
264    * control the composition of their child groups by using flex ratios. A group
265    * can define a `flexChildren: true` property, and in that case each of its
266    * children should have a `flex` property. Each child will be filled according
267    * to the ratio of its flex value and the sum of the flex values of all the
268    * children, similar to HTML flexbox. If some children do not fill up but
269    * others do, the filled-up children will be allowed to grow to use up the
270    * unfilled space.
271    *
272    * @param {object} group
273    *   The result group to fill.
274    * @param {object} limits
275    *   An object with optional `availableSpan` and `maxResultCount` properties
276    *   as described above. They will be used as the limits for the group.
277    * @param {object} state
278    *   The muxer state.
279    * @returns {Array}
280    *   `[results, usedLimits, hasMoreResults]` -- see `_addResults`.
281    */
282   _fillGroup(group, limits, state) {
283     // Get the group's suggestedIndex results. Reminder: `group.group` is a
284     // `RESULT_GROUP` constant.
285     let suggestedIndexResults;
286     let suggestedIndexAvailableSpan = 0;
287     let suggestedIndexAvailableCount = 0;
288     if ("group" in group) {
289       let results = state.suggestedIndexResultsByGroup.get(group.group);
290       if (results) {
291         // Subtract them from the group's limits so there will be room for them
292         // later. Discard results that can't be added.
293         let span = 0;
294         let resultCount = 0;
295         for (let result of results) {
296           if (this._canAddResult(result, state)) {
297             suggestedIndexResults ??= [];
298             suggestedIndexResults.push(result);
299             const spanSize = UrlbarUtils.getSpanForResult(result);
300             span += spanSize;
301             if (spanSize) {
302               resultCount++;
303             }
304           }
305         }
307         suggestedIndexAvailableSpan = Math.min(limits.availableSpan, span);
308         suggestedIndexAvailableCount = Math.min(
309           limits.maxResultCount,
310           resultCount
311         );
313         // Create a new `limits` object so we don't modify the caller's.
314         limits = { ...limits };
315         limits.availableSpan -= suggestedIndexAvailableSpan;
316         limits.maxResultCount -= suggestedIndexAvailableCount;
317       }
318     }
320     // Fill the group. If it has children, fill them recursively. Otherwise fill
321     // the group directly.
322     let [results, usedLimits, hasMoreResults] = group.children
323       ? this._fillGroupChildren(group, limits, state)
324       : this._addResults(group.group, limits, state);
326     // Add the group's suggestedIndex results.
327     if (suggestedIndexResults) {
328       let suggestedIndexUsedLimits = this._addSuggestedIndexResults(
329         suggestedIndexResults,
330         results,
331         {
332           availableSpan: suggestedIndexAvailableSpan,
333           maxResultCount: suggestedIndexAvailableCount,
334         },
335         state
336       );
337       for (let [key, value] of Object.entries(suggestedIndexUsedLimits)) {
338         usedLimits[key] += value;
339       }
340     }
342     return [results, usedLimits, hasMoreResults];
343   }
345   /**
346    * Helper for `_fillGroup` that fills a group's children.
347    *
348    * @param {object} group
349    *   The result group to fill. It's assumed to have a `children` property.
350    * @param {object} limits
351    *   An object with optional `availableSpan` and `maxResultCount` properties
352    *   as described in `_fillGroup`.
353    * @param {object} state
354    *   The muxer state.
355    * @param {Array} flexDataArray
356    *   See `_updateFlexData`.
357    * @returns {Array}
358    *   `[results, usedLimits, hasMoreResults]` -- see `_addResults`.
359    */
360   _fillGroupChildren(group, limits, state, flexDataArray = null) {
361     // If the group has flexed children, update the data we use during flex
362     // calculations.
363     //
364     // Handling flex is complicated so we discuss it briefly. We may do multiple
365     // passes for a group with flexed children in order to try to optimally fill
366     // them. If after one pass some children do not fill up but others do, we'll
367     // do another pass that tries to overfill the filled-up children while still
368     // respecting their flex ratios. We'll continue to do passes until all
369     // children stop filling up or we reach the parent's limits. The way we
370     // overfill children is by increasing their individual limits to make up for
371     // the unused space in their underfilled siblings. Before starting a new
372     // pass, we discard the results from the current pass so the new pass starts
373     // with a clean slate. That means we need to copy the global sort state
374     // (`state`) before modifying it in the current pass so we can use its
375     // original value in the next pass [1].
376     //
377     // [1] Instead of starting each pass with a clean slate in this way, we
378     // could accumulate results with each pass since we only ever add results to
379     // flexed children and never remove them. However, that would subvert muxer
380     // logic related to the global state (deduping, `_canAddResult`) since we
381     // generally assume the muxer adds results in the order they appear.
382     let stateCopy;
383     if (group.flexChildren) {
384       stateCopy = this._copyState(state);
385       flexDataArray = this._updateFlexData(group, limits, flexDataArray);
386     }
388     // Fill each child group, collecting all results in the `results` array.
389     let results = [];
390     let usedLimits = {};
391     for (let key of Object.keys(limits)) {
392       usedLimits[key] = 0;
393     }
394     let anyChildUnderfilled = false;
395     let anyChildHasMoreResults = false;
396     for (let i = 0; i < group.children.length; i++) {
397       let child = group.children[i];
398       let flexData = flexDataArray?.[i];
400       // Compute the child's limits.
401       let childLimits = {};
402       for (let key of Object.keys(limits)) {
403         childLimits[key] = flexData
404           ? flexData.limits[key]
405           : Math.min(
406               typeof child[key] == "number" ? child[key] : Infinity,
407               limits[key] - usedLimits[key]
408             );
409       }
411       // Recurse and fill the child.
412       let [childResults, childUsedLimits, childHasMoreResults] =
413         this._fillGroup(child, childLimits, state);
414       results = results.concat(childResults);
415       for (let key of Object.keys(usedLimits)) {
416         usedLimits[key] += childUsedLimits[key];
417       }
418       anyChildHasMoreResults = anyChildHasMoreResults || childHasMoreResults;
420       if (flexData?.hasMoreResults) {
421         // The child is flexed and we possibly added more results to it.
422         flexData.usedLimits = childUsedLimits;
423         flexData.hasMoreResults = childHasMoreResults;
424         anyChildUnderfilled =
425           anyChildUnderfilled ||
426           (!childHasMoreResults &&
427             [...Object.entries(childLimits)].every(
428               ([key, limit]) => flexData.usedLimits[key] < limit
429             ));
430       }
431     }
433     // If the children are flexed and some underfilled but others still have
434     // more results, do another pass.
435     if (anyChildUnderfilled && anyChildHasMoreResults) {
436       [results, usedLimits, anyChildHasMoreResults] = this._fillGroupChildren(
437         group,
438         limits,
439         stateCopy,
440         flexDataArray
441       );
443       // Update `state` in place so that it's also updated in the caller.
444       for (let [key, value] of Object.entries(stateCopy)) {
445         state[key] = value;
446       }
447     }
449     return [results, usedLimits, anyChildHasMoreResults];
450   }
452   /**
453    * Updates flex-related state used while filling a group.
454    *
455    * @param {object} group
456    *   The result group being filled.
457    * @param {object} limits
458    *   An object defining the group's limits as described in `_fillGroup`.
459    * @param {Array} flexDataArray
460    *   An array parallel to `group.children`. The object at index i corresponds
461    *   to the child in `group.children` at index i. Each object maintains some
462    *   flex-related state for its child and is updated during each pass in
463    *   `_fillGroup` for `group`. When this method is called in the first pass,
464    *   this argument should be null, and the method will create and return a new
465    *   `flexDataArray` array that should be used in the remainder of the first
466    *   pass and all subsequent passes.
467    * @returns {Array}
468    *   A new `flexDataArray` when called in the first pass, and `flexDataArray`
469    *   itself when called in subsequent passes.
470    */
471   _updateFlexData(group, limits, flexDataArray) {
472     flexDataArray =
473       flexDataArray ||
474       group.children.map((child, index) => {
475         let data = {
476           // The index of the corresponding child in `group.children`.
477           index,
478           // The child's limits.
479           limits: {},
480           // The fractional parts of the child's unrounded limits; see below.
481           limitFractions: {},
482           // The used-up portions of the child's limits.
483           usedLimits: {},
484           // True if `state.resultsByGroup` has more results of the child's
485           // `RESULT_GROUP`. This is not related to the child's limits.
486           hasMoreResults: true,
487           // The child's flex value.
488           flex: typeof child.flex == "number" ? child.flex : 0,
489         };
490         for (let key of Object.keys(limits)) {
491           data.limits[key] = 0;
492           data.limitFractions[key] = 0;
493           data.usedLimits[key] = 0;
494         }
495         return data;
496       });
498     // The data objects for children with more results (i.e., that are still
499     // fillable).
500     let fillableDataArray = [];
502     // The sum of the flex values of children with more results.
503     let fillableFlexSum = 0;
505     for (let data of flexDataArray) {
506       if (data.hasMoreResults) {
507         fillableFlexSum += data.flex;
508         fillableDataArray.push(data);
509       }
510     }
512     // Update each limit.
513     for (let [key, limit] of Object.entries(limits)) {
514       // Calculate the group's limit only including children with more results.
515       let fillableLimit = limit;
516       for (let data of flexDataArray) {
517         if (!data.hasMoreResults) {
518           fillableLimit -= data.usedLimits[key];
519         }
520       }
522       // Allow for the possibility that some children may have gone over limit.
523       // `fillableLimit` will be negative in that case.
524       fillableLimit = Math.max(fillableLimit, 0);
526       // Next we'll compute the limits of children with more results. This value
527       // is the sum of those limits. It may differ from `fillableLimit` due to
528       // the fact that each individual child limit must be an integer.
529       let summedFillableLimit = 0;
531       // Compute the limits of children with more results. If there are also
532       // children that don't have more results, then these new limits will be
533       // larger than they were in the previous pass.
534       for (let data of fillableDataArray) {
535         let unroundedLimit = fillableLimit * (data.flex / fillableFlexSum);
536         // `limitFraction` is the fractional part of the unrounded ideal limit.
537         // e.g., for 5.234 it will be 0.234. We use this to minimize the
538         // mathematical error when tweaking limits below.
539         data.limitFractions[key] = unroundedLimit - Math.floor(unroundedLimit);
540         data.limits[key] = Math.round(unroundedLimit);
541         summedFillableLimit += data.limits[key];
542       }
544       // As mentioned above, the sum of the individual child limits may not
545       // equal the group's fillable limit. If the sum is smaller, the group will
546       // end up with too few results. If it's larger, the group will have the
547       // correct number of results (since we stop adding results once limits are
548       // reached) but it may end up with a composition that does not reflect the
549       // child flex ratios as accurately as possible.
550       //
551       // In either case, tweak the individual limits so that (1) their sum
552       // equals the group's fillable limit, and (2) the composition respects the
553       // flex ratios with as little mathematical error as possible.
554       if (summedFillableLimit != fillableLimit) {
555         // Collect the flex datas with a non-zero limit fractions. We'll round
556         // them up or down depending on whether the sum is larger or smaller
557         // than the group's fillable limit.
558         let fractionalDataArray = fillableDataArray.filter(
559           data => data.limitFractions[key]
560         );
562         let diff;
563         if (summedFillableLimit < fillableLimit) {
564           // The sum is smaller. We'll increment individual limits until the sum
565           // is equal, starting with the child whose limit fraction is closest
566           // to 1 in order to minimize error.
567           diff = 1;
568           fractionalDataArray.sort((a, b) => {
569             // Sort by fraction descending so larger fractions are first.
570             let cmp = b.limitFractions[key] - a.limitFractions[key];
571             // Secondarily sort by index ascending so that children with the
572             // same fraction are incremented in the order they appear, allowing
573             // earlier children to have larger spans.
574             return cmp || a.index - b.index;
575           });
576         } else if (fillableLimit < summedFillableLimit) {
577           // The sum is larger. We'll decrement individual limits until the sum
578           // is equal, starting with the child whose limit fraction is closest
579           // to 0 in order to minimize error.
580           diff = -1;
581           fractionalDataArray.sort((a, b) => {
582             // Sort by fraction ascending so smaller fractions are first.
583             let cmp = a.limitFractions[key] - b.limitFractions[key];
584             // Secondarily sort by index descending so that children with the
585             // same fraction are decremented in reverse order, allowing earlier
586             // children to retain larger spans.
587             return cmp || b.index - a.index;
588           });
589         }
591         // Now increment or decrement individual limits until their sum is equal
592         // to the group's fillable limit.
593         while (summedFillableLimit != fillableLimit) {
594           if (!fractionalDataArray.length) {
595             // This shouldn't happen, but don't let it break us.
596             lazy.logger.error("fractionalDataArray is empty!");
597             break;
598           }
599           let data = flexDataArray[fractionalDataArray.shift().index];
600           data.limits[key] += diff;
601           summedFillableLimit += diff;
602         }
603       }
604     }
606     return flexDataArray;
607   }
609   /**
610    * Adds results to a group using the results from its `RESULT_GROUP` in
611    * `state.resultsByGroup`.
612    *
613    * @param {UrlbarUtils.RESULT_GROUP} groupConst
614    *   The group's `RESULT_GROUP`.
615    * @param {object} limits
616    *   An object defining the group's limits as described in `_fillGroup`.
617    * @param {object} state
618    *   Global state that we use to make decisions during this sort.
619    * @returns {Array}
620    *   `[results, usedLimits, hasMoreResults]` where:
621    *     results: A flat array of results in the group, empty if no results
622    *       were added.
623    *     usedLimits: An object defining the amount of each limit that the
624    *       results use. For each possible limit property (see `_fillGroup`),
625    *       there will be a corresponding property in this object. For example,
626    *       if 3 results are added with a total span of 4, then this object will
627    *       be: { maxResultCount: 3, availableSpan: 4 }
628    *     hasMoreResults: True if `state.resultsByGroup` has more results of
629    *       the same `RESULT_GROUP`. This is not related to the group's limits.
630    */
631   _addResults(groupConst, limits, state) {
632     let usedLimits = {};
633     for (let key of Object.keys(limits)) {
634       usedLimits[key] = 0;
635     }
637     // For form history, maxHistoricalSearchSuggestions == 0 implies the user
638     // has opted out of form history completely, so we override the max result
639     // count here in that case. Other values of maxHistoricalSearchSuggestions
640     // are ignored and we use the flex defined on the form history group.
641     if (
642       groupConst == UrlbarUtils.RESULT_GROUP.FORM_HISTORY &&
643       !lazy.UrlbarPrefs.get("maxHistoricalSearchSuggestions")
644     ) {
645       // Create a new `limits` object so we don't modify the caller's.
646       limits = { ...limits };
647       limits.maxResultCount = 0;
648     }
650     let addedResults = [];
651     let groupResults = state.resultsByGroup.get(groupConst);
652     while (
653       groupResults?.length &&
654       state.usedResultSpan < state.availableResultSpan &&
655       [...Object.entries(limits)].every(([k, limit]) => usedLimits[k] < limit)
656     ) {
657       let result = groupResults[0];
658       if (this._canAddResult(result, state)) {
659         if (!this.#updateUsedLimits(result, limits, usedLimits, state)) {
660           // Adding the result would exceed the group's available span, so stop
661           // adding results to it. Skip the shift() below so the result can be
662           // added to later groups.
663           break;
664         }
665         addedResults.push(result);
666       }
668       // We either add or discard results in the order they appear in
669       // `groupResults`, so shift() them off. That way later groups with the
670       // same `RESULT_GROUP` won't include results that earlier groups have
671       // added or discarded.
672       groupResults.shift();
673     }
675     return [addedResults, usedLimits, !!groupResults?.length];
676   }
678   /**
679    * Returns whether a result can be added to its group given the current sort
680    * state.
681    *
682    * @param {UrlbarResult} result
683    *   The result.
684    * @param {object} state
685    *   Global state that we use to make decisions during this sort.
686    * @returns {boolean}
687    *   True if the result can be added and false if it should be discarded.
688    */
689   // TODO (Bug 1741273): Refactor this method to avoid an eslint complexity
690   // error or increase the complexity threshold.
691   // eslint-disable-next-line complexity
692   _canAddResult(result, state) {
693     // Typically the first visible Suggest result is always added.
694     if (result.providerName == lazy.UrlbarProviderQuickSuggest.name) {
695       if (result.isHiddenExposure) {
696         // Always allow hidden exposure Suggest results.
697         return true;
698       }
700       if (state.quickSuggestResult && state.quickSuggestResult != result) {
701         // A Suggest result was already added.
702         return false;
703       }
705       // Don't add navigational suggestions that dupe the heuristic.
706       let heuristicUrl = state.context.heuristicResult?.payload.url;
707       if (
708         heuristicUrl &&
709         result.payload.telemetryType == "top_picks" &&
710         !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
711       ) {
712         let opts = {
713           stripHttp: true,
714           stripHttps: true,
715           stripWww: true,
716           trimSlash: true,
717         };
718         result.payload.dupedHeuristic =
719           UrlbarUtils.stripPrefixAndTrim(heuristicUrl, opts)[0] ==
720           UrlbarUtils.stripPrefixAndTrim(result.payload.url, opts)[0];
721         return !result.payload.dupedHeuristic;
722       }
724       return true;
725     }
727     // We expect UrlbarProviderPlaces sent us the highest-ranked www. and non-www
728     // origins, if any. Now, compare them to each other and to the heuristic
729     // result.
730     //
731     // 1. If the heuristic result is lower ranked than both, discard the www
732     //    origin, unless it has a different page title than the non-www
733     //    origin. This is a guard against deduping when www.site.com and
734     //    site.com have different content.
735     // 2. If the heuristic result is higher than either the www origin or
736     //    non-www origin:
737     //    2a. If the heuristic is a www origin, discard the non-www origin.
738     //    2b. If the heuristic is a non-www origin, discard the www origin.
739     if (
740       !result.heuristic &&
741       result.type == UrlbarUtils.RESULT_TYPE.URL &&
742       result.payload.url
743     ) {
744       let [strippedUrl, prefix] = UrlbarUtils.stripPrefixAndTrim(
745         result.payload.url,
746         {
747           stripHttp: true,
748           stripHttps: true,
749           stripWww: true,
750           trimEmptyQuery: true,
751         }
752       );
753       let topPrefixData = state.strippedUrlToTopPrefixAndTitle.get(strippedUrl);
754       // If the condition below is not met, we are deduping a result against
755       // itself.
756       if (
757         topPrefixData &&
758         (prefix != topPrefixData.prefix ||
759           result.providerName != topPrefixData.providerName)
760       ) {
761         let prefixRank = UrlbarUtils.getPrefixRank(prefix);
762         if (
763           (prefixRank < topPrefixData.rank &&
764             (prefix.endsWith("www.") == topPrefixData.prefix.endsWith("www.") ||
765               result.payload?.title == topPrefixData.title)) ||
766           (prefix == topPrefixData.prefix &&
767             result.providerName != topPrefixData.providerName)
768         ) {
769           return false;
770         }
771       }
772     }
774     // Discard results that dupe autofill.
775     if (
776       state.context.heuristicResult &&
777       state.context.heuristicResult.autofill &&
778       !result.autofill &&
779       state.context.heuristicResult.payload?.url == result.payload.url &&
780       state.context.heuristicResult.type == result.type &&
781       !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
782     ) {
783       return false;
784     }
786     // HeuristicFallback may add non-heuristic results in some cases, but those
787     // should be retained only if the heuristic result comes from it.
788     if (
789       !result.heuristic &&
790       result.providerName == "HeuristicFallback" &&
791       state.context.heuristicResult?.providerName != "HeuristicFallback"
792     ) {
793       return false;
794     }
796     if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
797       // Discard the result if a tab-to-search result was added already.
798       if (!state.canAddTabToSearch) {
799         return false;
800       }
802       // In cases where the heuristic result is not a URL and we have a
803       // tab-to-search result, the tab-to-search provider determined that the
804       // typed string is similar to an engine domain. We can let the
805       // tab-to-search result through.
806       if (state.context.heuristicResult?.type == UrlbarUtils.RESULT_TYPE.URL) {
807         // Discard the result if the heuristic result is not autofill and we are
808         // not making an exception for a fuzzy match.
809         if (
810           !state.context.heuristicResult.autofill &&
811           !result.payload.satisfiesAutofillThreshold
812         ) {
813           return false;
814         }
816         let autofillHostname = new URL(
817           state.context.heuristicResult.payload.url
818         ).hostname;
819         let [autofillDomain] = UrlbarUtils.stripPrefixAndTrim(
820           autofillHostname,
821           {
822             stripWww: true,
823           }
824         );
825         // Strip the public suffix because we want to allow matching "domain.it"
826         // with "domain.com".
827         autofillDomain = UrlbarUtils.stripPublicSuffixFromHost(autofillDomain);
828         if (!autofillDomain) {
829           return false;
830         }
832         // `searchUrlDomainWithoutSuffix` is the engine's domain with the public
833         // suffix already stripped, for example "www.mozilla.".
834         let [engineDomain] = UrlbarUtils.stripPrefixAndTrim(
835           result.payload.searchUrlDomainWithoutSuffix,
836           {
837             stripWww: true,
838           }
839         );
840         // Discard if the engine domain does not end with the autofilled one.
841         if (!engineDomain.endsWith(autofillDomain)) {
842           return false;
843         }
844       }
845     }
847     // Discard "Search in a Private Window" if appropriate.
848     if (
849       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
850       result.payload.inPrivateWindow &&
851       !state.canShowPrivateSearch
852     ) {
853       return false;
854     }
856     // Discard form history and remote suggestions that dupe previously added
857     // suggestions or the heuristic. We do not deduplicate rich suggestions so
858     // they do not visually disapear as the suggestion is completed and
859     // becomes the same url as the heuristic result.
860     if (
861       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
862       result.payload.lowerCaseSuggestion &&
863       !result.isRichSuggestion
864     ) {
865       let suggestion = result.payload.lowerCaseSuggestion.trim();
866       if (!suggestion || state.suggestions.has(suggestion)) {
867         return false;
868       }
869     }
871     // Discard tail suggestions if appropriate.
872     if (
873       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
874       result.payload.tail &&
875       !result.isRichSuggestion &&
876       !state.canShowTailSuggestions
877     ) {
878       return false;
879     }
881     // Discard remote tab results that dupes another remote tab or a
882     // switch-to-tab result.
883     if (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
884       if (state.addedRemoteTabUrls.has(result.payload.url)) {
885         return false;
886       }
887       let maybeDupeType = state.urlToTabResultType.get(result.payload.url);
888       if (maybeDupeType == UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
889         return false;
890       }
891     }
893     // Discard switch-to-tab results that dupes another switch-to-tab result.
894     if (
895       result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
896       state.addedSwitchTabUrls.has(makeMapKeyForTabResult(result))
897     ) {
898       return false;
899     }
901     // Discard history results that dupe either remote or switch-to-tab results.
902     if (
903       !result.heuristic &&
904       result.type == UrlbarUtils.RESULT_TYPE.URL &&
905       result.payload.url &&
906       state.urlToTabResultType.has(result.payload.url)
907     ) {
908       return false;
909     }
911     // Discard SERPs from browser history that dupe either the heuristic or
912     // previously added suggestions.
913     if (
914       result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
915       result.type == UrlbarUtils.RESULT_TYPE.URL &&
916       // If there's no suggestions, we're not going to have anything to match
917       // against, so avoid processing the url.
918       state.suggestions.size
919     ) {
920       let submission = Services.search.parseSubmissionURL(result.payload.url);
921       if (submission) {
922         let resultQuery = submission.terms.trim().toLocaleLowerCase();
923         if (state.suggestions.has(resultQuery)) {
924           // If the result's URL is the same as a brand new SERP URL created
925           // from the query string modulo certain URL params, then treat the
926           // result as a dupe and discard it.
927           let [newSerpURL] = UrlbarUtils.getSearchQueryUrl(
928             submission.engine,
929             submission.terms
930           );
931           if (
932             lazy.UrlbarSearchUtils.serpsAreEquivalent(
933               result.payload.url,
934               newSerpURL
935             )
936           ) {
937             return false;
938           }
939         }
940       }
941     }
943     // When in an engine search mode, discard URL results whose hostnames don't
944     // include the root domain of the search mode engine.
945     if (state.context.searchMode?.engineName && result.payload.url) {
946       let engine = Services.search.getEngineByName(
947         state.context.searchMode.engineName
948       );
949       if (engine) {
950         let searchModeRootDomain =
951           lazy.UrlbarSearchUtils.getRootDomainFromEngine(engine);
952         let resultUrl = new URL(result.payload.url);
953         // Add a trailing "." to increase the stringency of the check. This
954         // check covers most general cases. Some edge cases are not covered,
955         // like `resultUrl` being ebay.mydomain.com, which would escape this
956         // check if `searchModeRootDomain` was "ebay".
957         if (!resultUrl.hostname.includes(`${searchModeRootDomain}.`)) {
958           return false;
959         }
960       }
961     }
963     // Discard history results that dupe the quick suggest result.
964     if (
965       state.quickSuggestResult &&
966       !result.heuristic &&
967       result.type == UrlbarUtils.RESULT_TYPE.URL &&
968       lazy.QuickSuggest.isUrlEquivalentToResultUrl(
969         result.payload.url,
970         state.quickSuggestResult
971       )
972     ) {
973       return false;
974     }
976     // Discard history results whose URLs were originally sponsored. We use the
977     // presence of a partner's URL search param to detect these. The param is
978     // defined in the pref below, which is also used for the newtab page.
979     if (
980       result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
981       result.type == UrlbarUtils.RESULT_TYPE.URL
982     ) {
983       let param = Services.prefs.getCharPref(
984         "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam"
985       );
986       if (param) {
987         let [key, value] = param.split("=");
988         let searchParams;
989         try {
990           ({ searchParams } = new URL(result.payload.url));
991         } catch (error) {}
992         if (
993           (value === undefined && searchParams?.has(key)) ||
994           (value !== undefined && searchParams?.getAll(key).includes(value))
995         ) {
996           return false;
997         }
998       }
999     }
1001     // Heuristic results must always be the first result.  If this result is a
1002     // heuristic but we've already added results, discard it.  Normally this
1003     // should never happen because the standard result groups are set up so
1004     // that there's always at most one heuristic and it's always first, but
1005     // since result groups are stored in a pref and can therefore be modified
1006     // by the user, we perform this check.
1007     if (result.heuristic && state.usedResultSpan) {
1008       return false;
1009     }
1011     // Google search engine might suggest a result for unit conversion with
1012     // format that starts with "= ". If our UnitConversion can provide the
1013     // result, we discard the suggestion of Google in order to deduplicate.
1014     if (
1015       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
1016       result.payload.engine == "Google" &&
1017       result.payload.suggestion?.startsWith("= ") &&
1018       state.hasUnitConversionResult
1019     ) {
1020       return false;
1021     }
1023     // Discard results that have an embedded "url" param with the same value
1024     // as another result's url
1025     if (result.payload.url) {
1026       let urlParams = result.payload.url.split("?").pop();
1027       let embeddedUrl = new URLSearchParams(urlParams).get("url");
1029       if (state.addedResultUrls.has(embeddedUrl)) {
1030         return false;
1031       }
1032     }
1034     // Dedupe history results with different ref.
1035     if (
1036       lazy.UrlbarPrefs.get("deduplication.enabled") &&
1037       result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
1038       result.type == UrlbarUtils.RESULT_TYPE.URL &&
1039       !result.heuristic &&
1040       result.payload.lastVisit
1041     ) {
1042       let { base, ref } = UrlbarUtils.extractRefFromUrl(result.payload.url);
1043       let baseAndTitle = `${base} ${result.payload.title}`;
1044       let topRef = state.baseAndTitleToTopRef.get(baseAndTitle);
1046       let msSinceLastVisit = Date.now() - result.payload.lastVisit;
1047       let daysSinceLastVisit = msSinceLastVisit / MS_PER_DAY;
1048       let thresholdDays = lazy.UrlbarPrefs.get("deduplication.thresholdDays");
1050       if (daysSinceLastVisit >= thresholdDays && ref != topRef) {
1051         return false;
1052       }
1053     }
1055     // Include the result.
1056     return true;
1057   }
1059   /**
1060    * Updates the global state that we use to make decisions during sort.  This
1061    * should be called for results before we've decided whether to add or discard
1062    * them.
1063    *
1064    * @param {UrlbarResult} result
1065    *   The result.
1066    * @param {object} state
1067    *   Global state that we use to make decisions during this sort.
1068    */
1069   _updateStatePreAdd(result, state) {
1070     // Check whether the result should trigger exposure telemetry. If it will
1071     // not be visible because it's a hidden exposure, it should not affect the
1072     // muxer's state, so bail now.
1073     this.#setExposureTelemetryProperty(result);
1074     if (result.isHiddenExposure) {
1075       return;
1076     }
1078     // Keep track of the largest heuristic result span.
1079     if (result.heuristic && this._canAddResult(result, state)) {
1080       state.maxHeuristicResultSpan = Math.max(
1081         state.maxHeuristicResultSpan,
1082         UrlbarUtils.getSpanForResult(result)
1083       );
1084     }
1086     // Keep track of the total span of global suggestedIndex results so we can
1087     // make room for them at the end of the sort. Tab-to-search results are an
1088     // exception: There can be multiple TTS results but only one will be shown,
1089     // so we track the max TTS span separately.
1090     if (
1091       result.hasSuggestedIndex &&
1092       !result.isSuggestedIndexRelativeToGroup &&
1093       this._canAddResult(result, state)
1094     ) {
1095       let span = UrlbarUtils.getSpanForResult(result);
1096       if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
1097         state.maxTabToSearchResultSpan = Math.max(
1098           state.maxTabToSearchResultSpan,
1099           span
1100         );
1101       } else {
1102         state.globalSuggestedIndexResultSpan += span;
1103       }
1104     }
1106     // Save some state we'll use later to dedupe URL results.
1107     if (
1108       (result.type == UrlbarUtils.RESULT_TYPE.URL ||
1109         result.type == UrlbarUtils.RESULT_TYPE.KEYWORD) &&
1110       result.payload.url &&
1111       (!result.heuristic || !lazy.UrlbarPrefs.get("experimental.hideHeuristic"))
1112     ) {
1113       let [strippedUrl, prefix] = UrlbarUtils.stripPrefixAndTrim(
1114         result.payload.url,
1115         {
1116           stripHttp: true,
1117           stripHttps: true,
1118           stripWww: true,
1119           trimEmptyQuery: true,
1120         }
1121       );
1122       let prefixRank = UrlbarUtils.getPrefixRank(prefix);
1123       let topPrefixData = state.strippedUrlToTopPrefixAndTitle.get(strippedUrl);
1124       let topPrefixRank = topPrefixData ? topPrefixData.rank : -1;
1125       if (
1126         topPrefixRank < prefixRank ||
1127         // If a quick suggest result has the same stripped URL and prefix rank
1128         // as another result, store the quick suggest as the top rank so we
1129         // discard the other during deduping. That happens after the user picks
1130         // the quick suggest: The URL is added to history and later both a
1131         // history result and the quick suggest may match a query.
1132         (topPrefixRank == prefixRank &&
1133           result.providerName == lazy.UrlbarProviderQuickSuggest.name)
1134       ) {
1135         // strippedUrl => { prefix, title, rank, providerName }
1136         state.strippedUrlToTopPrefixAndTitle.set(strippedUrl, {
1137           prefix,
1138           title: result.payload.title,
1139           rank: prefixRank,
1140           providerName: result.providerName,
1141         });
1142       }
1143     }
1145     // Save some state to dedupe results that only differ by the ref of their URL.
1146     // Even though we are considering tab results and URL results of all sources
1147     // here to find the top ref, we will only dedupe URL results with history source.
1148     if (
1149       result.type == UrlbarUtils.RESULT_TYPE.URL ||
1150       result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH
1151     ) {
1152       let { base, ref } = UrlbarUtils.extractRefFromUrl(result.payload.url);
1154       // This is unique because all spaces in base will be url-encoded
1155       // so the part before the space is always the base url.
1156       let baseAndTitle = `${base} ${result.payload.title}`;
1158       // The first result should have the highest frecency so we set it as the top
1159       // ref for its base url. If a result is heuristic, always override an existing
1160       // top ref for its base url in case the heuristic provider was slow and a non
1161       // heuristic result was added first for the same base url.
1162       if (!state.baseAndTitleToTopRef.has(baseAndTitle) || result.heuristic) {
1163         state.baseAndTitleToTopRef.set(baseAndTitle, ref);
1164       }
1165     }
1167     // Save some state we'll use later to dedupe results from open/remote tabs.
1168     if (
1169       result.payload.url &&
1170       (result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH ||
1171         (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB &&
1172           !state.urlToTabResultType.has(makeMapKeyForTabResult(result))))
1173     ) {
1174       // url => result type
1175       state.urlToTabResultType.set(makeMapKeyForTabResult(result), result.type);
1176     }
1178     // If we find results other than the heuristic, "Search in Private
1179     // Window," or tail suggestions, then we should hide tail suggestions
1180     // since they're a last resort.
1181     if (
1182       state.canShowTailSuggestions &&
1183       !result.heuristic &&
1184       (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
1185         (!result.payload.inPrivateWindow && !result.payload.tail))
1186     ) {
1187       state.canShowTailSuggestions = false;
1188     }
1190     if (result.providerName == lazy.UrlbarProviderQuickSuggest.name) {
1191       state.quickSuggestResult ??= result;
1192     }
1194     state.hasUnitConversionResult =
1195       state.hasUnitConversionResult || result.providerName == "UnitConversion";
1197     // Keep track of result urls to dedupe results with the same url embedded
1198     // in its query string
1199     if (result.payload.url) {
1200       state.addedResultUrls.add(result.payload.url);
1201     }
1202   }
1204   /**
1205    * Updates the global state that we use to make decisions during sort.  This
1206    * should be called for results after they've been added.  It should not be
1207    * called for discarded results.
1208    *
1209    * @param {UrlbarResult} result
1210    *   The result.
1211    * @param {object} state
1212    *   Global state that we use to make decisions during this sort.
1213    */
1214   _updateStatePostAdd(result, state) {
1215     // bail early if the result will be hidden from the final view.
1216     if (result.isHiddenExposure) {
1217       return;
1218     }
1220     // Update heuristic state.
1221     if (result.heuristic) {
1222       state.context.heuristicResult = result;
1223       if (
1224         result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
1225         result.payload.query &&
1226         !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
1227       ) {
1228         let query = result.payload.query.trim().toLocaleLowerCase();
1229         if (query) {
1230           state.suggestions.add(query);
1231         }
1232       }
1233     }
1235     // The "Search in a Private Window" result should only be shown when there
1236     // are other results and all of them are searches.  It should not be shown
1237     // if the user typed an alias because that's an explicit engine choice.
1238     if (
1239       !Services.search.separatePrivateDefaultUrlbarResultEnabled ||
1240       (state.canShowPrivateSearch &&
1241         (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
1242           result.payload.providesSearchMode ||
1243           (result.heuristic && result.payload.keyword)))
1244     ) {
1245       state.canShowPrivateSearch = false;
1246     }
1248     // Update suggestions.
1249     if (
1250       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
1251       result.payload.lowerCaseSuggestion
1252     ) {
1253       let suggestion = result.payload.lowerCaseSuggestion.trim();
1254       if (suggestion) {
1255         state.suggestions.add(suggestion);
1256       }
1257     }
1259     // Avoid multiple tab-to-search results.
1260     // TODO (Bug 1670185): figure out better strategies to manage this case.
1261     if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
1262       state.canAddTabToSearch = false;
1263     }
1265     // Sync will send us duplicate remote tabs if multiple copies of a tab are
1266     // open on a synced client. Keep track of which remote tabs we've added to
1267     // dedupe these.
1268     if (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
1269       state.addedRemoteTabUrls.add(result.payload.url);
1270     }
1272     // Keep track of which switch tabs we've added to dedupe switch tabs.
1273     if (result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
1274       state.addedSwitchTabUrls.add(makeMapKeyForTabResult(result));
1275     }
1276   }
1278   /**
1279    * Inserts results with suggested indexes. This can be called for either
1280    * global or group-relative suggestedIndex results. It should be called after
1281    * `sortedResults` has been filled in.
1282    *
1283    * @param {Array} suggestedIndexResults
1284    *   Results with a `suggestedIndex` property.
1285    * @param {Array} sortedResults
1286    *   The sorted results. For global suggestedIndex results, this should be the
1287    *   final list of all results before suggestedIndex results are inserted. For
1288    *   group-relative suggestedIndex results, this should be the final list of
1289    *   results in the group before group-relative suggestedIndex results are
1290    *   inserted.
1291    * @param {object} limits
1292    *   An object defining span and count limits. See `_fillGroup()`.
1293    * @param {object} state
1294    *   Global state that we use to make decisions during this sort.
1295    * @returns {object}
1296    *   A `usedLimits` object that describes the total span and count of all the
1297    *   added results. See `_addResults`.
1298    */
1299   _addSuggestedIndexResults(
1300     suggestedIndexResults,
1301     sortedResults,
1302     limits,
1303     state
1304   ) {
1305     let usedLimits = {
1306       availableSpan: 0,
1307       maxResultCount: 0,
1308     };
1310     if (!suggestedIndexResults?.length) {
1311       // This is just a slight optimization; no need to continue.
1312       return usedLimits;
1313     }
1315     // Partition the results into positive- and negative-index arrays. Positive
1316     // indexes are relative to the start of the list and negative indexes are
1317     // relative to the end.
1318     let positive = [];
1319     let negative = [];
1320     for (let result of suggestedIndexResults) {
1321       let results = result.suggestedIndex < 0 ? negative : positive;
1322       results.push(result);
1323     }
1325     // Sort the positive results ascending so that results at the end of the
1326     // array don't end up offset by later insertions at the front.
1327     positive.sort((a, b) => {
1328       if (a.suggestedIndex !== b.suggestedIndex) {
1329         return a.suggestedIndex - b.suggestedIndex;
1330       }
1332       if (a.providerName === b.providerName) {
1333         return 0;
1334       }
1336       // If same suggestedIndex, change the displaying order along to following
1337       // provider priority.
1338       // TabToSearch > QuickSuggest > Other providers
1339       if (a.providerName === lazy.UrlbarProviderTabToSearch.name) {
1340         return 1;
1341       }
1342       if (b.providerName === lazy.UrlbarProviderTabToSearch.name) {
1343         return -1;
1344       }
1345       if (a.providerName === lazy.UrlbarProviderQuickSuggest.name) {
1346         return 1;
1347       }
1348       if (b.providerName === lazy.UrlbarProviderQuickSuggest.name) {
1349         return -1;
1350       }
1352       return 0;
1353     });
1355     // Conversely, sort the negative results descending so that results at the
1356     // front of the array don't end up offset by later insertions at the end.
1357     negative.sort((a, b) => b.suggestedIndex - a.suggestedIndex);
1359     // Insert the results. We start with the positive results because we have
1360     // tests that assume they're inserted first. In practice it shouldn't matter
1361     // because there's no good reason we would ever have a negative result come
1362     // before a positive result in the same query. Even if we did, we have to
1363     // insert one before the other, and there's no right or wrong order.
1364     for (let results of [positive, negative]) {
1365       let prevResult;
1366       let prevIndex;
1367       for (let result of results) {
1368         if (this._canAddResult(result, state)) {
1369           if (!this.#updateUsedLimits(result, limits, usedLimits, state)) {
1370             return usedLimits;
1371           }
1373           let index;
1374           if (
1375             prevResult &&
1376             prevResult.suggestedIndex == result.suggestedIndex
1377           ) {
1378             index = prevIndex;
1379           } else {
1380             index =
1381               result.suggestedIndex >= 0
1382                 ? Math.min(result.suggestedIndex, sortedResults.length)
1383                 : Math.max(result.suggestedIndex + sortedResults.length + 1, 0);
1384           }
1385           prevResult = result;
1386           prevIndex = index;
1387           sortedResults.splice(index, 0, result);
1388         }
1389       }
1390     }
1392     return usedLimits;
1393   }
1395   /**
1396    * Checks whether adding a result would exceed the given limits. If the limits
1397    * would be exceeded, this returns false and does nothing else. If the limits
1398    * would not be exceeded, the given used limits and state are updated to
1399    * account for the result, true is returned, and the caller should then add
1400    * the result to its list of sorted results.
1401    *
1402    * @param {UrlbarResult} result
1403    *   The result.
1404    * @param {object} limits
1405    *   An object defining span and count limits. See `_fillGroup()`.
1406    * @param {object} usedLimits
1407    *   An object with parallel properties to `limits` that describes how much of
1408    *   the limits have been used. See `_addResults()`.
1409    * @param {object} state
1410    *   The muxer state.
1411    * @returns {boolean}
1412    *   True if the limits were updated and the result can be added and false
1413    *   otherwise.
1414    */
1415   #updateUsedLimits(result, limits, usedLimits, state) {
1416     let span = UrlbarUtils.getSpanForResult(result);
1417     let newUsedSpan = usedLimits.availableSpan + span;
1418     if (limits.availableSpan < newUsedSpan) {
1419       // Adding the result would exceed the available span.
1420       return false;
1421     }
1423     usedLimits.availableSpan = newUsedSpan;
1424     if (span) {
1425       usedLimits.maxResultCount++;
1426     }
1428     state.usedResultSpan += span;
1429     this._updateStatePostAdd(result, state);
1431     return true;
1432   }
1434   /**
1435    * Checks exposure eligibility and visibility for the given result.
1436    * If the result passes the exposure check, we set `result.exposureTelemetry`
1437    * to the appropriate `UrlbarUtils.EXPOSURE_TELEMETRY` value.
1438    *
1439    * @param {UrlbarResult} result
1440    *   The result.
1441    */
1442   #setExposureTelemetryProperty(result) {
1443     const exposureResults = lazy.UrlbarPrefs.get("exposureResults");
1444     if (exposureResults.size) {
1445       const telemetryType = UrlbarUtils.searchEngagementTelemetryType(result);
1446       if (exposureResults.has(telemetryType)) {
1447         result.exposureTelemetry = lazy.UrlbarPrefs.get("showExposureResults")
1448           ? UrlbarUtils.EXPOSURE_TELEMETRY.SHOWN
1449           : UrlbarUtils.EXPOSURE_TELEMETRY.HIDDEN;
1450       }
1451     }
1452   }
1455 export var UrlbarMuxerUnifiedComplete = new MuxerUnifiedComplete();