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/. */
6 * This module exports a component used to sort results in a UrlbarQueryContext.
9 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
14 } from "resource:///modules/UrlbarUtils.sys.mjs";
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",
29 XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
30 UrlbarUtils.getLogger({ prefix: "MuxerUnifiedComplete" })
34 * Class used to create a muxer.
35 * The muxer receives and sorts results in a UrlbarQueryContext.
37 class MuxerUnifiedComplete extends UrlbarMuxer {
43 return "UnifiedComplete";
47 * Sorts results in the given UrlbarQueryContext.
49 * @param {UrlbarQueryContext} context
51 * @param {Array} unsortedResults
52 * The array of UrlbarResult that is not sorted yet.
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
62 // Global state we'll use to make decisions during this sort.
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.
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.
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);
99 result.hasSuggestedIndex && result.isSuggestedIndexRelativeToGroup
100 ? state.suggestedIndexResultsByGroup
101 : state.resultsByGroup;
102 let results = resultsByGroup.get(group);
105 resultsByGroup.set(group, results);
107 results.push(result);
109 // Update pre-add state.
110 this._updateStatePreAdd(result, state);
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,
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
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(
147 { availableSpan: state.availableResultSpan, maxResultCount: Infinity },
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
161 if (suggestedIndexResults) {
162 this._addSuggestedIndexResults(
163 suggestedIndexResults,
170 context.results = sortedResults;
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
179 * @param {object} state
180 * The muxer state to copy.
182 * A deep copy of the state.
185 let copy = Object.assign({}, state, {
186 resultsByGroup: new Map(),
187 suggestedIndexResultsByGroup: new Map(),
188 strippedUrlToTopPrefixAndTitle: new Map(
189 state.strippedUrlToTopPrefixAndTitle
191 urlToTabResultType: new Map(state.urlToTabResultType),
192 addedRemoteTabUrls: new Set(state.addedRemoteTabUrls),
193 addedSwitchTabUrls: new Set(state.addedSwitchTabUrls),
194 suggestions: new Set(state.suggestions),
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]);
208 * Recursively fills a result group and its children.
210 * There are two ways to limit the number of results in a group:
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
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
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.
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
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
241 * `[results, usedLimits, hasMoreResults]` -- see `_addResults`.
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(
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);
258 limits = { ...limits };
259 limits.availableSpan = Math.max(limits.availableSpan - span, 0);
260 limits.maxResultCount = Math.max(
261 limits.maxResultCount - suggestedIndexResults.length,
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,
280 for (let [key, value] of Object.entries(suggestedIndexUsedLimits)) {
281 usedLimits[key] += value;
285 return [results, usedLimits, hasMoreResults];
289 * Helper for `_fillGroup` that fills a group's children.
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
298 * @param {Array} flexDataArray
299 * See `_updateFlexData`.
301 * `[results, usedLimits, hasMoreResults]` -- see `_addResults`.
303 _fillGroupChildren(group, limits, state, flexDataArray = null) {
304 // If the group has flexed children, update the data we use during flex
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].
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.
326 if (group.flexChildren) {
327 stateCopy = this._copyState(state);
328 flexDataArray = this._updateFlexData(group, limits, flexDataArray);
331 // Fill each child group, collecting all results in the `results` array.
334 for (let key of Object.keys(limits)) {
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]
349 typeof child[key] == "number" ? child[key] : Infinity,
350 limits[key] - usedLimits[key]
354 // Recurse and fill the child.
359 ] = this._fillGroup(child, childLimits, state);
360 results = results.concat(childResults);
361 for (let key of Object.keys(usedLimits)) {
362 usedLimits[key] += childUsedLimits[key];
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
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(
389 // Update `state` in place so that it's also updated in the caller.
390 for (let [key, value] of Object.entries(stateCopy)) {
395 return [results, usedLimits, anyChildHasMoreResults];
399 * Updates flex-related state used while filling a group.
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.
414 * A new `flexDataArray` when called in the first pass, and `flexDataArray`
415 * itself when called in subsequent passes.
417 _updateFlexData(group, limits, flexDataArray) {
420 group.children.map((child, index) => {
422 // The index of the corresponding child in `group.children`.
424 // The child's limits.
426 // The fractional parts of the child's unrounded limits; see below.
428 // The used-up portions of the child's limits.
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,
436 for (let key of Object.keys(limits)) {
437 data.limits[key] = 0;
438 data.limitFractions[key] = 0;
439 data.usedLimits[key] = 0;
444 // The data objects for children with more results (i.e., that are still
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);
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];
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];
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.
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]
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.
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;
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.
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;
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!");
545 let data = flexDataArray[fractionalDataArray.shift().index];
546 data.limits[key] += diff;
547 summedFillableLimit += diff;
552 return flexDataArray;
556 * Adds results to a group using the results from its `RESULT_GROUP` in
557 * `state.resultsByGroup`.
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.
566 * `[results, usedLimits, hasMoreResults]` where:
567 * results: A flat array of results in the group, empty if no results
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.
577 _addResults(groupConst, limits, state) {
579 for (let key of Object.keys(limits)) {
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.
588 groupConst == UrlbarUtils.RESULT_GROUP.FORM_HISTORY &&
589 !lazy.UrlbarPrefs.get("maxHistoricalSearchSuggestions")
591 // Create a new `limits` object so we don't modify the caller's.
592 limits = { ...limits };
593 limits.maxResultCount = 0;
596 let addedResults = [];
597 let groupResults = state.resultsByGroup.get(groupConst);
599 groupResults?.length &&
600 state.usedResultSpan < state.availableResultSpan &&
601 [...Object.entries(limits)].every(([k, limit]) => usedLimits[k] < limit)
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.
613 addedResults.push(result);
614 usedLimits.availableSpan = newUsedSpan;
615 usedLimits.maxResultCount++;
616 state.usedResultSpan += span;
617 this._updateStatePostAdd(result, state);
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();
627 return [addedResults, usedLimits, !!groupResults?.length];
631 * Returns whether a result can be added to its group given the current sort
634 * @param {UrlbarResult} result
636 * @param {object} state
637 * Global state that we use to make decisions during this sort.
639 * True if the result can be added and false if it should be discarded.
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) {
652 let heuristicUrl = state.context.heuristicResult?.payload.url;
655 result.payload.subtype ==
656 lazy.UrlbarProviderQuickSuggest.RESULT_SUBTYPE.NAVIGATIONAL &&
657 !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
665 result.payload.dupedHeuristic =
666 UrlbarUtils.stripPrefixAndTrim(heuristicUrl, opts)[0] ==
667 UrlbarUtils.stripPrefixAndTrim(result.payload.url, opts)[0];
668 return !result.payload.dupedHeuristic;
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
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
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.
687 result.type == UrlbarUtils.RESULT_TYPE.URL &&
690 let [strippedUrl, prefix] = UrlbarUtils.stripPrefixAndTrim(
696 trimEmptyQuery: true,
699 let topPrefixData = state.strippedUrlToTopPrefixAndTitle.get(strippedUrl);
700 // If the condition below is not met, we are deduping a result against
704 (prefix != topPrefixData.prefix ||
705 result.providerName != topPrefixData.providerName)
707 let prefixRank = UrlbarUtils.getPrefixRank(prefix);
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)
720 // Discard results that dupe autofill.
722 state.context.heuristicResult &&
723 state.context.heuristicResult.autofill &&
725 state.context.heuristicResult.payload?.url == result.payload.url &&
726 state.context.heuristicResult.type == result.type &&
727 !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
732 // HeuristicFallback may add non-heuristic results in some cases, but those
733 // should be retained only if the heuristic result comes from it.
736 result.providerName == "HeuristicFallback" &&
737 state.context.heuristicResult?.providerName != "HeuristicFallback"
742 if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
743 // Discard the result if a tab-to-search result was added already.
744 if (!state.canAddTabToSearch) {
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.
756 !state.context.heuristicResult.autofill &&
757 !result.payload.satisfiesAutofillThreshold
762 let autofillHostname = new URL(
763 state.context.heuristicResult.payload.url
765 let [autofillDomain] = UrlbarUtils.stripPrefixAndTrim(
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) {
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(
786 // Discard if the engine domain does not end with the autofilled one.
787 if (!engineDomain.endsWith(autofillDomain)) {
793 // Discard "Search in a Private Window" if appropriate.
795 result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
796 result.payload.inPrivateWindow &&
797 !state.canShowPrivateSearch
802 // Discard form history and remote suggestions that dupe previously added
803 // suggestions or the heuristic.
805 result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
806 result.payload.lowerCaseSuggestion
808 let suggestion = result.payload.lowerCaseSuggestion.trim();
809 if (!suggestion || state.suggestions.has(suggestion)) {
814 // Discard tail suggestions if appropriate.
816 result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
817 result.payload.tail &&
818 !state.canShowTailSuggestions
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)) {
829 let maybeDupeType = state.urlToTabResultType.get(result.payload.url);
830 if (maybeDupeType == UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
835 // Discard switch-to-tab results that dupes another switch-to-tab result.
837 result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
838 state.addedSwitchTabUrls.has(result.payload.url)
843 // Discard history results that dupe either remote or switch-to-tab results.
846 result.type == UrlbarUtils.RESULT_TYPE.URL &&
847 result.payload.url &&
848 state.urlToTabResultType.has(result.payload.url)
853 // Discard SERPs from browser history that dupe either the heuristic or
854 // previously added suggestions.
856 result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
857 result.type == UrlbarUtils.RESULT_TYPE.URL
859 let submission = Services.search.parseSubmissionURL(result.payload.url);
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(
871 lazy.UrlbarSearchUtils.serpsAreEquivalent(
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
889 let searchModeRootDomain = lazy.UrlbarSearchUtils.getRootDomainFromEngine(
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}.`)) {
903 // Discard history results that dupe the quick suggest result.
905 state.quickSuggestResult &&
907 result.type == UrlbarUtils.RESULT_TYPE.URL &&
908 lazy.QuickSuggest.isURLEquivalentToResultURL(
910 state.quickSuggestResult
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.
920 result.source == UrlbarUtils.RESULT_SOURCE.HISTORY &&
921 result.type == UrlbarUtils.RESULT_TYPE.URL
923 let param = Services.prefs.getCharPref(
924 "browser.newtabpage.activity-stream.hideTopSitesWithSearchParam"
927 let [key, value] = param.split("=");
930 ({ searchParams } = new URL(result.payload.url));
933 (value === undefined && searchParams?.has(key)) ||
934 (value !== undefined && searchParams?.getAll(key).includes(value))
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) {
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.
955 result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
956 result.payload.engine == "Google" &&
957 result.payload.suggestion?.startsWith("= ") &&
958 state.hasUnitConversionResult
963 // Include the result.
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
972 * @param {UrlbarResult} result
974 * @param {object} state
975 * Global state that we use to make decisions during this sort.
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)
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`.
992 result.hasSuggestedIndex &&
993 !result.isSuggestedIndexRelativeToGroup &&
994 this._canAddResult(result, state)
996 let span = UrlbarUtils.getSpanForResult(result);
997 if (result.providerName == lazy.UrlbarProviderTabToSearch.name) {
998 state.maxTabToSearchResultSpan = Math.max(
999 state.maxTabToSearchResultSpan,
1003 state.availableResultSpan = Math.max(
1004 state.availableResultSpan - span,
1010 // Save some state we'll use later to dedupe URL results.
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"))
1017 let [strippedUrl, prefix] = UrlbarUtils.stripPrefixAndTrim(
1023 trimEmptyQuery: true,
1026 let prefixRank = UrlbarUtils.getPrefixRank(prefix);
1027 let topPrefixData = state.strippedUrlToTopPrefixAndTitle.get(strippedUrl);
1028 let topPrefixRank = topPrefixData ? topPrefixData.rank : -1;
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)
1039 // strippedUrl => { prefix, title, rank, providerName }
1040 state.strippedUrlToTopPrefixAndTitle.set(strippedUrl, {
1042 title: result.payload.title,
1044 providerName: result.providerName,
1049 // Save some state we'll use later to dedupe results from open/remote tabs.
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)))
1056 // url => result type
1057 state.urlToTabResultType.set(result.payload.url, result.type);
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.
1064 state.canShowTailSuggestions &&
1065 !result.heuristic &&
1066 (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
1067 (!result.payload.inPrivateWindow && !result.payload.tail))
1069 state.canShowTailSuggestions = false;
1072 if (result.providerName == lazy.UrlbarProviderQuickSuggest.name) {
1073 state.quickSuggestResult = result;
1076 if (result.providerName == lazy.UrlbarProviderWeather.name) {
1077 state.weatherResult = result;
1080 state.hasUnitConversionResult =
1081 state.hasUnitConversionResult || result.providerName == "UnitConversion";
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.
1089 * @param {UrlbarResult} result
1091 * @param {object} state
1092 * Global state that we use to make decisions during this sort.
1094 _updateStatePostAdd(result, state) {
1095 // Update heuristic state.
1096 if (result.heuristic) {
1097 state.context.heuristicResult = result;
1099 result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
1100 result.payload.query &&
1101 !lazy.UrlbarPrefs.get("experimental.hideHeuristic")
1103 let query = result.payload.query.trim().toLocaleLowerCase();
1105 state.suggestions.add(query);
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.
1114 !Services.search.separatePrivateDefaultUrlbarResultEnabled ||
1115 (state.canShowPrivateSearch &&
1116 (result.type != UrlbarUtils.RESULT_TYPE.SEARCH ||
1117 result.payload.providesSearchMode ||
1118 (result.heuristic && result.payload.keyword)))
1120 state.canShowPrivateSearch = false;
1123 // Update suggestions.
1125 result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
1126 result.payload.lowerCaseSuggestion
1128 let suggestion = result.payload.lowerCaseSuggestion.trim();
1130 state.suggestions.add(suggestion);
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
1146 lazy.UrlbarProviderTabToSearch.enginesShown.regular.add(
1147 result.payload.engine
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
1155 if (result.type == UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
1156 state.addedRemoteTabUrls.add(result.payload.url);
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);
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.
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
1178 * @param {object} state
1179 * Global state that we use to make decisions during this sort.
1181 * A `usedLimits` object that describes the total span and count of all the
1182 * added results. See `_addResults`.
1184 _addSuggestedIndexResults(suggestedIndexResults, sortedResults, state) {
1190 if (!suggestedIndexResults?.length) {
1191 // This is just a slight optimization; no need to continue.
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.
1200 for (let result of suggestedIndexResults) {
1201 let results = result.suggestedIndex < 0 ? negative : positive;
1202 results.push(result);
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;
1212 if (a.providerName === b.providerName) {
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) {
1222 if (b.providerName === lazy.UrlbarProviderTabToSearch.name) {
1225 if (a.providerName === lazy.UrlbarProviderQuickSuggest.name) {
1228 if (b.providerName === lazy.UrlbarProviderQuickSuggest.name) {
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]) {
1247 for (let result of results) {
1248 if (this._canAddResult(result, state)) {
1252 prevResult.suggestedIndex == result.suggestedIndex
1257 result.suggestedIndex >= 0
1258 ? Math.min(result.suggestedIndex, sortedResults.length)
1259 : Math.max(result.suggestedIndex + sortedResults.length + 1, 0);
1261 prevResult = result;
1263 sortedResults.splice(index, 0, result);
1264 usedLimits.availableSpan += UrlbarUtils.getSpanForResult(result);
1265 usedLimits.maxResultCount++;
1266 this._updateStatePostAdd(result, state);
1275 export var UrlbarMuxerUnifiedComplete = new MuxerUnifiedComplete();