diff options
Diffstat (limited to 'toolkit/components/search/SearchSuggestions.sys.mjs')
-rw-r--r-- | toolkit/components/search/SearchSuggestions.sys.mjs | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/toolkit/components/search/SearchSuggestions.sys.mjs b/toolkit/components/search/SearchSuggestions.sys.mjs new file mode 100644 index 0000000000..16ba9668f0 --- /dev/null +++ b/toolkit/components/search/SearchSuggestions.sys.mjs @@ -0,0 +1,221 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { FormAutoCompleteResult } from "resource://gre/modules/nsFormAutoCompleteResult.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + FormAutoCompleteResult: "resource://gre/modules/FormAutoComplete.sys.mjs", + FormHistoryClient: "resource://gre/modules/FormAutoComplete.sys.mjs", + + SearchSuggestionController: + "resource://gre/modules/SearchSuggestionController.sys.mjs", +}); + +/** + * SuggestAutoComplete is a base class that implements nsIAutoCompleteSearch + * and can collect results for a given search by using this.#suggestionController. + * We do it this way since the AutoCompleteController in Mozilla requires a + * unique XPCOM Service for every search provider, even if the logic for two + * providers is identical. + * + * @class + */ +class SuggestAutoComplete { + constructor() { + this.#suggestionController = new lazy.SearchSuggestionController(); + this.#suggestionController.maxLocalResults = this.#historyLimit; + } + + /** + * Notifies the front end of new results. + * + * @param {string} searchString + * The user's query string. + * @param {Array} results + * An array of results to the search. + * @param {object} formHistoryResult + * Any previous form history result. + * @private + */ + onResultsReady(searchString, results, formHistoryResult) { + if (this.#listener) { + let result = new FormAutoCompleteResult( + searchString, + Ci.nsIAutoCompleteResult.RESULT_SUCCESS, + 0, + "", + results.map(result => ({ + value: result, + label: result, + // We supply the comments field so that autocomplete does not kick + // in the unescaping of the results for display which it uses for + // urls. + comment: result, + removable: true, + })), + formHistoryResult + ); + + this.#listener.onSearchResult(this, result); + + // Null out listener to make sure we don't notify it twice + this.#listener = null; + } + } + + /** + * Initiates the search result gathering process. Part of + * nsIAutoCompleteSearch implementation. + * + * @param {string} searchString + * The user's query string. + * @param {string} searchParam + * unused, "an extra parameter"; even though this parameter and the + * next are unused, pass them through in case the form history + * service wants them + * @param {object} previousResult + * unused, a client-cached store of the previous generated resultset + * for faster searching. + * @param {object} listener + * object implementing nsIAutoCompleteObserver which we notify when + * results are ready. + */ + startSearch(searchString, searchParam, previousResult, listener) { + var formHistorySearchParam = searchParam.split("|")[0]; + + // Receive the information about the privacy mode of the window to which + // this search box belongs. The front-end's search.xml bindings passes this + // information in the searchParam parameter. The alternative would have + // been to modify nsIAutoCompleteSearch to add an argument to startSearch + // and patch all of autocomplete to be aware of this, but the searchParam + // argument is already an opaque argument, so this solution is hopefully + // less hackish (although still gross.) + var privacyMode = searchParam.split("|")[1] == "private"; + + // Start search immediately if possible, otherwise once the search + // service is initialized + if (Services.search.isInitialized) { + this.#triggerSearch( + searchString, + formHistorySearchParam, + listener, + privacyMode + ).catch(console.error); + return; + } + + Services.search + .init() + .then(() => { + this.#triggerSearch( + searchString, + formHistorySearchParam, + listener, + privacyMode + ).catch(console.error); + }) + .catch(result => + console.error( + "Could not initialize search service, bailing out: " + result + ) + ); + } + + /** + * Ends the search result gathering process. Part of nsIAutoCompleteSearch + * implementation. + */ + stopSearch() { + this.#suggestionController.stop(); + } + + #suggestionController; + + /** + * Maximum number of history items displayed. This is capped at 7 + * because the primary consumer (Firefox search bar) displays 10 rows + * by default, and so we want to leave some space for suggestions + * to be visible. + * + * @type {number} + */ + #historyLimit = 7; + + /** + * The object implementing nsIAutoCompleteObserver that we notify when + * we have found results. + * + * @type {object|null} + */ + #listener = null; + + /** + * Actual implementation of search. + * + * @param {string} searchString + * The user's query string. + * @param {string} searchParam + * unused + * @param {object} listener + * object implementing nsIAutoCompleteObserver which we notify when + * results are ready. + * @param {boolean} privacyMode + * True if the search was made from a private browsing mode context. + */ + async #triggerSearch(searchString, searchParam, listener, privacyMode) { + this.#listener = listener; + let results = await this.#suggestionController.fetch( + searchString, + privacyMode, + Services.search.defaultEngine + ); + + // If form history has results, add them to the list. + let finalResults = results.local.map(r => r.value); + + // If there are remote matches, add them. + if (results.remote.length) { + // now put the history results above the suggestions + // We shouldn't show tail suggestions in their full-text form. + let nonTailEntries = results.remote.filter( + e => !e.matchPrefix && !e.tail + ); + finalResults = finalResults.concat(nonTailEntries.map(e => e.value)); + } + + // Bug 1822297: This re-uses the wrappers from Satchel, to avoid re-writing + // our own nsIAutoCompleteSimpleResult implementation for now. However, + // we should do that at some stage to remove the dependency on satchel. + let client = new lazy.FormHistoryClient({ + formField: null, + inputName: this.#suggestionController.formHistoryParam, + }); + let formHistoryResult = new lazy.FormAutoCompleteResult( + client, + results.formHistoryResults, + this.#suggestionController.formHistoryParam, + searchString + ); + + // Notify the FE of our new results + this.onResultsReady(results.term, finalResults, formHistoryResult); + } + + QueryInterface = ChromeUtils.generateQI([ + "nsIAutoCompleteSearch", + "nsIAutoCompleteObserver", + ]); +} + +/** + * SearchSuggestAutoComplete is a service implementation that handles suggest + * results specific to web searches. + * + * @class + */ +export class SearchSuggestAutoComplete extends SuggestAutoComplete { + classID = Components.ID("{aa892eb4-ffbf-477d-9f9a-06c995ae9f27}"); + serviceURL = ""; +} |