1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This module exports a provider that offers a search engine when the user is
7 * typing a search engine domain.
13 } from "resource:///modules/UrlbarUtils.sys.mjs";
17 ChromeUtils.defineESModuleGetters(lazy, {
18 ActionsProviderContextualSearch:
19 "resource:///modules/ActionsProviderContextualSearch.sys.mjs",
20 UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
21 UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
22 UrlbarProviderAutofill: "resource:///modules/UrlbarProviderAutofill.sys.mjs",
23 UrlbarProviderGlobalActions:
24 "resource:///modules/UrlbarProviderGlobalActions.sys.mjs",
25 UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
26 UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
27 UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
30 const DYNAMIC_RESULT_TYPE = "onboardTabToSearch";
31 const VIEW_TEMPLATE = {
39 classList: ["urlbarView-no-wrap"],
44 classList: ["urlbarView-favicon"],
47 name: "text-container",
51 name: "first-row-container",
57 classList: ["urlbarView-title"],
66 name: "title-separator",
68 classList: ["urlbarView-title-separator"],
73 classList: ["urlbarView-action"],
92 * Initializes this provider's dynamic result. To be called after the creation
93 * of the provider singleton.
95 function initializeDynamicResult() {
96 lazy.UrlbarResult.addDynamicResultType(DYNAMIC_RESULT_TYPE);
97 lazy.UrlbarView.addDynamicViewTemplate(DYNAMIC_RESULT_TYPE, VIEW_TEMPLATE);
101 * Class used to create the provider.
103 class ProviderTabToSearch extends UrlbarProvider {
109 * Returns the name of this provider.
111 * @returns {string} the name of this provider.
114 return "TabToSearch";
118 * Returns the type of this provider.
120 * @returns {integer} one of the types from UrlbarUtils.PROVIDER_TYPE.*
123 return UrlbarUtils.PROVIDER_TYPE.PROFILE;
127 * Whether this provider should be invoked for the given context.
128 * If this method returns false, the providers manager won't start a query
129 * with this provider, to save on resources.
131 * @param {UrlbarQueryContext} queryContext The query context object
132 * @returns {boolean} Whether this provider should be invoked for the search.
134 async isActive(queryContext) {
136 queryContext.searchString &&
137 queryContext.tokens.length == 1 &&
138 !queryContext.searchMode &&
139 lazy.UrlbarPrefs.get("suggest.engines") &&
141 lazy.UrlbarProviderGlobalActions.isActive(queryContext) &&
142 lazy.ActionsProviderContextualSearch.isActive(queryContext)
148 * Gets the provider's priority.
150 * @returns {number} The provider's priority for the given query.
157 * This is called only for dynamic result types, when the urlbar view updates
158 * the view of one of the results of the provider. It should return an object
159 * describing the view update.
161 * @param {UrlbarResult} result The result whose view will be updated.
162 * @returns {object} An object describing the view update.
164 getViewUpdate(result) {
168 src: result.payload.icon,
173 id: "urlbar-result-action-search-w-engine",
175 engine: result.payload.engine,
181 id: result.payload.isGeneralPurposeEngine
182 ? "urlbar-result-action-tabtosearch-web"
183 : "urlbar-result-action-tabtosearch-other-engine",
185 engine: result.payload.engine,
191 id: "urlbar-tabtosearch-onboard",
198 * Called when a result from the provider is selected. "Selected" refers to
199 * the user highlighing the result with the arrow keys/Tab, before it is
200 * picked. onSelection is also called when a user clicks a result. In the
201 * event of a click, onSelection is called just before onEngagement.
203 * @param {UrlbarResult} result
204 * The result that was selected.
206 onSelection(result) {
207 // We keep track of the number of times the user interacts with
208 // tab-to-search onboarding results so we stop showing them after
209 // `tabToSearch.onboard.interactionsLeft` interactions.
210 // Also do not increment the counter if the result was interacted with less
211 // than 5 minutes ago. This is a guard against the user running up the
212 // counter by interacting with the same result repeatedly.
214 result.payload.dynamicType &&
215 (!this.onboardingInteractionAtTime ||
216 this.onboardingInteractionAtTime < Date.now() - 1000 * 60 * 5)
218 let interactionsLeft = lazy.UrlbarPrefs.get(
219 "tabToSearch.onboard.interactionsLeft"
222 if (interactionsLeft > 0) {
223 lazy.UrlbarPrefs.set(
224 "tabToSearch.onboard.interactionsLeft",
229 this.onboardingInteractionAtTime = Date.now();
233 onEngagement(queryContext, controller, details) {
234 let { result, element } = details;
235 if (result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) {
236 // Confirm search mode, but only for the onboarding (dynamic) result. The
237 // input will handle confirming search mode for the non-onboarding
238 // `RESULT_TYPE.SEARCH` result since it sets `providesSearchMode`.
239 element.ownerGlobal.gURLBar.maybeConfirmSearchModeFromResult({
246 onImpression(state, queryContext, controller, providerVisibleResults) {
248 let regularResultCount = 0;
249 let onboardingResultCount = 0;
250 providerVisibleResults.forEach(({ result }) => {
251 if (result.type === UrlbarUtils.RESULT_TYPE.DYNAMIC) {
252 let scalarKey = lazy.UrlbarSearchUtils.getSearchModeScalarKey({
253 engineName: result?.payload.engine,
255 Glean.urlbarTabtosearch.impressionsOnboarding[scalarKey].add(1);
256 onboardingResultCount += 1;
257 } else if (result.type === UrlbarUtils.RESULT_TYPE.SEARCH) {
258 let scalarKey = lazy.UrlbarSearchUtils.getSearchModeScalarKey({
259 engineName: result?.payload.engine,
261 Glean.urlbarTabtosearch.impressions[scalarKey].add(1);
262 regularResultCount += 1;
265 Glean.urlbar.tips["tabtosearch-shown"].add(regularResultCount);
266 Glean.urlbar.tips["tabtosearch_onboard-shown"].add(onboardingResultCount);
268 // If your test throws this error or causes another test to throw it, it
269 // is likely because your test showed a tab-to-search result but did not
270 // start and end the engagement in which it was shown. Be sure to fire an
271 // input event to start an engagement and blur the Urlbar to end it.
273 `Exception while recording TabToSearch telemetry: ${ex})`
279 * Defines whether the view should defer user selection events while waiting
280 * for the first result from this provider.
282 * @returns {boolean} Whether the provider wants to defer user selection
285 get deferUserSelection() {
292 * @param {object} queryContext The query context object
293 * @param {Function} addCallback Callback invoked by the provider to add a new
295 * @returns {Promise} resolved when the query stops.
297 async startQuery(queryContext, addCallback) {
298 // enginesForDomainPrefix only matches against engine domains.
299 // Remove trailing slashes and www. from the search string and check if the
300 // resulting string is worth matching.
301 let [searchStr] = UrlbarUtils.stripPrefixAndTrim(
302 queryContext.searchString,
308 // Skip any string that cannot be an origin.
310 !lazy.UrlbarTokenizer.looksLikeOrigin(searchStr, {
311 ignoreKnownDomains: true,
318 // Also remove the public suffix, if present, to allow for partial matches.
319 if (searchStr.includes(".")) {
320 searchStr = UrlbarUtils.stripPublicSuffixFromHost(searchStr);
323 // Add all matching engines.
324 let engines = await lazy.UrlbarSearchUtils.enginesForDomainPrefix(
327 matchAllDomainLevels: true,
330 if (!engines.length) {
334 const onboardingInteractionsLeft = lazy.UrlbarPrefs.get(
335 "tabToSearch.onboard.interactionsLeft"
338 // If the engine host begins with the search string, autofill may happen
339 // for it, and the Muxer will retain the result only if there's a matching
340 // autofill heuristic result.
341 // Otherwise, we may have a partial match, where the search string is at
342 // the boundary of a host part, for example "wiki" in "en.wikipedia.org".
343 // We put those engines apart, and later we check if their host satisfies
344 // the autofill threshold. If they do, we mark them with the
345 // "satisfiesAutofillThreshold" payload property, so the muxer can avoid
346 // filtering them out.
347 let partialMatchEnginesByHost = new Map();
349 for (let engine of engines) {
350 // Trim the engine host. This will also be set as the result url, so the
351 // Muxer can use it to filter.
352 let [host] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, {
355 // Check if the host may be autofilled.
356 if (host.startsWith(searchStr.toLocaleLowerCase())) {
357 if (onboardingInteractionsLeft > 0) {
358 addCallback(this, makeOnboardingResult(engine));
360 addCallback(this, makeResult(queryContext, engine));
365 // Otherwise it may be a partial match that would not be autofilled.
366 if (host.includes("." + searchStr.toLocaleLowerCase())) {
367 partialMatchEnginesByHost.set(engine.searchUrlDomain, engine);
368 // Don't continue here, we are looking for more partial matches.
370 // We also try to match the base domain of the searchUrlDomain,
371 // because otherwise for an engine like rakuten, we'd check pt.afl.rakuten.co.jp
372 // which redirects and is thus not saved in the history resulting in a low score.
374 let baseDomain = Services.eTLD.getBaseDomainFromHost(
375 engine.searchUrlDomain
377 if (baseDomain.startsWith(searchStr)) {
378 partialMatchEnginesByHost.set(baseDomain, engine);
381 if (partialMatchEnginesByHost.size) {
382 let host = await lazy.UrlbarProviderAutofill.getTopHostOverThreshold(
384 Array.from(partialMatchEnginesByHost.keys())
387 let engine = partialMatchEnginesByHost.get(host);
388 if (onboardingInteractionsLeft > 0) {
389 addCallback(this, makeOnboardingResult(engine, true));
391 addCallback(this, makeResult(queryContext, engine, true));
398 function makeOnboardingResult(engine, satisfiesAutofillThreshold = false) {
399 let result = new lazy.UrlbarResult(
400 UrlbarUtils.RESULT_TYPE.DYNAMIC,
401 UrlbarUtils.RESULT_SOURCE.SEARCH,
404 searchUrlDomainWithoutSuffix: searchUrlDomainWithoutSuffix(engine),
405 providesSearchMode: true,
406 icon: UrlbarUtils.ICON.SEARCH_GLASS,
407 dynamicType: DYNAMIC_RESULT_TYPE,
408 satisfiesAutofillThreshold,
411 result.resultSpan = 2;
412 result.suggestedIndex = 1;
416 function makeResult(context, engine, satisfiesAutofillThreshold = false) {
417 let result = new lazy.UrlbarResult(
418 UrlbarUtils.RESULT_TYPE.SEARCH,
419 UrlbarUtils.RESULT_SOURCE.SEARCH,
420 ...lazy.UrlbarResult.payloadAndSimpleHighlights(context.tokens, {
422 isGeneralPurposeEngine: engine.isGeneralPurposeEngine,
423 searchUrlDomainWithoutSuffix: searchUrlDomainWithoutSuffix(engine),
424 providesSearchMode: true,
425 icon: UrlbarUtils.ICON.SEARCH_GLASS,
427 satisfiesAutofillThreshold,
430 result.suggestedIndex = 1;
434 function searchUrlDomainWithoutSuffix(engine) {
435 let [value] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, {
438 return value.substr(0, value.length - engine.searchUrlPublicSuffix.length);
441 export var UrlbarProviderTabToSearch = new ProviderTabToSearch();
442 initializeDynamicResult();