diff --git a/toolkit/actors/AutoCompleteChild.sys.mjs b/toolkit/actors/AutoCompleteChild.sys.mjs index 3694e497d5c7..d2a6cb23ad46 100644 --- a/toolkit/actors/AutoCompleteChild.sys.mjs +++ b/toolkit/actors/AutoCompleteChild.sys.mjs @@ -13,6 +13,10 @@ ChromeUtils.defineESModuleGetters(lazy, { LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", }); +const gFormFillController = Cc[ + "@mozilla.org/satchel/form-fill-controller;1" +].getService(Ci.nsIFormFillController); + let autoCompleteListeners = new Set(); export class AutoCompleteChild extends JSWindowActorChild { @@ -190,6 +194,129 @@ export class AutoCompleteChild extends JSWindowActorChild { return results; } + + // Store the input to interested autocomplete providers mapping + #providersByInput = new WeakMap(); + + providersByInput(input) { + // This functions returns the interested providers that have called + // `markAsAutoCompletableField` for the given input and also the hard-coded + // autocomplete providers based on input type. + const providers = new Set(this.#providersByInput.get(input)); + + // The current design is that FormHisotry doesn't call `markAsAutoCompletable` + // for every eligilbe input. Instead, when FormFillController receives a focus event, + // it would control the if the is eligible to show form history. + // Because of the design, we need to ask FormHistory whether to search for autocomplete entries + // for every startSearch call + // TODO: uncomment after integrating FormHisotry with AutoCompleteChild + // providers.add(input.ownerGlobal.windowGlobalChild.getActor("FormHistory")); + return providers; + } + + /** + * This API should be used by an autocomplete entry provider to mark an input field + * as eligible for autocomplete for its type. + * When users click on an autocompletable input, we will search autocomplete entries + * from all the providers that have called this API for the given . + * + * An autocomplete provider should be a JSWindowActor and implements the following + * functions: + * - string actorName() + * - bool shouldSearchForAutoComplete(element); + * - jsval getAutoCompleteSearchOption(element); + * - jsval searchResultToAutoCompleteResult(searchString, element, record); + * See `FormAutofillChild` for example + * + * @param input - The HTML element that is considered autocompletable by the + * given provider + * @param provider - A module that provides autocomplete entries for a , for example, + * FormAutofill provides address or credit card autocomplete entries, + * LoginManager provides logins entreis. + */ + markAsAutoCompletableField(input, provider) { + gFormFillController.markAsAutoCompletableField(input); + + let providers = this.#providersByInput.get(input); + if (!providers) { + providers = new Set(); + this.#providersByInput.set(input, providers); + } + providers.add(provider); + } + + // Record the current ongoing search request. This is used by stopSearch + // to prevent notifying the autocomplete controller after receiving search request + // results that were issued prior to the call to stop the search. + #ongoingSearches = new Set(); + + async startSearch(searchString, input, listener) { + // TODO: This should be removed once we implement triggering autocomplete + // from the parent. + this.lastProfileAutoCompleteFocusedInput = input; + + // For all the autocomplete entry providers that previsouly marked + // this as autocompletable, ask the provider whether we should + // search for autocomplete entries in the parent. This is because the current + // design doesn't rely on the provider constantly monitor the and + // then mark/unmark an input. The provider generally calls the + // `markAsAutoCompletbleField` when it sees an is eliglbe for autocomplete. + // Here we ask the provider to exam the more detailedly to see + // whether we need to search for autocomplete entries at the time users + // click on the + const providers = this.providersByInput(input); + const data = Array.from(providers) + .filter(p => p.shouldSearchForAutoComplete(input, searchString)) + .map(p => ({ + actorName: p.actorName, + options: p.getAutoCompleteSearchOption(input, searchString), + })); + + let result = []; + + // We don't return empty result when no provider requests seaching entries in the + // parent because for some special cases, the autocomplete entries are coming + // from the content. For example, . + if (data.length) { + const promise = this.sendQuery("AutoComplete:StartSearch", { + searchString, + data, + }); + this.#ongoingSearches.add(promise); + result = await promise; + + // If the search is stopped, don't report back. + if (!this.#ongoingSearches.delete(promise)) { + return; + } + } + + for (const provider of providers) { + // Search result could be empty. However, an autocomplete provider might + // want to show an autoclmplete popup when there is no search result. For example, + // for FormHisotry, insecure warning for LoginManager. + const searchResult = result.find(r => r.actorName == provider.actorName); + const acResult = provider.searchResultToAutoCompleteResult( + searchString, + input, + searchResult + ); + + // We have not yet supported showing autocomplete entries from multiple providers, + // Note: The prioty is defined in AutoCompleteParent. + if (acResult) { + this.lastProfileAutoCompleteResult = acResult; + listener.onSearchCompletion(acResult); + return; + } + } + this.lastProfileAutoCompleteResult = null; + } + + stopSearch() { + this.lastProfileAutoCompleteResult = null; + this.#ongoingSearches.clear(); + } } AutoCompleteChild.prototype.QueryInterface = ChromeUtils.generateQI([ diff --git a/toolkit/actors/AutoCompleteParent.sys.mjs b/toolkit/actors/AutoCompleteParent.sys.mjs index 70cbcea44de2..8cb51c8e5892 100644 --- a/toolkit/actors/AutoCompleteParent.sys.mjs +++ b/toolkit/actors/AutoCompleteParent.sys.mjs @@ -376,7 +376,7 @@ export class AutoCompleteParent extends JSWindowActorParent { } } - receiveMessage(message) { + async receiveMessage(message) { let browser = this.browsingContext.top.embedderElement; if ( @@ -433,6 +433,12 @@ export class AutoCompleteParent extends JSWindowActorParent { this.closePopup(); break; } + + case "AutoComplete:StartSearch": { + const { searchString, data } = message.data; + const result = await this.#startSearch(searchString, data); + return result; + } } // Returning false to pacify ESLint, but this return value is // ignored by the messaging infrastructure. @@ -493,6 +499,45 @@ export class AutoCompleteParent extends JSWindowActorParent { } } + // This defines the supported autocomplete providers and the prioity to show the autocomplete + // entry. + #AUTOCOMPLETE_PROVIDERS = ["FormAutofill", "LoginManager", "FormHistory"]; + + /** + * Search across multiple module to gather autocomplete entries for a given search string. + * + * @param {string} searchString + * The input string used to query autocomplete entries across different + * autocomplete providers. + * @param {Array} providers + * An array of objects where each object has a `name` used to identify the actor + * name of the provider and `options` that are passed to the `searchAutoCompleteEntries` + * method of the actor. + * @returns {Array} An array of results objects with `name` of the provider and `entries` + * that are returned from the provider module's `searchAutoCompleteEntries` method. + */ + async #startSearch(searchString, providers) { + for (const name of this.#AUTOCOMPLETE_PROVIDERS) { + const provider = providers.find(p => p.actorName == name); + if (!provider) { + continue; + } + const { actorName, options } = provider; + const actor = + this.browsingContext.currentWindowGlobal.getActor(actorName); + const entries = await actor?.searchAutoCompleteEntries( + searchString, + options + ); + + // We have not yet supported showing autocomplete entries from multiple providers, + if (entries) { + return [{ actorName, ...entries }]; + } + } + return []; + } + stopSearch() {} /** diff --git a/toolkit/components/autocomplete/nsIAutoCompletePopup.idl b/toolkit/components/autocomplete/nsIAutoCompletePopup.idl index 607863c9a7cd..0c8e3c99d655 100644 --- a/toolkit/components/autocomplete/nsIAutoCompletePopup.idl +++ b/toolkit/components/autocomplete/nsIAutoCompletePopup.idl @@ -5,6 +5,7 @@ #include "nsISupports.idl" interface nsIAutoCompleteInput; +interface nsIFormFillCompleteObserver; webidl Element; @@ -69,4 +70,21 @@ interface nsIAutoCompletePopup : nsISupports * @return The currently selected result item index */ void selectBy(in boolean reverse, in boolean page); + + /* + * Search for a given string and notify a listener (either synchronously + * or asynchronously) of the result + * + * @param searchString - The string to search for + * @param searchParam - An extra parameter + * @param previousResult - A previous result to use for faster searching + * @param listener - A listener to notify when the search is complete + */ + void startSearch(in AString searchString, in Element element, in nsIFormFillCompleteObserver listener); + + /* + * Stop the search that is in progress + */ + void stopSearch(); + }; diff --git a/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs b/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs index 7594fc8fcf5f..56c5c29825b5 100644 --- a/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs +++ b/toolkit/components/formautofill/AutofillProfileAutoComplete.sys.mjs @@ -296,8 +296,6 @@ AutofillProfileAutoCompleteSearch.prototype = { export const ProfileAutocomplete = { QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), - lastProfileAutoCompleteResult: null, - lastProfileAutoCompleteFocusedInput: null, _registered: false, _factory: null, @@ -329,7 +327,6 @@ export const ProfileAutocomplete = { this._factory.unregister(); this._factory = null; this._registered = false; - this._lastAutoCompleteResult = null; Services.obs.removeObserver(this, "autocomplete-will-enter-text"); }, @@ -362,6 +359,17 @@ export const ProfileAutocomplete = { return actor.selectedIndex; }, + // TODO: This will be removed after we implement triggering autofill from the parent + get lastProfileAutoCompleteResult() { + return lazy.FormAutofillContent.activeAutofillChild + .lastProfileAutoCompleteResult; + }, + + get lastProfileAutoCompleteFocusedInput() { + return lazy.FormAutofillContent.activeAutofillChild + .lastProfileAutoCompleteFocusedInput; + }, + async _fillFromAutocompleteRow(focusedInput) { this.debug("_fillFromAutocompleteRow:", focusedInput); let formDetails = lazy.FormAutofillContent.activeFormDetails; diff --git a/toolkit/components/formautofill/FormAutofillChild.sys.mjs b/toolkit/components/formautofill/FormAutofillChild.sys.mjs index af8445943291..6eef2047146b 100644 --- a/toolkit/components/formautofill/FormAutofillChild.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillChild.sys.mjs @@ -8,11 +8,16 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs", AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs", AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs", + CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs", + GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs", + InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs", FormAutofill: "resource://autofill/FormAutofill.sys.mjs", FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", + FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs", FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", ProfileAutocomplete: @@ -656,16 +661,21 @@ export class FormAutofillChild extends JSWindowActorChild { } } - previewProfile(selectedIndex) { - let lastAutoCompleteResult = - lazy.ProfileAutocomplete.lastProfileAutoCompleteResult; - let focusedInput = this.activeInput; + get lastProfileAutoCompleteResult() { + return this.manager.getActor("AutoComplete")?.lastProfileAutoCompleteResult; + } + get lastProfileAutoCompleteFocusedInput() { + return this.manager.getActor("AutoComplete") + ?.lastProfileAutoCompleteFocusedInput; + } + + previewProfile(selectedIndex) { if ( selectedIndex === -1 || - !focusedInput || - !lastAutoCompleteResult || - lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill" + !this.activeInput || + this.lastProfileAutoCompleteResult?.getStyleAt(selectedIndex) != + "autofill" ) { lazy.ProfileAutocomplete._clearProfilePreview(); } else { @@ -701,6 +711,149 @@ export class FormAutofillChild extends JSWindowActorChild { return; } - formFillController.markAsAutofillField(field); + this.manager + .getActor("AutoComplete") + ?.markAsAutoCompletableField(field, this); + } + + get actorName() { + return "FormAutofill"; + } + + /** + * Get the search options when searching for autocomplete entries in the parent + * + * @param {HTMLInputElement} input - The input element to search for autocompelte entries + * @returns {object} the search options for the input + */ + getAutoCompleteSearchOption(input) { + const fieldDetail = this._fieldDetailsManager + ._getFormHandler(input) + ?.getFieldDetailByElement(input); + + const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm + ? "SignUpFormScenario" + : ""; + return { fieldName: fieldDetail?.fieldName, scenarioName }; + } + + /** + * Ask the provider whether it might have autocomplete entry to show + * for the given input. + * + * @param {HTMLInputElement} input - The input element to search for autocompelte entries + * @returns {boolean} true if we shold search for autocomplete entries + */ + shouldSearchForAutoComplete(input) { + const fieldDetail = this._fieldDetailsManager + ._getFormHandler(input) + ?.getFieldDetailByElement(input); + if (!fieldDetail) { + return false; + } + const fieldName = fieldDetail.fieldName; + const isAddressField = lazy.FormAutofillUtils.isAddressField(fieldName); + const searchPermitted = isAddressField + ? lazy.FormAutofill.isAutofillAddressesEnabled + : lazy.FormAutofill.isAutofillCreditCardsEnabled; + // If the specified autofill feature is pref off, do not search + if (!searchPermitted) { + return false; + } + + // No profile can fill the currently-focused input. + if (!lazy.FormAutofillContent.savedFieldNames.has(fieldName)) { + return false; + } + + // The current form has already been populated and the field is not + // an empty credit card field. + const isCreditCardField = + lazy.FormAutofillUtils.isCreditCardField(fieldName); + const isInputAutofilled = + this.activeHandler.getFilledStateByElement(input) == + lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED; + const filledRecordGUID = this.activeSection.filledRecordGUID; + if ( + !isInputAutofilled && + filledRecordGUID && + !(isCreditCardField && this.activeInput.value === "") + ) { + return false; + } + + // (address only) less than 3 inputs are covered by all saved fields in the storage. + if ( + isAddressField && + this.activeSection.allFieldNames.filter(field => + lazy.FormAutofillContent.savedFieldNames.has(field) + ).length < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD + ) { + return false; + } + + return true; + } + + /** + * Convert the search result to autocomplete results + * + * @param {string} searchString - The string to search for + * @param {HTMLInputElement} input - The input element to search for autocompelte entries + * @param {Array} records - autocomplete records + * @returns {AutocompleteResult} + */ + searchResultToAutoCompleteResult(searchString, input, records) { + if (!records) { + return null; + } + + const entries = records.records; + const externalEntries = records.externalEntries; + + const fieldDetail = this._fieldDetailsManager + ._getFormHandler(input) + ?.getFieldDetailByElement(input); + if (!fieldDetail) { + return null; + } + + const adaptedRecords = this.activeSection.getAdaptedProfiles(entries); + const isSecure = lazy.InsecurePasswordUtils.isFormSecure( + this.activeHandler.form + ); + const isInputAutofilled = + this.activeHandler.getFilledStateByElement(input) == + lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED; + const allFieldNames = this.activeSection.allFieldNames; + + const AutocompleteResult = lazy.FormAutofillUtils.isAddressField( + fieldDetail.fieldName + ) + ? lazy.AddressResult + : lazy.CreditCardResult; + + const acResult = new AutocompleteResult( + searchString, + fieldDetail.fieldName, + allFieldNames, + adaptedRecords, + { isSecure, isInputAutofilled } + ); + + acResult.externalEntries.push( + ...externalEntries.map( + entry => + new lazy.GenericAutocompleteItem( + entry.image, + entry.title, + entry.subtitle, + entry.fillMessageName, + entry.fillMessageData + ) + ) + ); + + return acResult; } } diff --git a/toolkit/components/formautofill/FormAutofillParent.sys.mjs b/toolkit/components/formautofill/FormAutofillParent.sys.mjs index 34dac8ce1502..bc56baeea7fd 100644 --- a/toolkit/components/formautofill/FormAutofillParent.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillParent.sys.mjs @@ -267,17 +267,8 @@ export class FormAutofillParent extends JSWindowActorParent { break; } case "FormAutofill:GetRecords": { - const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({ - formOrigin: this.formOrigin, - scenarioName: data.scenarioName, - hasInput: !!data.searchString?.length, - }); - const recordsPromise = FormAutofillParent.getRecords(data); - const [records, externalEntries] = await Promise.all([ - recordsPromise, - relayPromise, - ]); - return { records, externalEntries }; + const records = await FormAutofillParent.getRecords(data); + return { records }; } case "FormAutofill:OnFormSubmit": { this.notifyMessageObservers("onFormSubmitted", data); @@ -401,6 +392,43 @@ export class FormAutofillParent extends JSWindowActorParent { } } + /** + * Retrieves autocomplete entries for a given search string and data context. + * + * @param {string} searchString + * The search string used to filter autocomplete entries. + * @param {object} options + * @param {string} options.fieldName + * The name of the field for which autocomplete entries are being fetched. + * @param {string} options.scenarioName + * The scenario name used in the autocomplete operation to fetch external entries. + * @returns {Promise} A promise that resolves to an object containing two properties: `records` and `externalEntries`. + * `records` is an array of autofill records from the form's internal data, sorted by `timeLastUsed`. + * `externalEntries` is an array of external autocomplete items fetched based on the scenario. + */ + async searchAutoCompleteEntries(searchString, options) { + const { fieldName, scenarioName } = options; + const relayPromise = lazy.FirefoxRelay.autocompleteItemsAsync({ + formOrigin: this.formOrigin, + scenarioName, + hasInput: !!searchString?.length, + }); + + const recordsPromise = FormAutofillParent.getRecords({ + searchString, + fieldName, + }); + const [records, externalEntries] = await Promise.all([ + recordsPromise, + relayPromise, + ]); + + // Sort addresses by timeLastUsed for showing the lastest used address at top. + records.sort((a, b) => b.timeLastUsed - a.timeLastUsed); + + return { records, externalEntries }; + } + /** * Get the records from profile store and return results back to content * process. It will decrypt the credit card number and append diff --git a/toolkit/components/satchel/components.conf b/toolkit/components/satchel/components.conf index d5a670efd91c..88c98708bc05 100644 --- a/toolkit/components/satchel/components.conf +++ b/toolkit/components/satchel/components.conf @@ -9,7 +9,7 @@ Classes = [ 'cid': '{895db6c7-dbdf-40ea-9f64-b175033243dc}', 'contract_ids': [ '@mozilla.org/satchel/form-fill-controller;1', - '@mozilla.org/autocomplete/search;1?name=form-history', + '@mozilla.org/autocomplete/search;1?name=form-fill-controller', ], 'type': 'nsFormFillController', 'constructor': 'nsFormFillController::GetSingleton', diff --git a/toolkit/components/satchel/nsFormFillController.cpp b/toolkit/components/satchel/nsFormFillController.cpp index 61d23d157cef..eee75ceac1e1 100644 --- a/toolkit/components/satchel/nsFormFillController.cpp +++ b/toolkit/components/satchel/nsFormFillController.cpp @@ -522,18 +522,15 @@ nsFormFillController::GetSearchCount(uint32_t* aSearchCount) { NS_IMETHODIMP nsFormFillController::GetSearchAt(uint32_t index, nsACString& _retval) { - if (mAutofillInputs.Get(mFocusedInput)) { - MOZ_LOG(sLogger, LogLevel::Debug, ("GetSearchAt: autofill-profiles field")); - nsCOMPtr profileSearch = do_GetService( - "@mozilla.org/autocomplete/search;1?name=autofill-profiles"); - if (profileSearch) { - _retval.AssignLiteral("autofill-profiles"); - return NS_OK; - } - } + MOZ_LOG(sLogger, LogLevel::Debug, + ("GetSearchAt: form-fill-controller field")); - MOZ_LOG(sLogger, LogLevel::Debug, ("GetSearchAt: form-history field")); - _retval.AssignLiteral("form-history"); + // The better solution should be AutoCompleteController gets the + // nsIAutoCompleteSearch interface from AutoCompletePopup and invokes the + // StartSearch without going through FormFillController. Currently + // FormFillController acts as the proxy to find the AutoCompletePopup for + // AutoCompleteController. + _retval.AssignLiteral("form-fill-controller"); return NS_OK; } @@ -669,52 +666,56 @@ nsFormFillController::StartSearch(const nsAString& aSearchString, nsIAutoCompleteObserver* aListener) { MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch for %p", mFocusedInput)); - nsresult rv; + if (mFocusedInput && mFocusedPopup) { + if (mAutofillInputs.Get(mFocusedInput)) { + MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: formautofill field")); - // If the login manager has indicated it's responsible for this field, let it - // handle the autocomplete. Otherwise, handle with form history. - // This method is sometimes called in unit tests and from XUL without a - // focused node. - if (mFocusedInput && (mPwmgrInputs.Get(mFocusedInput) || - mFocusedInput->HasBeenTypePassword())) { - MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: login field")); + mLastListener = aListener; + return mFocusedPopup->StartSearch(aSearchString, mFocusedInput, this); + // If the login manager has indicated it's responsible for this field, let + // it handle the autocomplete. Otherwise, handle with form history. This + // method is sometimes called in unit tests and from XUL without a focused + // node. + } else if (mPwmgrInputs.Get(mFocusedInput) || + mFocusedInput->HasBeenTypePassword()) { + MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: login field")); - // Handle the case where a password field is focused but - // MarkAsLoginManagerField wasn't called because password manager is - // disabled. - if (!mLoginManagerAC) { - mLoginManagerAC = - do_GetService("@mozilla.org/login-manager/autocompletesearch;1"); + // Handle the case where a password field is focused but + // MarkAsLoginManagerField wasn't called because password manager is + // disabled. + if (!mLoginManagerAC) { + mLoginManagerAC = + do_GetService("@mozilla.org/login-manager/autocompletesearch;1"); + } + + if (NS_WARN_IF(!mLoginManagerAC)) { + return NS_ERROR_FAILURE; + } + + // XXX aPreviousResult shouldn't ever be a historyResult type, since we're + // not letting satchel manage the field? + mLastListener = aListener; + return mLoginManagerAC->StartSearch(aSearchString, aPreviousResult, + mFocusedInput, this); } - - if (NS_WARN_IF(!mLoginManagerAC)) { - return NS_ERROR_FAILURE; - } - - // XXX aPreviousResult shouldn't ever be a historyResult type, since we're - // not letting satchel manage the field? - mLastListener = aListener; - rv = mLoginManagerAC->StartSearch(aSearchString, aPreviousResult, - mFocusedInput, this); - NS_ENSURE_SUCCESS(rv, rv); - } else { - MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: non-login field")); - mLastListener = aListener; - - bool addDataList = IsTextControl(mFocusedInput); - if (addDataList) { - MaybeObserveDataListMutations(); - } - - auto* formHistoryAutoComplete = GetFormHistoryAutoComplete(); - NS_ENSURE_TRUE(formHistoryAutoComplete, NS_ERROR_FAILURE); - - formHistoryAutoComplete->AutoCompleteSearchAsync( - aSearchParam, aSearchString, mFocusedInput, aPreviousResult, - addDataList, this); - mLastFormHistoryAutoComplete = formHistoryAutoComplete; } + MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: non-login field")); + mLastListener = aListener; + + bool addDataList = IsTextControl(mFocusedInput); + if (addDataList) { + MaybeObserveDataListMutations(); + } + + auto* formHistoryAutoComplete = GetFormHistoryAutoComplete(); + NS_ENSURE_TRUE(formHistoryAutoComplete, NS_ERROR_FAILURE); + + formHistoryAutoComplete->AutoCompleteSearchAsync( + aSearchParam, aSearchString, mFocusedInput, aPreviousResult, addDataList, + this); + mLastFormHistoryAutoComplete = formHistoryAutoComplete; + return NS_OK; } @@ -767,6 +768,10 @@ nsFormFillController::StopSearch() { mLastFormHistoryAutoComplete = nullptr; } + if (mFocusedPopup) { + mFocusedPopup->StopSearch(); + } + if (mLoginManagerAC) { mLoginManagerAC->StopSearch(); } diff --git a/toolkit/components/satchel/test/unit/test_previous_result.js b/toolkit/components/satchel/test/unit/test_previous_result.js index a782832db7aa..e9583d8d61c9 100644 --- a/toolkit/components/satchel/test/unit/test_previous_result.js +++ b/toolkit/components/satchel/test/unit/test_previous_result.js @@ -19,7 +19,7 @@ var aaListener = { function run_test() { do_test_pending(); let search = Cc[ - "@mozilla.org/autocomplete/search;1?name=form-history" + "@mozilla.org/autocomplete/search;1?name=form-fill-controller" ].getService(Ci.nsIAutoCompleteSearch); search.startSearch("aa", "", null, aaListener); }