Bug 1824490 - Use the end page value rather than the start page value of the previous...
[gecko.git] / browser / components / urlbar / UrlbarMuxerUnifiedComplete.sys.mjs
blob50668a5c2b9742883e0f2598971822f0139e3279
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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
11 import {
12   UrlbarMuxer,
13   UrlbarUtils,
14 } from "resource:///modules/UrlbarUtils.sys.mjs";
16 const lazy = {};
18 ChromeUtils.defineESModuleGetters(lazy, {
19   QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
20   UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
21   UrlbarProviderQuickSuggest:
22     "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
23   UrlbarProviderTabToSearch:
24     "resource:///modules/UrlbarProviderTabToSearch.sys.mjs",
25   UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
26   UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
27 });
29 XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
30   UrlbarUtils.getLogger({ prefix: "MuxerUnifiedComplete" })
33 /**
34  * Class used to create a muxer.
35  * The muxer receives and sorts results in a UrlbarQueryContext.
36  */
37 class MuxerUnifiedComplete extends UrlbarMuxer {
38   constructor() {
39     super();
40   }
42   get name() {
43     return "UnifiedComplete";
44   }
46   /**
47    * Sorts results in the given UrlbarQueryContext.
48    *
49    * @param {UrlbarQueryContext} context
50    *   The query context.
51    * @param {Array} unsortedResults
52    *   The array of UrlbarResult that is not sorted yet.
53    */
54   sort(context, unsortedResults) {
55     // This method is called multiple times per keystroke, so it should be as
56     // fast and efficient as possible.  We do two passes through the results:
57     // one to collect state for the second pass, and then a second to build the
58     // sorted list of results.  If you find yourself writing something like
59     // context.results.find(), filter(), sort(), etc., modify one or both passes
60     // instead.
62     // Global state we'll use to make decisions during this sort.
63     let state = {
64       context,
65       // RESULT_GROUP => array of results belonging to the group, excluding
66       // group-relative suggestedIndex results
67       resultsByGroup: new Map(),
68       // RESULT_GROUP => array of group-relative suggestedIndex results
69       // belonging to the group
70       suggestedIndexResultsByGroup: new Map(),
71       // This is analogous to `maxResults` except it's the total available
72       // result span instead of the total available result count. We'll add
73       // results until `usedResultSpan` would exceed `availableResultSpan`.
74       availableResultSpan: context.maxResults,
75       // The total span of results that have been added so far.
76       usedResultSpan: 0,
77       strippedUrlToTopPrefixAndTitle: new Map(),
78       urlToTabResultType: new Map(),
79       addedRemoteTabUrls: new Set(),
80       addedSwitchTabUrls: new Set(),
81       canShowPrivateSearch: unsortedResults.length > 1,
82       canShowTailSuggestions: true,
83       // Form history and remote suggestions added so far.  Used for deduping
84       // suggestions.  Also includes the heuristic query string if the heuristic
85       // is a search result.  All strings in the set are lowercased.
86       suggestions: new Set(),
87       canAddTabToSearch: true,
88       hasUnitConversionResult: false,
89       maxHeuristicResultSpan: 0,
90       maxTabToSearchResultSpan: 0,
91       // When you add state, update _copyState() as necessary.
92     };
94     // Do the first pass over all results to build some state.
95     for (let result of unsortedResults) {
96       // Add each result to the appropriate `resultsByGroup` map.
97       let group = UrlbarUtils.getResultGroup(result);
98       let resultsByGroup =
99         result.hasSuggestedIndex && result.isSuggestedIndexRelativeToGroup
100           ? state.suggestedIndexResultsByGroup
101           : state.resultsByGroup;
102       let results = resultsByGroup.get(group);
103       if (!results) {
104         results = [];
105         resultsByGroup.set(group, results);
106       }
107       results.push(result);
109       // Update pre-add state.
110       this._updateStatePreAdd(result, state);
111     }
113     // Now that the first pass is done, adjust the available result span.
114     if (state.maxTabToSearchResultSpan) {
115       // Subtract the max tab-to-search span.
116       state.availableResultSpan = Math.max(
117         state.availableResultSpan - state.maxTabToSearchResultSpan,
118         0
119       );
120     }
121     if (state.maxHeuristicResultSpan) {
122       if (lazy.UrlbarPrefs.get("experimental.hideHeuristic")) {
123         // The heuristic is hidden. The muxer will include it but the view will
124         // hide it. Increase the available span to compensate so that the total
125         // visible span accurately reflects `context.maxResults`.
126         state.availableResultSpan += state.maxHeuristicResultSpan;
127       } else if (context.maxResults > 0) {
128         // `context.maxResults` is positive. Make sure there's room for the
129         // heuristic even if it means exceeding `context.maxResults`.
130         state.availableResultSpan = Math.max(
131           state.availableResultSpan,
132           state.maxHeuristicResultSpan
133         );
134       }
135     }
137     // Determine the result groups to use for this sort.  In search mode with
138     // an engine, show search suggestions first.
139     let rootGroup = context.searchMode?.engineName
140       ? lazy.UrlbarPrefs.makeResultGroups({ showSearchSuggestionsFirst: true })
141       : lazy.UrlbarPrefs.resultGroups;
142     lazy.logger.debug(`Groups: ${rootGroup}`);
144     // Fill the root group.
145     let [sortedResults] = this._fillGroup(
146       rootGroup,
147       { availableSpan: state.availableResultSpan, maxResultCount: Infinity },
148       state
149     );
151     // Add global suggestedIndex results unless the max result count is zero,
152     // which isn't really supported but it's easy to honor here. We add them all
153     // even if they exceed the max because we assume they're high-priority
154     // results that should always be shown, and as long as the max is positive
155     // it's not a problem to exceed it sometimes. In practice that will happen
156     // only for small, non-default values of `maxRichResults`.
157     if (context.maxResults > 0) {
158       let suggestedIndexResults = state.resultsByGroup.get(
159         UrlbarUtils.RESULT_GROUP.SUGGESTED_INDEX
160       );
161       if (suggestedIndexResults) {
162         this._addSuggestedIndexResults(
163           suggestedIndexResults,
164           sortedResults,
165           state
166         );
167       }
168     }
170     context.results = sortedResults;
171   }
173   /**
174    * Returns a *deep* copy of state (except for `state.context`, which is
175    * shallow copied).  i.e., any Maps, Sets, and arrays in the state should be
176    * recursively copied so that the original state is not modified when the copy
177    * is modified.
178    *
179    * @param {object} state
180    *   The muxer state to copy.
181    * @returns {object}
182    *   A deep copy of the state.
183    */
184   _copyState(state) {
185     let copy = Object.assign({}, state, {
186       resultsByGroup: new Map(),
187       suggestedIndexResultsByGroup: new Map(),
188       strippedUrlToTopPrefixAndTitle: new Map(
189         state.strippedUrlToTopPrefixAndTitle
190       ),
191       urlToTabResultType: new Map(state.urlToTabResultType),
192       addedRemoteTabUrls: new Set(state.addedRemoteTabUrls),
193       addedSwitchTabUrls: new Set(state.addedSwitchTabUrls),
194       suggestions: new Set(state.suggestions),
195     });
197     // Deep copy the `resultsByGroup` maps.
198     for (let key of ["resultsByGroup", "suggestedIndexResultsByGroup"]) {
199       for (let [group, results] of state[key]) {
200         copy[key].set(group, [...results]);
201       }
202     }
204     return copy;
205   }
207   /**
208    * Recursively fills a result group and its children.
209    *
210    * There are two ways to limit the number of results in a group:
211    *
212    * (1) By max total result span using the `availableSpan` property. The group
213    * will be filled so that the total span of its results does not exceed this
214    * value.
215    *
216    * (2) By max total result count using the `maxResultCount` property. The
217    * group will be filled so that the total number of its results does not
218    * exceed this value.
219    *
220    * Both `availableSpan` and `maxResultCount` may be defined, and the group's
221    * results will be capped to whichever limit is reached first. If either is
222    * not defined, then the group inherits that limit from its parent group.
223    *
224    * In addition to limiting their total number of results, groups can also
225    * control the composition of their child groups by using flex ratios. A group
226    * can define a `flexChildren: true` property, and in that case each of its
227    * children should have a `flex` property. Each child will be filled according
228    * to the ratio of its flex value and the sum of the flex values of all the
229    * children, similar to HTML flexbox. If some children do not fill up but
230    * others do, the filled-up children will be allowed to grow to use up the
231    * unfilled space.
232    *
233    * @param {object} group
234    *   The result group to fill.
235    * @param {object} limits
236    *   An object with optional `availableSpan` and `maxResultCount` properties
237    *   as described above. They will be used as the limits for the group.
238    * @param {object} state
239    *   The muxer state.
240    * @returns {Array}
241    *   `[results, usedLimits, hasMoreResults]` -- see `_addResults`.
242    */
243   _fillGroup(group, limits, state) {
244     // Get the group's suggestedIndex results. Reminder: `group.group` is a
245     // `RESULT_GROUP` constant.
246     let suggestedIndexResults;
247     if ("group" in group) {
248       suggestedIndexResults = state.suggestedIndexResultsByGroup.get(
249         group.group
250       );
251       if (suggestedIndexResults) {
252         // Subtract them from the group's limits so there will be room for them
253         // later. Create a new `limits` object so we don't modify the caller's.
254         let span = suggestedIndexResults.reduce((sum, result) => {
255           sum += UrlbarUtils.getSpanForResult(result);
256           return sum;
257         }, 0);
258         limits = { ...limits };
259         limits.availableSpan = Math.max(limits.availableSpan - span, 0);
260         limits.maxResultCount = Math.max(
261           limits.maxResultCount - suggestedIndexResults.length,
262           0
263         );
264       }
265     }
267     // Fill the group. If it has children, fill them recursively. Otherwise fill
268     // the group directly.
269     let [results, usedLimits, hasMoreResults] = group.children
270       ? this._fillGroupChildren(group, limits, state)
271       : this._addResults(group.group, limits, state);
273     // Add the group's suggestedIndex results.
274     if (suggestedIndexResults) {
275       let suggestedIndexUsedLimits = this._addSuggestedIndexResults(
276         suggestedIndexResults,
277         results,
278         state
279       );
280       for (let [key, value] of Object.entries(suggestedIndexUsedLimits)) {
281         usedLimits[key] += value;
282       }
283     }
285     return [results, usedLimits, hasMoreResults];
286   }
288   /**
289    * Helper for `_fillGroup` that fills a group's children.
290    *
291    * @param {object} group
292    *   The result group to fill. It's assumed to have a `children` property.
293    * @param {object} limits
294    *   An object with optional `availableSpan` and `maxResultCount` properties
295    *   as described in `_fillGroup`.
296    * @param {object} state
297    *   The muxer state.
298    * @param {Array} flexDataArray
299    *   See `_updateFlexData`.
300    * @returns {Array}
301    *   `[results, usedLimits, hasMoreResults]` -- see `_addResults`.
302    */
303   _fillGroupChildren(group, limits, state, flexDataArray = null) {
304     // If the group has flexed children, update the data we use during flex
305     // calculations.
306     //
307     // Handling flex is complicated so we discuss it briefly. We may do multiple
308     // passes for a group with flexed children in order to try to optimally fill
309     // them. If after one pass some children do not fill up but others do, we'll
310     // do another pass that tries to overfill the filled-up children while still
311     // respecting their flex ratios. We'll continue to do passes until all
312     // children stop filling up or we reach the parent's limits. The way we
313     // overfill children is by increasing their individual limits to make up for
314     // the unused space in their underfilled siblings. Before starting a new
315     // pass, we discard the results from the current pass so the new pass starts
316     // with a clean slate. That means we need to copy the global sort state
317     // (`state`) before modifying it in the current pass so we can use its
318     // original value in the next pass [1].
319     //
320     // [1] Instead of starting each pass with a clean slate in this way, we
321     // could accumulate results with each pass since we only ever add results to
322     // flexed children and never remove them. However, that would subvert muxer
323     // logic related to the global state (deduping, `_canAddResult`) since we
324     // generally assume the muxer adds results in the order they appear.
325     let stateCopy;
326     if (group.flexChildren) {
327       stateCopy = this._copyState(state);
328       flexDataArray = this._updateFlexData(group, limits, flexDataArray);
329     }
331     // Fill each child group, collecting all results in the `results` array.
332     let results = [];
333     let usedLimits = {};
334     for (let key of Object.keys(limits)) {
335       usedLimits[key] = 0;
336     }
337     let anyChildUnderfilled = false;
338     let anyChildHasMoreResults = false;
339     for (let i = 0; i < group.children.length; i++) {
340       let child = group.children[i];
341       let flexData = flexDataArray?.[i];
343       // Compute the child's limits.
344       let childLimits = {};
345       for (let key of Object.keys(limits)) {
346         childLimits[key] = flexData
347           ? flexData.limits[key]
348           : Math.min(
349               typeof child[key] == "number" ? child[key] : Infinity,
350               limits[key] - usedLimits[key]
351             );
352       }
354       // Recurse and fill the child.
355       let [
356         childResults,
357         childUsedLimits,
358         childHasMoreResults,
359       ] = this._fillGroup(child, childLimits, state);
360       results = results.concat(childResults);
361       for (let key of Object.keys(usedLimits)) {
362         usedLimits[key] += childUsedLimits[key];
363       }
364       anyChildHasMoreResults = anyChildHasMoreResults || childHasMoreResults;
366       if (flexData?.hasMoreResults) {
367         // The child is flexed and we possibly added more results to it.
368         flexData.usedLimits = childUsedLimits;
369         flexData.hasMoreResults = childHasMoreResults;
370         anyChildUnderfilled =
371           anyChildUnderfilled ||
372           (!childHasMoreResults &&
373             [...Object.entries(childLimits)].every(
374               ([key, limit]) => flexData.usedLimits[key] < limit
375             ));
376       }
377     }
379     // If the children are flexed and some underfilled but others still have
380     // more results, do another pass.
381     if (anyChildUnderfilled && anyChildHasMoreResults) {
382       [results, usedLimits, anyChildHasMoreResults] = this._fillGroupChildren(
383         group,
384         limits,
385         stateCopy,
386         flexDataArray
387       );
389       // Update `state` in place so that it's also updated in the caller.
390       for (let [key, value] of Object.entries(stateCopy)) {
391         state[key] = value;
392       }
393     }
395     return [results, usedLimits, anyChildHasMoreResults];
396   }
398   /**
399    * Updates flex-related state used while filling a group.
400    *
401    * @param {object} group
402    *   The result group being filled.
403    * @param {object} limits
404    *   An object defining the group's limits as described in `_fillGroup`.
405    * @param {Array} flexDataArray
406    *   An array parallel to `group.children`. The object at index i corresponds
407    *   to the child in `group.children` at index i. Each object maintains some
408    *   flex-related state for its child and is updated during each pass in
409    *   `_fillGroup` for `group`. When this method is called in the first pass,
410    *   this argument should be null, and the method will create and return a new
411    *   `flexDataArray` array that should be used in the remainder of the first
412    *   pass and all subsequent passes.
413    * @returns {Array}
414    *   A new `flexDataArray` when called in the first pass, and `flexDataArray`
415    *   itself when called in subsequent passes.
416    */
417   _updateFlexData(group, limits, flexDataArray) {
418     flexDataArray =
419       flexDataArray ||
420       group.children.map((child, index) => {
421         let data = {
422           // The index of the corresponding child in `group.children`.
423           index,
424           // The child's limits.
425           limits: {},
426           // The fractional parts of the child's unrounded limits; see below.
427           limitFractions: {},
428           // The used-up portions of the child's limits.
429           usedLimits: {},
430           // True if `state.resultsByGroup` has more results of the child's
431           // `RESULT_GROUP`. This is not related to the child's limits.
432           hasMoreResults: true,
433           // The child's flex value.
434           flex: typeof child.flex == "number" ? child.flex : 0,
435         };
436         for (let key of Object.keys(limits)) {
437           data.limits[key] = 0;
438           data.limitFractions[key] = 0;
439           data.usedLimits[key] = 0;
440         }
441         return data;
442       });
444     // The data objects for children with more results (i.e., that are still
445     // fillable).
446     let fillableDataArray = [];
448     // The sum of the flex values of children with more results.
449     let fillableFlexSum = 0;
451     for (let data of flexDataArray) {
452       if (data.hasMoreResults) {
453         fillableFlexSum += data.flex;
454         fillableDataArray.push(data);
455       }
456     }
458     // Update each limit.
459     for (let [key, limit] of Object.entries(limits)) {
460       // Calculate the group's limit only including children with more results.
461       let fillableLimit = limit;
462       for (let data of flexDataArray) {
463         if (!data.hasMoreResults) {
464           fillableLimit -= data.usedLimits[key];
465         }
466       }
468       // Allow for the possibility that some children may have gone over limit.
469       // `fillableLimit` will be negative in that case.
470       fillableLimit = Math.max(fillableLimit, 0);
472       // Next we'll compute the limits of children with more results. This value
473       // is the sum of those limits. It may differ from `fillableLimit` due to
474       // the fact that each individual child limit must be an integer.
475       let summedFillableLimit = 0;
477       // Compute the limits of children with more results. If there are also
478       // children that don't have more results, then these new limits will be
479       // larger than they were in the previous pass.
480       for (let data of fillableDataArray) {
481         let unroundedLimit = fillableLimit * (data.flex / fillableFlexSum);
482         // `limitFraction` is the fractional part of the unrounded ideal limit.
483         // e.g., for 5.234 it will be 0.234. We use this to minimize the
484         // mathematical error when tweaking limits below.
485         data.limitFractions[key] = unroundedLimit - Math.floor(unroundedLimit);
486         data.limits[key] = Math.round(unroundedLimit);
487         summedFillableLimit += data.limits[key];
488       }
490       // As mentioned above, the sum of the individual child limits may not
491       // equal the group's fillable limit. If the sum is smaller, the group will
492       // end up with too few results. If it's larger, the group will have the
493       // correct number of results (since we stop adding results once limits are
494       // reached) but it may end up with a composition that does not reflect the
495       // child flex ratios as accurately as possible.
496       //
497       // In either case, tweak the individual limits so that (1) their sum
498       // equals the group's fillable limit, and (2) the composition respects the
499       // flex ratios with as little mathematical error as possible.
500       if (summedFillableLimit != fillableLimit) {
501         // Collect the flex datas with a non-zero limit fractions. We'll round
502         // them up or down depending on whether the sum is larger or smaller
503         // than the group's fillable limit.
504         let fractionalDataArray = fillableDataArray.filter(
505           data => data.limitFractions[key]
506         );
508         let diff;
509         if (summedFillableLimit < fillableLimit) {
510           // The sum is smaller. We'll increment individual limits until the sum
511           // is equal, starting with the child whose limit fraction is closest
512           // to 1 in order to minimize error.
513           diff = 1;
514           fractionalDataArray.sort((a, b) => {
515             // Sort by fraction descending so larger fractions are first.
516             let cmp = b.limitFractions[key] - a.limitFractions[key];
517             // Secondarily sort by index ascending so that children with the
518             // same fraction are incremented in the order they appear, allowing
519             // earlier children to have larger spans.
520             return cmp || a.index - b.index;
521           });
522         } else if (fillableLimit < summedFillableLimit) {
523           // The sum is larger. We'll decrement individual limits until the sum
524           // is equal, starting with the child whose limit fraction is closest
525           // to 0 in order to minimize error.
526           diff = -1;
527           fractionalDataArray.sort((a, b) => {
528             // Sort by fraction ascending so smaller fractions are first.
529             let cmp = a.limitFractions[key] - b.limitFractions[key];
530             // Secondarily sort by index descending so that children with the
531             // same fraction are decremented in reverse order, allowing earlier
532             // children to retain larger spans.
533             return cmp || b.index - a.index;
534           });
535         }
537         // Now increment or decrement individual limits until their sum is equal
538         // to the group's fillable limit.
539         while (summedFillableLimit != fillableLimit) {
540           if (!fractionalDataArray.length) {
541             // This shouldn't happen, but don't let it break us.
542             lazy.logger.error("fractionalDataArray is empty!");
543             break;
544           }
545           let data = flexDataArray[fractionalDataArray.shift().index];
546           data.limits[key] += diff;
547           summedFillableLimit += diff;
548         }
549       }
550     }
552     return flexDataArray;
553   }
555   /**
556    * Adds results to a group using the results from its `RESULT_GROUP` in
557    * `state.resultsByGroup`.
558    *
559    * @param {UrlbarUtils.RESULT_GROUP} groupConst
560    *   The group's `RESULT_GROUP`.
561    * @param {object} limits
562    *   An object defining the group's limits as described in `_fillGroup`.
563    * @param {object} state
564    *   Global state that we use to make decisions during this sort.
565    * @returns {Array}
566    *   `[results, usedLimits, hasMoreResults]` where:
567    *     results: A flat array of results in the group, empty if no results
568    *       were added.
569    *     usedLimits: An object defining the amount of each limit that the
570    *       results use. For each possible limit property (see `_fillGroup`),
571    *       there will be a corresponding property in this object. For example,
572    *       if 3 results are added with a total span of 4, then this object will
573    *       be: { maxResultCount: 3, availableSpan: 4 }
574    *     hasMoreResults: True if `state.resultsByGroup` has more results of
575    *       the same `RESULT_GROUP`. This is not related to the group's limits.
576    */
577   _addResults(groupConst, limits, state) {
578     let usedLimits = {};
579     for (let key of Object.keys(limits)) {
580       usedLimits[key] = 0;
581     }
583     // For form history, maxHistoricalSearchSuggestions == 0 implies the user
584     // has opted out of form history completely, so we override the max result
585     // count here in that case. Other values of maxHistoricalSearchSuggestions
586     // are ignored and we use the flex defined on the form history group.
587     if (
588       groupConst == UrlbarUtils.RESULT_GROUP.FORM_HISTORY &&
589       !lazy.UrlbarPrefs.get("maxHistoricalSearchSuggestions")
590     ) {
591       // Create a new `limits` object so we don't modify the caller's.
592       limits = { ...limits };
593       limits.maxResultCount = 0;
594     }
596     let addedResults = [];
597     let groupResults = state.resultsByGroup.get(groupConst);
598     while (
599       groupResults?.length &&
600       state.usedResultSpan < state.availableResultSpan &&
601       [...Object.entries(limits)].every(([k, limit]) => usedLimits[k] < limit)
602     ) {
603       let result = groupResults[0];
604       if (this._canAddResult(result, state)) {
605         let span = UrlbarUtils.getSpanForResult(result);
606         let newUsedSpan = usedLimits.availableSpan + span;
607         if (limits.availableSpan < newUsedSpan) {
608           // Adding the result would exceed the group's available span, so stop
609           // adding results to it. Skip the shift() below so the result can be
610           // added to later groups.
611           break;
612         }
613         addedResults.push(result);
614         usedLimits.availableSpan = newUsedSpan;
615         usedLimits.maxResultCount++;
616         state.usedResultSpan += span;
617         this._updateStatePostAdd(result, state);
618       }
620       // We either add or discard results in the order they appear in
621       // `groupResults`, so shift() them off. That way later groups with the
622       // same `RESULT_GROUP` won't include results that earlier groups have
623       // added or discarded.
624       groupResults.shift();
625     }
627     return [addedResults, usedLimits, !!groupResults?.length];
628   }
630   /**
631    * Returns whether a result can be added to its group given the current sort
632    * state.
633    *
634    * @param {UrlbarResult} result
635    *   The result.
636    * @param {object} state
637    *   Global state that we use to make decisions during this sort.
638    * @returns {boolean}
639    *   True if the result can be added and false if it should be discarded.
640    */
641   // TODO (Bug 1741273): Refactor this method to avoid an eslint complexity
642   // error or increase the complexity threshold.
643   // eslint-disable-next-line complexity
644   _canAddResult(result, state) {
645     // QuickSuggest results are shown unless a weather result is also present
646     // or they are navigational suggestions that duplicate the heuristic.
647     if (result.providerName == lazy.UrlbarProviderQuickSuggest.name) {
648       if (state.weatherResult) {
649         return false;
650       }
652       let heuristicUrl = state.context.heuristicResult?.payload.url;
653       if (
654         heuristicUrl &&
655         result.payload.subtype ==
656           lazy.UrlbarProviderQuickSuggest.RESULT_SUBTYPE.NAVIGATIONAL &&
657         !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
658       ) {
659         let opts = {
660           stripHttp: true,
661           stripHttps: true,
662           stripWww: true,
663           trimSlash: true,
664         };
665         result.payload.dupedHeuristic =
666           UrlbarUtils.stripPrefixAndTrim(heuristicUrl, opts)[0] ==
667           UrlbarUtils.stripPrefixAndTrim(result.payload.url, opts)[0];
668         return !result.payload.dupedHeuristic;
669       }
670       return true;
671     }
673     // We expect UrlbarProviderPlaces sent us the highest-ranked www. and non-www
674     // origins, if any. Now, compare them to each other and to the heuristic
675     // result.
676     //
677     // 1. If the heuristic result is lower ranked than both, discard the www
678     //    origin, unless it has a different page title than the non-www
679     //    origin. This is a guard against deduping when www.site.com and
680     //    site.com have different content.
681     // 2. If the heuristic result is higher than either the www origin or
682     //    non-www origin:
683     //    2a. If the heuristic is a www origin, discard the non-www origin.
684     //    2b. If the heuristic is a non-www origin, discard the www origin.
685     if (
686       !result.heuristic &&
687       result.type == UrlbarUtils.RESULT_TYPE.URL &&
688       result.payload.url
689     ) {
690       let [strippedUrl, prefix] = UrlbarUtils.stripPrefixAndTrim(
691         result.payload.url,
692         {
693           stripHttp: true,
694           stripHttps: true,
695           stripWww: true,
696           trimEmptyQuery: true,
697         }
698       );
699       let topPrefixData = state.strippedUrlToTopPrefixAndTitle.get(strippedUrl);
700       // If the condition below is not met, we are deduping a result against
701       // itself.
702       if (
703         topPrefixData &&
704         (prefix != topPrefixData.prefix ||
705           result.providerName != topPrefixData.providerName)
706       ) {
707         let prefixRank = UrlbarUtils.getPrefixRank(prefix);
708         if (
709           (prefixRank < topPrefixData.rank &&
710             (prefix.endsWith("www.") == topPrefixData.prefix.endsWith("www.") ||
711               result.payload?.title == topPrefixData.title)) ||
712           (prefix == topPrefixData.prefix &&
713             result.providerName != topPrefixData.providerName)
714         ) {
715           return false;
716         }
717       }
718     }
720     // Discard results that dupe autofill.
721     if (
722       state.context.heuristicResult &&
723       state.context.heuristicResult.autofill &&
724       !result.autofill &&
725       state.context.heuristicResult.payload?.url == result.payload.url &&
726       state.context.heuristicResult.type == result.type &&
727       !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
728     ) {
729       return false;
730     }
732     // HeuristicFallback may add non-heuristic results in some cases, but those
733     // should be retained only if the heuristic result comes from it.
734     if (
735       !result.heuristic &&
736       result.providerName == "HeuristicFallback" &&
737       state.context.heuristicResult?.providerName != "HeuristicFallback"
738     ) {
739       return false;
740     }
742     if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
743       // Discard the result if a tab-to-search result was added already.
744       if (!state.canAddTabToSearch) {
745         return false;
746       }
748       // In cases where the heuristic result is not a URL and we have a
749       // tab-to-search result, the tab-to-search provider determined that the
750       // typed string is similar to an engine domain. We can let the
751       // tab-to-search result through.
752       if (state.context.heuristicResult?.type == UrlbarUtils.RESULT_TYPE.URL) {
753         // Discard the result if the heuristic result is not autofill and we are
754         // not making an exception for a fuzzy match.
755         if (
756           !state.context.heuristicResult.autofill &&
757           !result.payload.satisfiesAutofillThreshold
758         ) {
759           return false;
760         }
762         let autofillHostname = new URL(
763           state.context.heuristicResult.payload.url
764         ).hostname;
765         let [autofillDomain] = UrlbarUtils.stripPrefixAndTrim(
766           autofillHostname,
767           {
768             stripWww: true,
769           }
770         );
771         // Strip the public suffix because we want to allow matching "domain.it"
772         // with "domain.com".
773         autofillDomain = UrlbarUtils.stripPublicSuffixFromHost(autofillDomain);
774         if (!autofillDomain) {
775           return false;
776         }
778         // For tab-to-search results, result.payload.url is the engine's domain
779         // with the public suffix already stripped, for example "www.mozilla.".
780         let [engineDomain] = UrlbarUtils.stripPrefixAndTrim(
781           result.payload.url,
782           {
783             stripWww: true,
784           }
785         );
786         // Discard if the engine domain does not end with the autofilled one.
787         if (!engineDomain.endsWith(autofillDomain)) {
788           return false;
789         }
790       }
791     }
793     // Discard "Search in a Private Window" if appropriate.
794     if (
795       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
796       result.payload.inPrivateWindow &&
797       !state.canShowPrivateSearch
798     ) {
799       return false;
800     }
802     // Discard form history and remote suggestions that dupe previously added
803     // suggestions or the heuristic.
804     if (
805       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
806       result.payload.lowerCaseSuggestion
807     ) {
808       let suggestion = result.payload.lowerCaseSuggestion.trim();
809       if (!suggestion || state.suggestions.has(suggestion)) {
810         return false;
811       }
812     }
814     // Discard tail suggestions if appropriate.
815     if (
816       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
817       result.payload.tail &&
818       !state.canShowTailSuggestions
819     ) {
820       return false;
821     }
823     // Discard remote tab results that dupes another remote tab or a
824     // switch-to-tab result.
825     if (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
826       if (state.addedRemoteTabUrls.has(result.payload.url)) {
827         return false;
828       }
829       let maybeDupeType = state.urlToTabResultType.get(result.payload.url);
830       if (maybeDupeType == UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
831         return false;
832       }
833     }
835     // Discard switch-to-tab results that dupes another switch-to-tab result.
836     if (
837       result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
838       state.addedSwitchTabUrls.has(result.payload.url)
839     ) {
840       return false;
841     }
843     // Discard history results that dupe either remote or switch-to-tab results.
844     if (
845       !result.heuristic &&
846       result.type == UrlbarUtils.RESULT_TYPE.URL &&
847       result.payload.url &&
848       state.urlToTabResultType.has(result.payload.url)
849     ) {
850       return false;
851     }
853     // Discard SERPs from browser history that dupe either the heuristic or
854     // previously added suggestions.
855     if (
856       result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
857       result.type == UrlbarUtils.RESULT_TYPE.URL
858     ) {
859       let submission = Services.search.parseSubmissionURL(result.payload.url);
860       if (submission) {
861         let resultQuery = submission.terms.trim().toLocaleLowerCase();
862         if (state.suggestions.has(resultQuery)) {
863           // If the result's URL is the same as a brand new SERP URL created
864           // from the query string modulo certain URL params, then treat the
865           // result as a dupe and discard it.
866           let [newSerpURL] = UrlbarUtils.getSearchQueryUrl(
867             submission.engine,
868             submission.terms
869           );
870           if (
871             lazy.UrlbarSearchUtils.serpsAreEquivalent(
872               result.payload.url,
873               newSerpURL
874             )
875           ) {
876             return false;
877           }
878         }
879       }
880     }
882     // When in an engine search mode, discard URL results whose hostnames don't
883     // include the root domain of the search mode engine.
884     if (state.context.searchMode?.engineName && result.payload.url) {
885       let engine = Services.search.getEngineByName(
886         state.context.searchMode.engineName
887       );
888       if (engine) {
889         let searchModeRootDomain = lazy.UrlbarSearchUtils.getRootDomainFromEngine(
890           engine
891         );
892         let resultUrl = new URL(result.payload.url);
893         // Add a trailing "." to increase the stringency of the check. This
894         // check covers most general cases. Some edge cases are not covered,
895         // like `resultUrl` being ebay.mydomain.com, which would escape this
896         // check if `searchModeRootDomain` was "ebay".
897         if (!resultUrl.hostname.includes(`${searchModeRootDomain}.`)) {
898           return false;
899         }
900       }
901     }
903     // Discard history results that dupe the quick suggest result.
904     if (
905       state.quickSuggestResult &&
906       !result.heuristic &&
907       result.type == UrlbarUtils.RESULT_TYPE.URL &&
908       lazy.QuickSuggest.isURLEquivalentToResultURL(
909         result.payload.url,
910         state.quickSuggestResult
911       )
912     ) {
913       return false;
914     }
916     // Discard history results whose URLs were originally sponsored. We use the
917     // presence of a partner's URL search param to detect these. The param is
918     // defined in the pref below, which is also used for the newtab page.
919     if (
920       result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
921       result.type == UrlbarUtils.RESULT_TYPE.URL
922     ) {
923       let param = Services.prefs.getCharPref(
924         "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam"
925       );
926       if (param) {
927         let [key, value] = param.split("=");
928         let searchParams;
929         try {
930           ({ searchParams } = new URL(result.payload.url));
931         } catch (error) {}
932         if (
933           (value === undefined && searchParams?.has(key)) ||
934           (value !== undefined && searchParams?.getAll(key).includes(value))
935         ) {
936           return false;
937         }
938       }
939     }
941     // Heuristic results must always be the first result.  If this result is a
942     // heuristic but we've already added results, discard it.  Normally this
943     // should never happen because the standard result groups are set up so
944     // that there's always at most one heuristic and it's always first, but
945     // since result groups are stored in a pref and can therefore be modified
946     // by the user, we perform this check.
947     if (result.heuristic && state.usedResultSpan) {
948       return false;
949     }
951     // Google search engine might suggest a result for unit conversion with
952     // format that starts with "= ". If our UnitConversion can provide the
953     // result, we discard the suggestion of Google in order to deduplicate.
954     if (
955       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
956       result.payload.engine == "Google" &&
957       result.payload.suggestion?.startsWith("= ") &&
958       state.hasUnitConversionResult
959     ) {
960       return false;
961     }
963     // Include the result.
964     return true;
965   }
967   /**
968    * Updates the global state that we use to make decisions during sort.  This
969    * should be called for results before we've decided whether to add or discard
970    * them.
971    *
972    * @param {UrlbarResult} result
973    *   The result.
974    * @param {object} state
975    *   Global state that we use to make decisions during this sort.
976    */
977   _updateStatePreAdd(result, state) {
978     // Keep track of the largest heuristic result span.
979     if (result.heuristic && this._canAddResult(result, state)) {
980       state.maxHeuristicResultSpan = Math.max(
981         state.maxHeuristicResultSpan,
982         UrlbarUtils.getSpanForResult(result)
983       );
984     }
986     // Subtract from `availableResultSpan` the span of global suggestedIndex
987     // results so there will be room for them at the end of the sort. Except
988     // when `maxRichResults` is zero and other special cases, we assume
989     // suggestedIndex results will always be shown regardless of the total
990     // available result span, `context.maxResults`, and `maxRichResults`.
991     if (
992       result.hasSuggestedIndex &&
993       !result.isSuggestedIndexRelativeToGroup &&
994       this._canAddResult(result, state)
995     ) {
996       let span = UrlbarUtils.getSpanForResult(result);
997       if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
998         state.maxTabToSearchResultSpan = Math.max(
999           state.maxTabToSearchResultSpan,
1000           span
1001         );
1002       } else {
1003         state.availableResultSpan = Math.max(
1004           state.availableResultSpan - span,
1005           0
1006         );
1007       }
1008     }
1010     // Save some state we'll use later to dedupe URL results.
1011     if (
1012       (result.type == UrlbarUtils.RESULT_TYPE.URL ||
1013         result.type == UrlbarUtils.RESULT_TYPE.KEYWORD) &&
1014       result.payload.url &&
1015       (!result.heuristic || !lazy.UrlbarPrefs.get("experimental.hideHeuristic"))
1016     ) {
1017       let [strippedUrl, prefix] = UrlbarUtils.stripPrefixAndTrim(
1018         result.payload.url,
1019         {
1020           stripHttp: true,
1021           stripHttps: true,
1022           stripWww: true,
1023           trimEmptyQuery: true,
1024         }
1025       );
1026       let prefixRank = UrlbarUtils.getPrefixRank(prefix);
1027       let topPrefixData = state.strippedUrlToTopPrefixAndTitle.get(strippedUrl);
1028       let topPrefixRank = topPrefixData ? topPrefixData.rank : -1;
1029       if (
1030         topPrefixRank < prefixRank ||
1031         // If a quick suggest result has the same stripped URL and prefix rank
1032         // as another result, store the quick suggest as the top rank so we
1033         // discard the other during deduping. That happens after the user picks
1034         // the quick suggest: The URL is added to history and later both a
1035         // history result and the quick suggest may match a query.
1036         (topPrefixRank == prefixRank &&
1037           result.providerName == lazy.UrlbarProviderQuickSuggest.name)
1038       ) {
1039         // strippedUrl => { prefix, title, rank, providerName }
1040         state.strippedUrlToTopPrefixAndTitle.set(strippedUrl, {
1041           prefix,
1042           title: result.payload.title,
1043           rank: prefixRank,
1044           providerName: result.providerName,
1045         });
1046       }
1047     }
1049     // Save some state we'll use later to dedupe results from open/remote tabs.
1050     if (
1051       result.payload.url &&
1052       (result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH ||
1053         (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB &&
1054           !state.urlToTabResultType.has(result.payload.url)))
1055     ) {
1056       // url => result type
1057       state.urlToTabResultType.set(result.payload.url, result.type);
1058     }
1060     // If we find results other than the heuristic, "Search in Private
1061     // Window," or tail suggestions, then we should hide tail suggestions
1062     // since they're a last resort.
1063     if (
1064       state.canShowTailSuggestions &&
1065       !result.heuristic &&
1066       (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
1067         (!result.payload.inPrivateWindow && !result.payload.tail))
1068     ) {
1069       state.canShowTailSuggestions = false;
1070     }
1072     if (result.providerName == lazy.UrlbarProviderQuickSuggest.name) {
1073       state.quickSuggestResult = result;
1074     }
1076     if (result.providerName == lazy.UrlbarProviderWeather.name) {
1077       state.weatherResult = result;
1078     }
1080     state.hasUnitConversionResult =
1081       state.hasUnitConversionResult || result.providerName == "UnitConversion";
1082   }
1084   /**
1085    * Updates the global state that we use to make decisions during sort.  This
1086    * should be called for results after they've been added.  It should not be
1087    * called for discarded results.
1088    *
1089    * @param {UrlbarResult} result
1090    *   The result.
1091    * @param {object} state
1092    *   Global state that we use to make decisions during this sort.
1093    */
1094   _updateStatePostAdd(result, state) {
1095     // Update heuristic state.
1096     if (result.heuristic) {
1097       state.context.heuristicResult = result;
1098       if (
1099         result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
1100         result.payload.query &&
1101         !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
1102       ) {
1103         let query = result.payload.query.trim().toLocaleLowerCase();
1104         if (query) {
1105           state.suggestions.add(query);
1106         }
1107       }
1108     }
1110     // The "Search in a Private Window" result should only be shown when there
1111     // are other results and all of them are searches.  It should not be shown
1112     // if the user typed an alias because that's an explicit engine choice.
1113     if (
1114       !Services.search.separatePrivateDefaultUrlbarResultEnabled ||
1115       (state.canShowPrivateSearch &&
1116         (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
1117           result.payload.providesSearchMode ||
1118           (result.heuristic && result.payload.keyword)))
1119     ) {
1120       state.canShowPrivateSearch = false;
1121     }
1123     // Update suggestions.
1124     if (
1125       result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
1126       result.payload.lowerCaseSuggestion
1127     ) {
1128       let suggestion = result.payload.lowerCaseSuggestion.trim();
1129       if (suggestion) {
1130         state.suggestions.add(suggestion);
1131       }
1132     }
1134     // Avoid multiple tab-to-search results.
1135     // TODO (Bug 1670185): figure out better strategies to manage this case.
1136     if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
1137       state.canAddTabToSearch = false;
1138       // We want to record in urlbar.tips once per engagement per engine. Since
1139       // whether these results are shown is dependent on the Muxer, we must
1140       // add to `enginesShown` here.
1141       if (result.payload.dynamicType) {
1142         lazy.UrlbarProviderTabToSearch.enginesShown.onboarding.add(
1143           result.payload.engine
1144         );
1145       } else {
1146         lazy.UrlbarProviderTabToSearch.enginesShown.regular.add(
1147           result.payload.engine
1148         );
1149       }
1150     }
1152     // Sync will send us duplicate remote tabs if multiple copies of a tab are
1153     // open on a synced client. Keep track of which remote tabs we've added to
1154     // dedupe these.
1155     if (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
1156       state.addedRemoteTabUrls.add(result.payload.url);
1157     }
1159     // Keep track of which switch tabs we've added to dedupe switch tabs.
1160     if (result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
1161       state.addedSwitchTabUrls.add(result.payload.url);
1162     }
1163   }
1165   /**
1166    * Inserts results with suggested indexes. This can be called for either
1167    * global or group-relative suggestedIndex results. It should be called after
1168    * `sortedResults` has been filled in.
1169    *
1170    * @param {Array} suggestedIndexResults
1171    *   Results with a `suggestedIndex` property.
1172    * @param {Array} sortedResults
1173    *   The sorted results. For global suggestedIndex results, this should be the
1174    *   final list of all results before suggestedIndex results are inserted. For
1175    *   group-relative suggestedIndex results, this should be the final list of
1176    *   results in the group before group-relative suggestedIndex results are
1177    *   inserted.
1178    * @param {object} state
1179    *   Global state that we use to make decisions during this sort.
1180    * @returns {object}
1181    *   A `usedLimits` object that describes the total span and count of all the
1182    *   added results. See `_addResults`.
1183    */
1184   _addSuggestedIndexResults(suggestedIndexResults, sortedResults, state) {
1185     let usedLimits = {
1186       availableSpan: 0,
1187       maxResultCount: 0,
1188     };
1190     if (!suggestedIndexResults?.length) {
1191       // This is just a slight optimization; no need to continue.
1192       return usedLimits;
1193     }
1195     // Partition the results into positive- and negative-index arrays. Positive
1196     // indexes are relative to the start of the list and negative indexes are
1197     // relative to the end.
1198     let positive = [];
1199     let negative = [];
1200     for (let result of suggestedIndexResults) {
1201       let results = result.suggestedIndex < 0 ? negative : positive;
1202       results.push(result);
1203     }
1205     // Sort the positive results ascending so that results at the end of the
1206     // array don't end up offset by later insertions at the front.
1207     positive.sort((a, b) => {
1208       if (a.suggestedIndex !== b.suggestedIndex) {
1209         return a.suggestedIndex - b.suggestedIndex;
1210       }
1212       if (a.providerName === b.providerName) {
1213         return 0;
1214       }
1216       // If same suggestedIndex, change the displaying order along to following
1217       // provider priority.
1218       // TabToSearch > QuickSuggest > Other providers
1219       if (a.providerName === lazy.UrlbarProviderTabToSearch.name) {
1220         return 1;
1221       }
1222       if (b.providerName === lazy.UrlbarProviderTabToSearch.name) {
1223         return -1;
1224       }
1225       if (a.providerName === lazy.UrlbarProviderQuickSuggest.name) {
1226         return 1;
1227       }
1228       if (b.providerName === lazy.UrlbarProviderQuickSuggest.name) {
1229         return -1;
1230       }
1232       return 0;
1233     });
1235     // Conversely, sort the negative results descending so that results at the
1236     // front of the array don't end up offset by later insertions at the end.
1237     negative.sort((a, b) => b.suggestedIndex - a.suggestedIndex);
1239     // Insert the results. We start with the positive results because we have
1240     // tests that assume they're inserted first. In practice it shouldn't matter
1241     // because there's no good reason we would ever have a negative result come
1242     // before a positive result in the same query. Even if we did, we have to
1243     // insert one before the other, and there's no right or wrong order.
1244     for (let results of [positive, negative]) {
1245       let prevResult;
1246       let prevIndex;
1247       for (let result of results) {
1248         if (this._canAddResult(result, state)) {
1249           let index;
1250           if (
1251             prevResult &&
1252             prevResult.suggestedIndex == result.suggestedIndex
1253           ) {
1254             index = prevIndex;
1255           } else {
1256             index =
1257               result.suggestedIndex >= 0
1258                 ? Math.min(result.suggestedIndex, sortedResults.length)
1259                 : Math.max(result.suggestedIndex + sortedResults.length + 1, 0);
1260           }
1261           prevResult = result;
1262           prevIndex = index;
1263           sortedResults.splice(index, 0, result);
1264           usedLimits.availableSpan += UrlbarUtils.getSpanForResult(result);
1265           usedLimits.maxResultCount++;
1266           this._updateStatePostAdd(result, state);
1267         }
1268       }
1269     }
1271     return usedLimits;
1272   }
1275 export var UrlbarMuxerUnifiedComplete = new MuxerUnifiedComplete();