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"use strict";
6
7/**
8 * This module exports the UrlbarPrefs singleton, which manages preferences for
9 * the urlbar. It also provides access to urlbar Nimbus features as if they are
10 * preferences.
11 */
12
13var EXPORTED_SYMBOLS = ["UrlbarPrefs", "UrlbarPrefsObserver"];
14
15const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
16const { XPCOMUtils } = ChromeUtils.import(
17  "resource://gre/modules/XPCOMUtils.jsm"
18);
19
20XPCOMUtils.defineLazyModuleGetters(this, {
21  NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
22  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
23});
24
25const PREF_URLBAR_BRANCH = "browser.urlbar.";
26
27// Prefs are defined as [pref name, default value] or [pref name, [default
28// value, type]].  In the former case, the getter method name is inferred from
29// the typeof the default value.
30//
31// NOTE: Don't name prefs (relative to the `browser.urlbar` branch) the same as
32// Nimbus urlbar features. Doing so would cause a name collision because pref
33// names and Nimbus feature names are both kept as keys in UrlbarPref's map. For
34// a list of Nimbus features, see: toolkit/components/nimbus/FeatureManifest.js
35const PREF_URLBAR_DEFAULTS = new Map([
36  // Whether we announce to screen readers when tab-to-search results are
37  // inserted.
38  ["accessibility.tabToSearch.announceResults", true],
39
40  // "Autofill" is the name of the feature that automatically completes domains
41  // and URLs that the user has visited as the user is typing them in the urlbar
42  // textbox.  If false, autofill will be disabled.
43  ["autoFill", true],
44
45  // If true, the domains of the user's installed search engines will be
46  // autofilled even if the user hasn't actually visited them.
47  ["autoFill.searchEngines", false],
48
49  // Affects the frecency threshold of the autofill algorithm.  The threshold is
50  // the mean of all origin frecencies plus one standard deviation multiplied by
51  // this value.  See UrlbarProviderPlaces.
52  ["autoFill.stddevMultiplier", [0.0, "float"]],
53
54  // Whether using `ctrl` when hitting return/enter in the URL bar
55  // (or clicking 'go') should prefix 'www.' and suffix
56  // browser.fixup.alternate.suffix to the URL bar value prior to
57  // navigating.
58  ["ctrlCanonizesURLs", true],
59
60  // Whether copying the entire URL from the location bar will put a human
61  // readable (percent-decoded) URL on the clipboard.
62  ["decodeURLsOnCopy", false],
63
64  // The amount of time (ms) to wait after the user has stopped typing before
65  // fetching results.  However, we ignore this for the very first result (the
66  // "heuristic" result).  We fetch it as fast as possible.
67  ["delay", 50],
68
69  // Some performance tests disable this because extending the urlbar needs
70  // layout information that we can't get before the first paint. (Or we could
71  // but this would mean flushing layout.)
72  ["disableExtendForTests", false],
73
74  // Ensure we use trailing dots for DNS lookups for single words that could
75  // be hosts.
76  ["dnsResolveFullyQualifiedNames", true],
77
78  // Controls when to DNS resolve single word search strings, after they were
79  // searched for. If the string is resolved as a valid host, show a
80  // "Did you mean to go to 'host'" prompt.
81  // 0 - never resolve; 1 - use heuristics (default); 2 - always resolve
82  ["dnsResolveSingleWordsAfterSearch", 1],
83
84  // Whether telemetry events should be recorded.
85  ["eventTelemetry.enabled", false],
86
87  // Whether we expand the font size when when the urlbar is
88  // focused.
89  ["experimental.expandTextOnFocus", false],
90
91  // Whether the urlbar displays a permanent search button.
92  ["experimental.searchButton", false],
93
94  // When we send events to extensions, we wait this amount of time in
95  // milliseconds for them to respond before timing out.
96  ["extension.timeout", 400],
97
98  // When true, `javascript:` URLs are not included in search results.
99  ["filter.javascript", true],
100
101  // Applies URL highlighting and other styling to the text in the urlbar input.
102  ["formatting.enabled", true],
103
104  // Whether the results panel should be kept open during IME composition.
105  ["keepPanelOpenDuringImeComposition", false],
106
107  // For search suggestion results, we truncate the user's search string to this
108  // number of characters before fetching results.
109  ["maxCharsForSearchSuggestions", 20],
110
111  // The maximum number of form history results to include.
112  ["maxHistoricalSearchSuggestions", 0],
113
114  // The maximum number of results in the urlbar popup.
115  ["maxRichResults", 10],
116
117  // Whether addresses and search results typed into the address bar
118  // should be opened in new tabs by default.
119  ["openintab", false],
120
121  // When true, URLs in the user's history that look like search result pages
122  // are styled to look like search engine results instead of the usual history
123  // results.
124  ["restyleSearches", false],
125
126  // Controls the composition of results.  The default value is computed by
127  // calling:
128  //   makeResultBuckets({
129  //     showSearchSuggestionsFirst: UrlbarPrefs.get(
130  //       "showSearchSuggestionsFirst"
131  //     ),
132  //   });
133  // The value of this pref is a JSON string of the root bucket.  See below.
134  ["resultGroups", ""],
135
136  // If true, we show tail suggestions when available.
137  ["richSuggestions.tail", true],
138
139  // Hidden pref. Disables checks that prevent search tips being shown, thus
140  // showing them every time the newtab page or the default search engine
141  // homepage is opened.
142  ["searchTips.test.ignoreShowLimits", false],
143
144  // Whether to show each local search shortcut button in the view.
145  ["shortcuts.bookmarks", true],
146  ["shortcuts.tabs", true],
147  ["shortcuts.history", true],
148
149  // Whether to show search suggestions before general results.
150  ["showSearchSuggestionsFirst", true],
151
152  // Whether speculative connections should be enabled.
153  ["speculativeConnect.enabled", true],
154
155  // Whether results will include the user's bookmarks.
156  ["suggest.bookmark", true],
157
158  // Whether results will include the user's history.
159  ["suggest.history", true],
160
161  // Whether results will include switch-to-tab results.
162  ["suggest.openpage", true],
163
164  // Whether results will include search suggestions.
165  ["suggest.searches", false],
166
167  // Whether results will include search engines (e.g. tab-to-search).
168  ["suggest.engines", true],
169
170  // Whether results will include top sites and the view will open on focus.
171  ["suggest.topsites", true],
172
173  // Whether results will include a calculator.
174  ["suggest.calculator", false],
175
176  // Whether results will include QuickSuggest suggestions.
177  ["suggest.quicksuggest", true],
178
179  // Whether the user has seen the onboarding dialog.
180  ["quicksuggest.showedOnboardingDialog", false],
181
182  // Count the restarts before showing the onboarding dialog.
183  ["quicksuggest.seenRestarts", 0],
184
185  // Whether to show QuickSuggest related logs.
186  ["quicksuggest.log", false],
187
188  // When using switch to tabs, if set to true this will move the tab into the
189  // active window.
190  ["switchTabs.adoptIntoActiveWindow", false],
191
192  // The number of remaining times the user can interact with tab-to-search
193  // onboarding results before we stop showing them.
194  ["tabToSearch.onboard.interactionsLeft", 3],
195
196  // The number of times the user has been shown the onboarding search tip.
197  ["tipShownCount.searchTip_onboard", 0],
198
199  // The number of times the user has been shown the redirect search tip.
200  ["tipShownCount.searchTip_redirect", 0],
201
202  // Remove redundant portions from URLs.
203  ["trimURLs", true],
204
205  // If true, top sites may include sponsored ones.
206  ["sponsoredTopSites", false],
207
208  // Whether unit conversion is enabled.
209  ["unitConversion.enabled", false],
210
211  // The index where we show unit conversion results.
212  ["unitConversion.suggestedIndex", 1],
213
214  // Results will include a built-in set of popular domains when this is true.
215  ["usepreloadedtopurls.enabled", false],
216
217  // After this many days from the profile creation date, the built-in set of
218  // popular domains will no longer be included in the results.
219  ["usepreloadedtopurls.expire_days", 14],
220
221  // Controls the empty search behavior in Search Mode:
222  //  0 - Show nothing
223  //  1 - Show search history
224  //  2 - Show search and browsing history
225  ["update2.emptySearchBehavior", 0],
226]);
227const PREF_OTHER_DEFAULTS = new Map([
228  ["browser.fixup.dns_first_for_single_words", false],
229  ["browser.search.suggest.enabled", true],
230  ["browser.search.suggest.enabled.private", false],
231  ["keyword.enabled", true],
232  ["ui.popup.disable_autohide", false],
233]);
234
235// Maps preferences under browser.urlbar.suggest to behavior names, as defined
236// in mozIPlacesAutoComplete.
237const SUGGEST_PREF_TO_BEHAVIOR = {
238  history: "history",
239  bookmark: "bookmark",
240  openpage: "openpage",
241  searches: "search",
242};
243
244const PREF_TYPES = new Map([
245  ["boolean", "Bool"],
246  ["float", "Float"],
247  ["number", "Int"],
248  ["string", "Char"],
249]);
250
251/**
252 * Builds the standard result buckets and returns the root bucket.  Result
253 * buckets determine the composition of results in the muxer, i.e., how they're
254 * grouped and sorted.  Each bucket is an object that looks like this:
255 *
256 * {
257 *   {UrlbarUtils.RESULT_GROUP} [group]
258 *     This is defined only on buckets without children, and it determines the
259 *     result group that the bucket will contain.
260 *   {number} [maxResultCount]
261 *     An optional maximum number of results the bucket can contain.  If it's
262 *     not defined and the parent bucket does not define `flexChildren: true`,
263 *     then the max is the parent's max.  If the parent bucket defines
264 *     `flexChildren: true`, then `maxResultCount` is ignored.
265 *   {boolean} [flexChildren]
266 *     If true, then child buckets are "flexed", similar to flex in HTML.  Each
267 *     child bucket should define the `flex` property (or, if they don't, `flex`
268 *     is assumed to be zero).  `flex` is a number that defines the ratio of a
269 *     child's result count to the total result count of all children.  More
270 *     specifically, `flex: X` on a child means that the initial maximum result
271 *     count of the child is `parentMaxResultCount * (X / N)`, where `N` is the
272 *     sum of the `flex` values of all children.  If there are any child buckets
273 *     that cannot be completely filled, then the muxer will attempt to overfill
274 *     the children that were completely filled, while still respecting their
275 *     relative `flex` values.
276 *   {number} [flex]
277 *     The flex value of the bucket.  This should be defined only on buckets
278 *     where the parent defines `flexChildren: true`.  See `flexChildren` for a
279 *     discussion of flex.
280 *   {array} [children]
281 *     An array of child bucket objects.
282 * }
283 *
284 * @param {boolean} showSearchSuggestionsFirst
285 *   If true, the suggestions bucket will come before the general bucket.
286 * @returns {object}
287 *   The root bucket.
288 */
289function makeResultBuckets({ showSearchSuggestionsFirst }) {
290  let rootBucket = {
291    children: [
292      // heuristic
293      {
294        maxResultCount: 1,
295        children: [
296          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TEST },
297          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_EXTENSION },
298          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_SEARCH_TIP },
299          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_OMNIBOX },
300          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_ENGINE_ALIAS },
301          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_BOOKMARK_KEYWORD },
302          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_AUTOFILL },
303          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_PRELOADED },
304          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_TOKEN_ALIAS_ENGINE },
305          { group: UrlbarUtils.RESULT_GROUP.HEURISTIC_FALLBACK },
306        ],
307      },
308      // extensions using the omnibox API
309      {
310        group: UrlbarUtils.RESULT_GROUP.OMNIBOX,
311        availableSpan: UrlbarUtils.MAX_OMNIBOX_RESULT_COUNT - 1,
312      },
313    ],
314  };
315
316  // Prepare the parent bucket for suggestions and general.
317  let mainBucket = {
318    flexChildren: true,
319    children: [
320      // suggestions
321      {
322        children: [
323          {
324            flexChildren: true,
325            children: [
326              {
327                // If `maxHistoricalSearchSuggestions` == 0, the muxer forces
328                // `maxResultCount` to be zero and flex is ignored, per query.
329                flex: 2,
330                group: UrlbarUtils.RESULT_GROUP.FORM_HISTORY,
331              },
332              {
333                flex: 4,
334                group: UrlbarUtils.RESULT_GROUP.REMOTE_SUGGESTION,
335              },
336            ],
337          },
338          {
339            group: UrlbarUtils.RESULT_GROUP.TAIL_SUGGESTION,
340          },
341        ],
342      },
343      // general
344      {
345        children: [
346          {
347            availableSpan: 3,
348            group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
349          },
350          {
351            flexChildren: true,
352            children: [
353              {
354                flex: 1,
355                group: UrlbarUtils.RESULT_GROUP.REMOTE_TAB,
356              },
357              {
358                flex: 2,
359                group: UrlbarUtils.RESULT_GROUP.GENERAL,
360              },
361              {
362                // We show relatively many about-page results because they're
363                // only added for queries starting with "about:".
364                flex: 2,
365                group: UrlbarUtils.RESULT_GROUP.ABOUT_PAGES,
366              },
367              {
368                flex: 1,
369                group: UrlbarUtils.RESULT_GROUP.PRELOADED,
370              },
371            ],
372          },
373          {
374            group: UrlbarUtils.RESULT_GROUP.INPUT_HISTORY,
375          },
376        ],
377      },
378    ],
379  };
380  if (!showSearchSuggestionsFirst) {
381    mainBucket.children.reverse();
382  }
383  mainBucket.children[0].flex = 2;
384  mainBucket.children[1].flex = 1;
385  rootBucket.children.push(mainBucket);
386
387  return rootBucket;
388}
389
390/**
391 * Preferences class.  The exported object is a singleton instance.
392 */
393class Preferences {
394  /**
395   * Constructor
396   */
397  constructor() {
398    this._map = new Map();
399    this.QueryInterface = ChromeUtils.generateQI([
400      "nsIObserver",
401      "nsISupportsWeakReference",
402    ]);
403    Services.prefs.addObserver(PREF_URLBAR_BRANCH, this, true);
404    for (let pref of PREF_OTHER_DEFAULTS.keys()) {
405      Services.prefs.addObserver(pref, this, true);
406    }
407    this._observerWeakRefs = [];
408    this.addObserver(this);
409    NimbusFeatures.urlbar.onUpdate(() => this._onNimbusUpdate());
410  }
411
412  /**
413   * Returns the value for the preference with the given name.
414   * For preferences in the "browser.urlbar."" branch, the passed-in name
415   * should be relative to the branch. It's also possible to get prefs from the
416   * PREF_OTHER_DEFAULTS Map, specifying their full name.
417   *
418   * @param {string} pref
419   *        The name of the preference to get.
420   * @returns {*} The preference value.
421   */
422  get(pref) {
423    let value = this._map.get(pref);
424    if (value === undefined) {
425      value = this._getPrefValue(pref);
426      this._map.set(pref, value);
427    }
428    return value;
429  }
430
431  /**
432   * Sets the value for the preference with the given name.
433   * For preferences in the "browser.urlbar."" branch, the passed-in name
434   * should be relative to the branch. It's also possible to set prefs from the
435   * PREF_OTHER_DEFAULTS Map, specifying their full name.
436   *
437   * @param {string} pref
438   *        The name of the preference to set.
439   * @param {*} value The preference value.
440   */
441  set(pref, value) {
442    let { defaultValue, set } = this._getPrefDescriptor(pref);
443    if (typeof value != typeof defaultValue) {
444      throw new Error(`Invalid value type ${typeof value} for pref ${pref}`);
445    }
446    set(pref, value);
447  }
448
449  /**
450   * Clears the value for the preference with the given name.
451   *
452   * @param {string} pref
453   *        The name of the preference to clear.
454   */
455  clear(pref) {
456    let { clear } = this._getPrefDescriptor(pref);
457    clear(pref);
458  }
459
460  /**
461   * Builds the standard result buckets.  See makeResultBuckets.
462   *
463   * @param {object} options
464   *   See makeResultBuckets.
465   * @returns {object}
466   *   The root bucket.
467   */
468  makeResultBuckets(options) {
469    return makeResultBuckets(options);
470  }
471
472  /**
473   * Sets the value of the resultGroups pref to the current default buckets.
474   * This should be called from BrowserGlue._migrateUI when the default buckets
475   * are modified.
476   */
477  migrateResultBuckets() {
478    this.set(
479      "resultGroups",
480      JSON.stringify(
481        makeResultBuckets({
482          showSearchSuggestionsFirst: this.get("showSearchSuggestionsFirst"),
483        })
484      )
485    );
486  }
487
488  /**
489   * Adds a preference observer.  Observers are held weakly.
490   *
491   * @param {object} observer
492   *        An object that must have a method named `onPrefChanged`, which will
493   *        be called when a urlbar preference changes.  It will be passed the
494   *        pref name.  For prefs in the `browser.urlbar.` branch, the name will
495   *        be relative to the branch.  For other prefs, the name will be the
496   *        full name.
497   */
498  addObserver(observer) {
499    this._observerWeakRefs.push(Cu.getWeakReference(observer));
500  }
501
502  /**
503   * Observes preference changes.
504   *
505   * @param {nsISupports} subject
506   * @param {string} topic
507   * @param {string} data
508   */
509  observe(subject, topic, data) {
510    let pref = data.replace(PREF_URLBAR_BRANCH, "");
511    if (!PREF_URLBAR_DEFAULTS.has(pref) && !PREF_OTHER_DEFAULTS.has(pref)) {
512      return;
513    }
514    for (let i = 0; i < this._observerWeakRefs.length; ) {
515      let observer = this._observerWeakRefs[i].get();
516      if (!observer) {
517        // The observer has been GC'ed, so remove it from our list.
518        this._observerWeakRefs.splice(i, 1);
519      } else {
520        observer.onPrefChanged(pref);
521        ++i;
522      }
523    }
524  }
525
526  /**
527   * Called when a pref tracked by UrlbarPrefs changes.
528   *
529   * @param {string} pref
530   *        The name of the pref, relative to `browser.urlbar.` if the pref is
531   *        in that branch.
532   */
533  onPrefChanged(pref) {
534    this._map.delete(pref);
535
536    // Some prefs may influence others.
537    switch (pref) {
538      case "showSearchSuggestionsFirst":
539        this.set(
540          "resultGroups",
541          JSON.stringify(
542            makeResultBuckets({ showSearchSuggestionsFirst: this.get(pref) })
543          )
544        );
545        return;
546    }
547
548    if (pref.startsWith("suggest.")) {
549      this._map.delete("defaultBehavior");
550    }
551  }
552
553  /**
554   * Called when the `NimbusFeatures.urlbar` value changes.
555   */
556  _onNimbusUpdate() {
557    for (let key of Object.keys(this._nimbus)) {
558      this._map.delete(key);
559    }
560    this.__nimbus = null;
561  }
562
563  get _nimbus() {
564    if (!this.__nimbus) {
565      this.__nimbus = NimbusFeatures.urlbar.getAllVariables();
566    }
567    return this.__nimbus;
568  }
569
570  /**
571   * Returns the raw value of the given preference straight from Services.prefs.
572   *
573   * @param {string} pref
574   *        The name of the preference to get.
575   * @returns {*} The raw preference value.
576   */
577  _readPref(pref) {
578    let { defaultValue, get } = this._getPrefDescriptor(pref);
579    return get(pref, defaultValue);
580  }
581
582  /**
583   * Returns a validated and/or fixed-up value of the given preference.  The
584   * value may be validated for correctness, or it might be converted into a
585   * different value that is easier to work with than the actual value stored in
586   * the preferences branch.  Not all preferences require validation or fixup.
587   *
588   * The values returned from this method are the values that are made public by
589   * this module.
590   *
591   * @param {string} pref
592   *        The name of the preference to get.
593   * @returns {*} The validated and/or fixed-up preference value.
594   */
595  _getPrefValue(pref) {
596    switch (pref) {
597      case "defaultBehavior": {
598        let val = 0;
599        for (let type of Object.keys(SUGGEST_PREF_TO_BEHAVIOR)) {
600          let behavior = `BEHAVIOR_${SUGGEST_PREF_TO_BEHAVIOR[
601            type
602          ].toUpperCase()}`;
603          val |=
604            this.get("suggest." + type) && Ci.mozIPlacesAutoComplete[behavior];
605        }
606        return val;
607      }
608      case "resultGroups":
609        try {
610          return JSON.parse(this._readPref(pref));
611        } catch (ex) {}
612        return makeResultBuckets({
613          showSearchSuggestionsFirst: this.get("showSearchSuggestionsFirst"),
614        });
615    }
616    return this._readPref(pref);
617  }
618
619  /**
620   * Returns a descriptor of the given preference.
621   * @param {string} pref The preference to examine.
622   * @returns {object} An object describing the pref with the following shape:
623   *          { defaultValue, get, set, clear }
624   */
625  _getPrefDescriptor(pref) {
626    let branch = Services.prefs.getBranch(PREF_URLBAR_BRANCH);
627    let defaultValue = PREF_URLBAR_DEFAULTS.get(pref);
628    if (defaultValue === undefined) {
629      branch = Services.prefs;
630      defaultValue = PREF_OTHER_DEFAULTS.get(pref);
631      if (defaultValue === undefined) {
632        let nimbus = this._getNimbusDescriptor(pref);
633        if (nimbus) {
634          return nimbus;
635        }
636        throw new Error("Trying to access an unknown pref " + pref);
637      }
638    }
639
640    let type;
641    if (!Array.isArray(defaultValue)) {
642      type = PREF_TYPES.get(typeof defaultValue);
643    } else {
644      if (defaultValue.length != 2) {
645        throw new Error("Malformed pref def: " + pref);
646      }
647      [defaultValue, type] = defaultValue;
648      type = PREF_TYPES.get(type);
649    }
650    if (!type) {
651      throw new Error("Unknown pref type: " + pref);
652    }
653    return {
654      defaultValue,
655      get: branch[`get${type}Pref`],
656      // Float prefs are stored as Char.
657      set: branch[`set${type == "Float" ? "Char" : type}Pref`],
658      clear: branch.clearUserPref,
659    };
660  }
661
662  /**
663   * Returns a descriptor for the given Nimbus property, if it exists.
664   *
665   * @param {string} name
666   *   The name of the desired property in the object returned from
667   *   NimbusFeatures.urlbar.getAllVariables().
668   * @returns {object}
669   *   An object describing the property's value with the following shape (same
670   *   as _getPrefDescriptor()):
671   *     { defaultValue, get, set, clear }
672   *   If the property doesn't exist, null is returned.
673   */
674  _getNimbusDescriptor(name) {
675    if (!this._nimbus.hasOwnProperty(name)) {
676      return null;
677    }
678    return {
679      defaultValue: this._nimbus[name],
680      get: () => this._nimbus[name],
681      set() {
682        throw new Error(`'${name}' is a Nimbus value and cannot be set`);
683      },
684      clear() {
685        throw new Error(`'${name}' is a Nimbus value and cannot be cleared`);
686      },
687    };
688  }
689
690  /**
691   * Initializes the showSearchSuggestionsFirst pref based on the matchBuckets
692   * pref.  This function can be removed when the corresponding UI migration in
693   * BrowserGlue.jsm is no longer needed.
694   */
695  initializeShowSearchSuggestionsFirstPref() {
696    let matchBuckets = [];
697    let pref = Services.prefs.getCharPref("browser.urlbar.matchBuckets", "");
698    try {
699      matchBuckets = pref.split(",").map(v => {
700        let bucket = v.split(":");
701        return [bucket[0].trim().toLowerCase(), Number(bucket[1])];
702      });
703    } catch (ex) {}
704    let bucketNames = matchBuckets.map(bucket => bucket[0]);
705    let suggestionIndex = bucketNames.indexOf("suggestion");
706    let generalIndex = bucketNames.indexOf("general");
707    let showSearchSuggestionsFirst =
708      generalIndex < 0 ||
709      (suggestionIndex >= 0 && suggestionIndex < generalIndex);
710    let oldValue = Services.prefs.getBoolPref(
711      "browser.urlbar.showSearchSuggestionsFirst"
712    );
713    Services.prefs.setBoolPref(
714      "browser.urlbar.showSearchSuggestionsFirst",
715      showSearchSuggestionsFirst
716    );
717
718    // Pref observers aren't called when a pref is set to its current value, but
719    // we always want to set matchBuckets to the appropriate default value via
720    // onPrefChanged, so call it now if necessary.  This is really only
721    // necessary for tests since the only time this function is called outside
722    // of tests is by a UI migration in BrowserGlue.
723    if (oldValue == showSearchSuggestionsFirst) {
724      this.onPrefChanged("showSearchSuggestionsFirst");
725    }
726  }
727}
728
729var UrlbarPrefs = new Preferences();
730