tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

UrlbarMuxerStandard.sys.mjs (57777B)


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