fune/toolkit/components/autocomplete/nsAutoCompleteController.cpp
Dimi a6fc3d1c7d Bug 1893623 - P1. Trigger autofill from the parent process r=credential-management-reviewers,sgalich
Currently, when users autocomplete a field for an address, credit card, or login, Firefox also "autofills"
the relevant fields. Here is a quick summary of how we currently manage this process:

1. Users click on an input field, the autocomplete popup is displayed, and Firefox searches for options
   so users can choose which value to autocomplete.
2. AutoCompleteChild searches for the value to autocomplete based on the type of the input field, along
   with the entire profile. For example, when we autocomplete a cc-number field, we also send cc-name, cc-exp, etc., to the child process.
3. AutoCompleteController autocompletes the focused input.
4. AutoCompleteController notifies the corresponding module, which then autofills the remaining fields.

Currently, step 4 is triggered directly in the child process. This patch moves the logic of step 4 from the
child process to the parent process. This change is a prerequisite for supporting autofill across frames and
will also enable us not to send the entire profile in step 2.

Differential Revision: https://phabricator.services.mozilla.com/D208752
2024-04-29 20:35:04 +00:00

1713 lines
57 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#include "nsAutoCompleteController.h"
#include "nsAutoCompleteSimpleResult.h"
#include "nsNetCID.h"
#include "nsIIOService.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsIScriptSecurityManager.h"
#include "nsIObserverService.h"
#include "nsServiceManagerUtils.h"
#include "mozilla/Services.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/KeyboardEventBinding.h"
#include "mozilla/dom/Event.h"
static const char* kAutoCompleteSearchCID =
"@mozilla.org/autocomplete/search;1?name=";
using namespace mozilla;
NS_IMPL_CYCLE_COLLECTION_CLASS(nsAutoCompleteController)
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsAutoCompleteController)
MOZ_KnownLive(tmp)->SetInput(nullptr);
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsAutoCompleteController)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInput)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSearches)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResults)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mResultCache)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(nsAutoCompleteController)
NS_IMPL_CYCLE_COLLECTING_RELEASE(nsAutoCompleteController)
NS_INTERFACE_TABLE_HEAD(nsAutoCompleteController)
NS_INTERFACE_TABLE(nsAutoCompleteController, nsIAutoCompleteController,
nsIAutoCompleteObserver, nsITimerCallback, nsINamed)
NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsAutoCompleteController)
NS_INTERFACE_MAP_END
nsAutoCompleteController::nsAutoCompleteController()
: mDefaultIndexCompleted(false),
mPopupClosedByCompositionStart(false),
mProhibitAutoFill(false),
mUserClearedAutoFill(false),
mCompositionState(eCompositionState_None),
mSearchStatus(nsAutoCompleteController::STATUS_NONE),
mMatchCount(0),
mSearchesOngoing(0),
mSearchesFailed(0),
mCompletedSelectionIndex(-1) {}
nsAutoCompleteController::~nsAutoCompleteController() { SetInput(nullptr); }
void nsAutoCompleteController::SetValueOfInputTo(const nsString& aValue) {
mSetValue = aValue;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
input->SetTextValue(aValue);
}
////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteController
NS_IMETHODIMP
nsAutoCompleteController::GetSearchStatus(uint16_t* aSearchStatus) {
*aSearchStatus = mSearchStatus;
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::GetMatchCount(uint32_t* aMatchCount) {
*aMatchCount = mMatchCount;
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::GetInput(nsIAutoCompleteInput** aInput) {
*aInput = mInput;
NS_IF_ADDREF(*aInput);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::SetInitiallySelectedIndex(int32_t aSelectedIndex) {
// First forward to the popup.
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
NS_ENSURE_STATE(input);
nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
NS_ENSURE_STATE(popup);
popup->SetSelectedIndex(aSelectedIndex);
// Now take care of internal stuff.
bool completeSelection;
if (NS_SUCCEEDED(input->GetCompleteSelectedIndex(&completeSelection)) &&
completeSelection) {
mCompletedSelectionIndex = aSelectedIndex;
}
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::SetInput(nsIAutoCompleteInput* aInput) {
// Don't do anything if the input isn't changing.
if (mInput == aInput) return NS_OK;
Unused << ResetInternalState();
if (mInput) {
mSearches.Clear();
ClosePopup();
}
mInput = aInput;
// Nothing more to do if the input was just being set to null.
if (!mInput) {
return NS_OK;
}
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
// Reset the current search string.
nsAutoString value;
input->GetTextValue(value);
SetSearchStringInternal(value);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::ResetInternalState() {
// Clear out the current search context
if (mInput) {
nsAutoString value;
mInput->GetTextValue(value);
// Stop all searches in case they are async.
Unused << StopSearch();
Unused << ClearResults();
SetSearchStringInternal(value);
}
mPlaceholderCompletionString.Truncate();
mDefaultIndexCompleted = false;
mProhibitAutoFill = false;
mSearchStatus = nsIAutoCompleteController::STATUS_NONE;
mMatchCount = 0;
mCompletedSelectionIndex = -1;
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::StartSearch(const nsAString& aSearchString) {
// If composition is ongoing don't start searching yet, until it is committed.
if (mCompositionState == eCompositionState_Composing) {
return NS_OK;
}
SetSearchStringInternal(aSearchString);
StartSearches();
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleText(bool* _retval) {
*_retval = false;
// Note: the events occur in the following order when IME is used.
// 1. a compositionstart event(HandleStartComposition)
// 2. some input events (HandleText), eCompositionState_Composing
// 3. a compositionend event(HandleEndComposition)
// 4. an input event(HandleText), eCompositionState_Committing
// We should do nothing during composition.
if (mCompositionState == eCompositionState_Composing) {
return NS_OK;
}
bool handlingCompositionCommit =
(mCompositionState == eCompositionState_Committing);
bool popupClosedByCompositionStart = mPopupClosedByCompositionStart;
if (handlingCompositionCommit) {
mCompositionState = eCompositionState_None;
mPopupClosedByCompositionStart = false;
}
if (!mInput) {
// Stop all searches in case they are async.
StopSearch();
// Note: if now is after blur and IME end composition,
// check mInput before calling.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31
NS_ERROR(
"Called before attaching to the control or after detaching from the "
"control");
return NS_OK;
}
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
nsAutoString newValue;
input->GetTextValue(newValue);
// Stop all searches in case they are async.
StopSearch();
if (!mInput) {
// StopSearch() can call PostSearchCleanup() which might result
// in a blur event, which could null out mInput, so we need to check it
// again. See bug #395344 for more details
return NS_OK;
}
bool disabled;
input->GetDisableAutoComplete(&disabled);
NS_ENSURE_TRUE(!disabled, NS_OK);
// Usually we don't search again if the new string is the same as the last
// one. However, if this is called immediately after compositionend event, we
// need to search the same value again since the search was canceled at
// compositionstart event handler. The new string might also be the same as
// the last search if the autofilled portion was cleared. In this case, we may
// want to search again.
// Whether the user removed some text at the end.
bool userRemovedText =
newValue.Length() < mSearchString.Length() &&
Substring(mSearchString, 0, newValue.Length()).Equals(newValue);
// Whether the user is repeating the previous search.
bool repeatingPreviousSearch =
!userRemovedText && newValue.Equals(mSearchString);
mUserClearedAutoFill =
repeatingPreviousSearch &&
newValue.Length() < mPlaceholderCompletionString.Length() &&
Substring(mPlaceholderCompletionString, 0, newValue.Length())
.Equals(newValue);
if (!handlingCompositionCommit && newValue.Length() > 0 &&
repeatingPreviousSearch) {
return NS_OK;
}
if (userRemovedText) {
// We need to throw away previous results so we don't try to search
// through them again.
ClearResults();
mProhibitAutoFill = true;
mPlaceholderCompletionString.Truncate();
} else {
mProhibitAutoFill = false;
}
SetSearchStringInternal(newValue);
bool noRollupOnEmptySearch;
nsresult rv = input->GetNoRollupOnEmptySearch(&noRollupOnEmptySearch);
NS_ENSURE_SUCCESS(rv, rv);
// Don't search if the value is empty
if (newValue.Length() == 0 && !noRollupOnEmptySearch) {
// If autocomplete popup was closed by compositionstart event handler,
// we should reopen it forcibly even if the value is empty.
if (popupClosedByCompositionStart && handlingCompositionCommit) {
bool cancel;
HandleKeyNavigation(dom::KeyboardEvent_Binding::DOM_VK_DOWN, &cancel);
return NS_OK;
}
ClosePopup();
return NS_OK;
}
*_retval = true;
StartSearches();
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleEnter(bool aIsPopupSelection,
dom::Event* aEvent, bool* _retval) {
*_retval = false;
if (!mInput) return NS_OK;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
// allow the event through unless there is something selected in the popup
input->GetPopupOpen(_retval);
if (*_retval) {
nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
if (popup) {
int32_t selectedIndex;
popup->GetSelectedIndex(&selectedIndex);
*_retval = selectedIndex >= 0;
}
}
// Stop the search, and handle the enter.
StopSearch();
// StopSearch() can call PostSearchCleanup() which might result
// in a blur event, which could null out mInput, so we need to check it
// again. See bug #408463 for more details
if (!mInput) {
return NS_OK;
}
EnterMatch(aIsPopupSelection, aEvent);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleEscape(bool* _retval) {
*_retval = false;
if (!mInput) return NS_OK;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
// allow the event through if the popup is closed
input->GetPopupOpen(_retval);
// Stop all searches in case they are async.
StopSearch();
ClearResults();
RevertTextValue();
ClosePopup();
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleStartComposition() {
NS_ENSURE_TRUE(mCompositionState != eCompositionState_Composing, NS_OK);
mPopupClosedByCompositionStart = false;
mCompositionState = eCompositionState_Composing;
if (!mInput) return NS_OK;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
bool disabled;
input->GetDisableAutoComplete(&disabled);
if (disabled) return NS_OK;
// Stop all searches in case they are async.
StopSearch();
bool isOpen = false;
input->GetPopupOpen(&isOpen);
if (isOpen) {
ClosePopup();
bool stillOpen = false;
input->GetPopupOpen(&stillOpen);
mPopupClosedByCompositionStart = !stillOpen;
}
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleEndComposition() {
NS_ENSURE_TRUE(mCompositionState == eCompositionState_Composing, NS_OK);
// We can't yet retrieve the committed value from the editor, since it isn't
// completely committed yet. Set mCompositionState to
// eCompositionState_Committing, so that when HandleText() is called (in
// response to the "input" event), we know that we should handle the
// committed text.
mCompositionState = eCompositionState_Committing;
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleTab() {
bool cancel;
return HandleEnter(false, nullptr, &cancel);
}
NS_IMETHODIMP
nsAutoCompleteController::HandleKeyNavigation(uint32_t aKey, bool* _retval) {
// By default, don't cancel the event
*_retval = false;
if (!mInput) {
// Stop all searches in case they are async.
StopSearch();
// Note: if now is after blur and IME end composition,
// check mInput before calling.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31
NS_ERROR(
"Called before attaching to the control or after detaching from the "
"control");
return NS_OK;
}
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
bool disabled;
input->GetDisableAutoComplete(&disabled);
NS_ENSURE_TRUE(!disabled, NS_OK);
if (aKey == dom::KeyboardEvent_Binding::DOM_VK_UP ||
aKey == dom::KeyboardEvent_Binding::DOM_VK_DOWN ||
aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_UP ||
aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_DOWN) {
bool isOpen = false;
input->GetPopupOpen(&isOpen);
if (isOpen) {
// Prevent the input from handling up/down events, as it may move
// the cursor to home/end on some systems
*_retval = true;
bool reverse = aKey == dom::KeyboardEvent_Binding::DOM_VK_UP ||
aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_UP;
bool page = aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_UP ||
aKey == dom::KeyboardEvent_Binding::DOM_VK_PAGE_DOWN;
// Fill in the value of the textbox with whatever is selected in the popup
// if the completeSelectedIndex attribute is set. We check this before
// calling SelectBy of an earlier attempt to avoid crashing.
bool completeSelection;
input->GetCompleteSelectedIndex(&completeSelection);
// The user has keyed up or down to change the selection. Stop the search
// (if there is one) now so that the results do not change while the user
// is making a selection.
Unused << StopSearch();
// Instruct the result view to scroll by the given amount and direction
popup->SelectBy(reverse, page);
if (completeSelection) {
int32_t selectedIndex;
popup->GetSelectedIndex(&selectedIndex);
if (selectedIndex >= 0) {
// A result is selected, so fill in its value
nsAutoString value;
if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) {
// If the result is the previously autofilled string, then restore
// the search string and selection that existed when the result was
// autofilled. Else, fill the result and move the caret to the end.
int32_t start;
if (value.Equals(mPlaceholderCompletionString,
nsCaseInsensitiveStringComparator)) {
start = mSearchString.Length();
value = mPlaceholderCompletionString;
SetValueOfInputTo(value);
} else {
start = value.Length();
SetValueOfInputTo(value);
}
input->SelectTextRange(start, value.Length());
}
mCompletedSelectionIndex = selectedIndex;
} else {
// Nothing is selected, so fill in the last typed value
SetValueOfInputTo(mSearchString);
input->SelectTextRange(mSearchString.Length(),
mSearchString.Length());
mCompletedSelectionIndex = -1;
}
}
} else {
// Only show the popup if the caret is at the start or end of the input
// and there is no selection, so that the default defined key shortcuts
// for up and down move to the beginning and end of the field otherwise.
if (aKey == dom::KeyboardEvent_Binding::DOM_VK_UP ||
aKey == dom::KeyboardEvent_Binding::DOM_VK_DOWN) {
const bool isUp = aKey == dom::KeyboardEvent_Binding::DOM_VK_UP;
int32_t start, end;
input->GetSelectionStart(&start);
input->GetSelectionEnd(&end);
if (isUp) {
if (start > 0 || start != end) {
return NS_OK;
}
} else {
nsAutoString text;
input->GetTextValue(text);
if (start != end || end < (int32_t)text.Length()) {
return NS_OK;
}
}
}
// Some script may have changed the value of the text field since our
// last keypress or after our focus handler and we don't want to
// search for a stale string.
nsAutoString value;
input->GetTextValue(value);
SetSearchStringInternal(value);
// Open the popup if there has been a previous non-errored search, or
// else kick off a new search
bool hadPreviousSearch = false;
for (uint32_t i = 0; i < mResults.Length(); ++i) {
nsAutoString oldSearchString;
uint16_t oldResult = 0;
nsIAutoCompleteResult* oldResultObject = mResults[i];
if (oldResultObject &&
NS_SUCCEEDED(oldResultObject->GetSearchResult(&oldResult)) &&
oldResult != nsIAutoCompleteResult::RESULT_FAILURE &&
NS_SUCCEEDED(oldResultObject->GetSearchString(oldSearchString)) &&
oldSearchString.Equals(mSearchString,
nsCaseInsensitiveStringComparator)) {
hadPreviousSearch = true;
break;
}
}
if (hadPreviousSearch) {
if (mMatchCount) {
OpenPopup();
}
} else {
// Stop all searches in case they are async.
StopSearch();
if (!mInput) {
// StopSearch() can call PostSearchCleanup() which might result
// in a blur event, which could null out mInput, so we need to check
// it again. See bug #395344 for more details
return NS_OK;
}
StartSearches();
}
bool isOpen = false;
input->GetPopupOpen(&isOpen);
if (isOpen) {
// Prevent the default action if we opened the popup in any of the code
// paths above.
*_retval = true;
}
}
} else if (aKey == dom::KeyboardEvent_Binding::DOM_VK_LEFT ||
aKey == dom::KeyboardEvent_Binding::DOM_VK_RIGHT
#ifndef XP_MACOSX
|| aKey == dom::KeyboardEvent_Binding::DOM_VK_HOME
#endif
) {
// The user hit a text-navigation key.
bool isOpen = false;
input->GetPopupOpen(&isOpen);
// If minresultsforpopup > 1 and there's less matches than the minimum
// required, the popup is not open, but the search suggestion is showing
// inline, so we should proceed as if we had the popup.
uint32_t minResultsForPopup;
input->GetMinResultsForPopup(&minResultsForPopup);
if (isOpen || (mMatchCount > 0 && mMatchCount < minResultsForPopup)) {
// For completeSelectedIndex autocomplete fields, if the popup shouldn't
// close when the caret is moved, don't adjust the text value or caret
// position.
bool completeSelection;
input->GetCompleteSelectedIndex(&completeSelection);
if (isOpen) {
bool noRollup;
input->GetNoRollupOnCaretMove(&noRollup);
if (noRollup) {
if (completeSelection) {
return NS_OK;
}
}
}
int32_t selectionEnd;
input->GetSelectionEnd(&selectionEnd);
int32_t selectionStart;
input->GetSelectionStart(&selectionStart);
bool shouldCompleteSelection =
(uint32_t)selectionEnd == mPlaceholderCompletionString.Length() &&
selectionStart < selectionEnd;
int32_t selectedIndex;
popup->GetSelectedIndex(&selectedIndex);
bool completeDefaultIndex;
input->GetCompleteDefaultIndex(&completeDefaultIndex);
if (completeDefaultIndex && shouldCompleteSelection) {
// We usually try to preserve the casing of what user has typed, but
// if he wants to autocomplete, we will replace the value with the
// actual autocomplete result. Note that the autocomplete input can also
// be showing e.g. "bar >> foo bar" if the search matched "bar", a
// word not at the start of the full value "foo bar".
// The user wants explicitely to use that result, so this ensures
// association of the result with the autocompleted text.
nsAutoString value;
nsAutoString inputValue;
input->GetTextValue(inputValue);
if (NS_SUCCEEDED(GetDefaultCompleteValue(-1, false, value))) {
nsAutoString suggestedValue;
int32_t pos = inputValue.Find(u" >> ");
if (pos > 0) {
inputValue.Right(suggestedValue, inputValue.Length() - pos - 4);
} else {
suggestedValue = inputValue;
}
if (value.Equals(suggestedValue, nsCaseInsensitiveStringComparator)) {
SetValueOfInputTo(value);
input->SelectTextRange(value.Length(), value.Length());
}
}
} else if (!completeDefaultIndex && !completeSelection &&
selectedIndex >= 0) {
// The pop-up is open and has a selection, take its value
nsAutoString value;
if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) {
SetValueOfInputTo(value);
input->SelectTextRange(value.Length(), value.Length());
}
}
// Close the pop-up even if nothing was selected
ClearSearchTimer();
ClosePopup();
}
// Update last-searched string to the current input, since the input may
// have changed. Without this, subsequent backspaces look like text
// additions, not text deletions.
nsAutoString value;
input->GetTextValue(value);
SetSearchStringInternal(value);
}
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::HandleDelete(bool* _retval) {
*_retval = false;
if (!mInput) return NS_OK;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
bool isOpen = false;
input->GetPopupOpen(&isOpen);
if (!isOpen || mMatchCount == 0) {
// Nothing left to delete, proceed as normal
bool unused = false;
HandleText(&unused);
return NS_OK;
}
nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
NS_ENSURE_TRUE(popup, NS_ERROR_FAILURE);
int32_t index, searchIndex, matchIndex;
popup->GetSelectedIndex(&index);
if (index == -1) {
// No match is selected in the list
bool unused = false;
HandleText(&unused);
return NS_OK;
}
MatchIndexToSearch(index, &searchIndex, &matchIndex);
NS_ENSURE_TRUE(searchIndex >= 0 && matchIndex >= 0, NS_ERROR_FAILURE);
nsIAutoCompleteResult* result = mResults.SafeObjectAt(searchIndex);
NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
bool removable;
nsresult rv = result->IsRemovableAt(matchIndex, &removable);
NS_ENSURE_SUCCESS(rv, rv);
if (!removable) {
return NS_OK;
}
nsAutoString search;
input->GetSearchParam(search);
// Clear the match in our result and in the DB.
result->RemoveValueAt(matchIndex);
--mMatchCount;
// We removed it, so make sure we cancel the event that triggered this call.
*_retval = true;
// Unselect the current item.
popup->SetSelectedIndex(-1);
// Adjust index, if needed.
MOZ_ASSERT(index >= 0); // We verified this above, after MatchIndexToSearch.
if (static_cast<uint32_t>(index) >= mMatchCount) index = mMatchCount - 1;
if (mMatchCount > 0) {
// There are still matches in the popup, select the current index again.
popup->SetSelectedIndex(index);
// Complete to the new current value.
bool shouldComplete = false;
input->GetCompleteDefaultIndex(&shouldComplete);
if (shouldComplete) {
nsAutoString value;
if (NS_SUCCEEDED(GetResultValueAt(index, false, value))) {
CompleteValue(value);
}
}
// Invalidate the popup.
popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_DELETE);
} else {
// Nothing left in the popup, clear any pending search timers and
// close the popup.
ClearSearchTimer();
uint32_t minResults;
input->GetMinResultsForPopup(&minResults);
if (minResults) {
ClosePopup();
}
}
return NS_OK;
}
nsresult nsAutoCompleteController::GetResultAt(int32_t aIndex,
nsIAutoCompleteResult** aResult,
int32_t* aMatchIndex) {
int32_t searchIndex;
MatchIndexToSearch(aIndex, &searchIndex, aMatchIndex);
NS_ENSURE_TRUE(searchIndex >= 0 && *aMatchIndex >= 0, NS_ERROR_FAILURE);
*aResult = mResults.SafeObjectAt(searchIndex);
NS_ENSURE_TRUE(*aResult, NS_ERROR_FAILURE);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::GetValueAt(int32_t aIndex, nsAString& _retval) {
GetResultLabelAt(aIndex, _retval);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::GetLabelAt(int32_t aIndex, nsAString& _retval) {
GetResultLabelAt(aIndex, _retval);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::GetCommentAt(int32_t aIndex, nsAString& _retval) {
int32_t matchIndex;
nsIAutoCompleteResult* result;
nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
NS_ENSURE_SUCCESS(rv, rv);
return result->GetCommentAt(matchIndex, _retval);
}
NS_IMETHODIMP
nsAutoCompleteController::GetStyleAt(int32_t aIndex, nsAString& _retval) {
int32_t matchIndex;
nsIAutoCompleteResult* result;
nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
NS_ENSURE_SUCCESS(rv, rv);
return result->GetStyleAt(matchIndex, _retval);
}
NS_IMETHODIMP
nsAutoCompleteController::GetImageAt(int32_t aIndex, nsAString& _retval) {
int32_t matchIndex;
nsIAutoCompleteResult* result;
nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
NS_ENSURE_SUCCESS(rv, rv);
return result->GetImageAt(matchIndex, _retval);
}
NS_IMETHODIMP
nsAutoCompleteController::GetFinalCompleteValueAt(int32_t aIndex,
nsAString& _retval) {
int32_t matchIndex;
nsIAutoCompleteResult* result;
nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
NS_ENSURE_SUCCESS(rv, rv);
return result->GetFinalCompleteValueAt(matchIndex, _retval);
}
NS_IMETHODIMP
nsAutoCompleteController::SetSearchString(const nsAString& aSearchString) {
SetSearchStringInternal(aSearchString);
return NS_OK;
}
NS_IMETHODIMP
nsAutoCompleteController::GetSearchString(nsAString& aSearchString) {
aSearchString = mSearchString;
return NS_OK;
}
////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteObserver
NS_IMETHODIMP
nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch* aSearch,
nsIAutoCompleteResult* aResult) {
MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch));
uint16_t result = 0;
if (aResult) {
aResult->GetSearchResult(&result);
}
// If our results are incremental, the search is still ongoing.
if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
--mSearchesOngoing;
}
// Look up the index of the search which is returning.
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
if (mSearches[i] == aSearch) {
ProcessResult(i, aResult);
break;
}
}
// If a match is found in ProcessResult, PostSearchCleanup will open the popup
PostSearchCleanup();
return NS_OK;
}
////////////////////////////////////////////////////////////////////////
//// nsITimerCallback
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHODIMP
nsAutoCompleteController::Notify(nsITimer* timer) {
mTimer = nullptr;
return DoSearches();
}
////////////////////////////////////////////////////////////////////////
//// nsINamed
NS_IMETHODIMP
nsAutoCompleteController::GetName(nsACString& aName) {
aName.AssignLiteral("nsAutoCompleteController");
return NS_OK;
}
////////////////////////////////////////////////////////////////////////
//// nsAutoCompleteController
nsresult nsAutoCompleteController::OpenPopup() {
uint32_t minResults;
mInput->GetMinResultsForPopup(&minResults);
if (mMatchCount >= minResults) {
nsCOMPtr<nsIAutoCompleteInput> input = mInput;
return input->SetPopupOpen(true);
}
return NS_OK;
}
nsresult nsAutoCompleteController::ClosePopup() {
if (!mInput) {
return NS_OK;
}
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
bool isOpen = false;
input->GetPopupOpen(&isOpen);
if (!isOpen) return NS_OK;
nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
MOZ_ALWAYS_SUCCEEDS(input->SetPopupOpen(false));
return popup->SetSelectedIndex(-1);
}
nsresult nsAutoCompleteController::BeforeSearches() {
NS_ENSURE_STATE(mInput);
mSearchStatus = nsIAutoCompleteController::STATUS_SEARCHING;
mDefaultIndexCompleted = false;
bool invalidatePreviousResult = false;
mInput->GetInvalidatePreviousResult(&invalidatePreviousResult);
if (!invalidatePreviousResult) {
// ClearResults will clear the mResults array, but we should pass the
// previous result to each search to allow reusing it. So we temporarily
// cache the current results until AfterSearches().
if (!mResultCache.AppendObjects(mResults)) {
return NS_ERROR_OUT_OF_MEMORY;
}
}
ClearResults(true);
mSearchesOngoing = mSearches.Length();
mSearchesFailed = 0;
// notify the input that the search is beginning
mInput->OnSearchBegin();
return NS_OK;
}
nsresult nsAutoCompleteController::StartSearch() {
NS_ENSURE_STATE(mInput);
nsCOMPtr<nsIAutoCompleteInput> input = mInput;
// Iterate a copy of |mSearches| so that we don't run into trouble if the
// array is mutated while we're still in the loop. An nsIAutoCompleteSearch
// implementation could synchronously start a new search when StartSearch()
// is called and that would lead to assertions down the way.
nsCOMArray<nsIAutoCompleteSearch> searchesCopy(mSearches);
for (uint32_t i = 0; i < searchesCopy.Length(); ++i) {
nsCOMPtr<nsIAutoCompleteSearch> search = searchesCopy[i];
nsIAutoCompleteResult* result = mResultCache.SafeObjectAt(i);
if (result) {
uint16_t searchResult;
result->GetSearchResult(&searchResult);
if (searchResult != nsIAutoCompleteResult::RESULT_SUCCESS &&
searchResult != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
searchResult != nsIAutoCompleteResult::RESULT_NOMATCH)
result = nullptr;
}
nsAutoString searchParam;
nsresult rv = input->GetSearchParam(searchParam);
if (NS_FAILED(rv)) return rv;
uint32_t userContextId;
rv = input->GetUserContextId(&userContextId);
if (NS_SUCCEEDED(rv) &&
userContextId != nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID) {
searchParam.AppendLiteral(" user-context-id:");
searchParam.AppendInt(userContextId, 10);
}
rv = search->StartSearch(mSearchString, searchParam, result,
static_cast<nsIAutoCompleteObserver*>(this));
if (NS_FAILED(rv)) {
++mSearchesFailed;
MOZ_ASSERT(mSearchesOngoing > 0);
--mSearchesOngoing;
}
// Because of the joy of nested event loops (which can easily happen when
// some code uses a generator for an asynchronous AutoComplete search),
// nsIAutoCompleteSearch::StartSearch might cause us to be detached from our
// input field. The next time we iterate, we'd be touching something that
// we shouldn't be, and result in a crash.
if (!mInput) {
// The search operation has been finished.
return NS_OK;
}
}
return NS_OK;
}
void nsAutoCompleteController::AfterSearches() {
mResultCache.Clear();
// if the below evaluates to true, that means mSearchesOngoing must be 0
if (mSearchesFailed == mSearches.Length()) {
PostSearchCleanup();
}
}
NS_IMETHODIMP
nsAutoCompleteController::StopSearch() {
// Stop the timer if there is one
ClearSearchTimer();
// Stop any ongoing asynchronous searches
if (mSearchStatus == nsIAutoCompleteController::STATUS_SEARCHING) {
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
nsCOMPtr<nsIAutoCompleteSearch> search = mSearches[i];
search->StopSearch();
}
mSearchesOngoing = 0;
// since we were searching, but now we've stopped,
// we need to call PostSearchCleanup()
PostSearchCleanup();
}
return NS_OK;
}
void nsAutoCompleteController::MaybeCompletePlaceholder() {
MOZ_ASSERT(mInput);
if (!mInput) { // or mInput depending on what you choose
MOZ_ASSERT_UNREACHABLE("Input should always be valid at this point");
return;
}
int32_t selectionStart;
mInput->GetSelectionStart(&selectionStart);
int32_t selectionEnd;
mInput->GetSelectionEnd(&selectionEnd);
// Check if the current input should be completed with the placeholder string
// from the last completion until the actual search results come back.
// The new input string needs to be compatible with the last completed string.
// E.g. if the new value is "fob", but the last completion was "foobar",
// then the last completion is incompatible.
// If the search string is the same as the last completion value, then don't
// complete the value again (this prevents completion to happen e.g. if the
// cursor is moved and StartSeaches() is invoked).
// In addition, the selection must be at the end of the current input to
// trigger the placeholder completion.
bool usePlaceholderCompletion =
!mUserClearedAutoFill && !mPlaceholderCompletionString.IsEmpty() &&
mPlaceholderCompletionString.Length() > mSearchString.Length() &&
selectionEnd == selectionStart &&
selectionEnd == (int32_t)mSearchString.Length() &&
StringBeginsWith(mPlaceholderCompletionString, mSearchString,
nsCaseInsensitiveStringComparator);
if (usePlaceholderCompletion) {
CompleteValue(mPlaceholderCompletionString);
} else {
mPlaceholderCompletionString.Truncate();
}
}
nsresult nsAutoCompleteController::StartSearches() {
// Don't create a new search timer if we're already waiting for one to fire.
// If we don't check for this, we won't be able to cancel the original timer
// and may crash when it fires (bug 236659).
if (mTimer || !mInput) return NS_OK;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
if (!mSearches.Length()) {
// Initialize our list of search objects
uint32_t searchCount;
input->GetSearchCount(&searchCount);
mResults.SetCapacity(searchCount);
mSearches.SetCapacity(searchCount);
const char* searchCID = kAutoCompleteSearchCID;
for (uint32_t i = 0; i < searchCount; ++i) {
// Use the search name to create the contract id string for the search
// service
nsAutoCString searchName;
input->GetSearchAt(i, searchName);
nsAutoCString cid(searchCID);
cid.Append(searchName);
// Use the created cid to get a pointer to the search service and store it
// for later
nsCOMPtr<nsIAutoCompleteSearch> search = do_GetService(cid.get());
if (search) {
mSearches.AppendObject(search);
}
}
}
// Check if the current input should be completed with the placeholder string
// from the last completion until the actual search results come back.
MaybeCompletePlaceholder();
// Get the timeout for delayed searches.
uint32_t timeout;
input->GetTimeout(&timeout);
if (timeout == 0) {
// If the timeout is 0, we still have to execute the delayed searches,
// otherwise this will be a no-op.
return DoSearches();
}
// Now start the delayed searches.
return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, timeout,
nsITimer::TYPE_ONE_SHOT);
}
nsresult nsAutoCompleteController::DoSearches() {
nsresult rv = BeforeSearches();
if (NS_FAILED(rv)) return rv;
StartSearch();
// All the searches have been started, just finish.
AfterSearches();
return NS_OK;
}
nsresult nsAutoCompleteController::ClearSearchTimer() {
if (mTimer) {
mTimer->Cancel();
mTimer = nullptr;
}
return NS_OK;
}
nsresult nsAutoCompleteController::EnterMatch(bool aIsPopupSelection,
dom::Event* aEvent) {
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
bool forceComplete;
input->GetForceComplete(&forceComplete);
int32_t selectedIndex;
popup->GetSelectedIndex(&selectedIndex);
// Ask the popup if it wants to enter a special value into the textbox
nsAutoString value;
nsAutoString comment;
popup->GetOverrideValue(value);
if (value.IsEmpty()) {
bool shouldComplete;
input->GetCompleteDefaultIndex(&shouldComplete);
bool completeSelection;
input->GetCompleteSelectedIndex(&completeSelection);
if (selectedIndex >= 0) {
nsAutoString inputValue;
input->GetTextValue(inputValue);
GetCommentAt(selectedIndex, comment);
if (aIsPopupSelection || !completeSelection) {
// We need to fill-in the value if:
// * completeselectedindex is false
// * A match in the popup was confirmed
GetResultValueAt(selectedIndex, true, value);
} else if (mDefaultIndexCompleted &&
inputValue.Equals(mPlaceholderCompletionString,
nsCaseInsensitiveStringComparator)) {
// We also need to fill-in the value if the default index completion was
// confirmed, though we cannot use the selectedIndex cause the selection
// may have been changed by the mouse in the meanwhile.
GetFinalDefaultCompleteValue(value);
} else if (mCompletedSelectionIndex != -1) {
// If completeselectedindex is true, and EnterMatch was not invoked by
// mouse-clicking a match (for example the user pressed Enter),
// don't fill in the value as it will have already been filled in as
// needed, unless the selected match has a final complete value that
// differs from the user-facing value.
nsAutoString finalValue;
GetResultValueAt(mCompletedSelectionIndex, true, finalValue);
if (!inputValue.Equals(finalValue)) {
value = finalValue;
}
}
} else if (shouldComplete) {
// We usually try to preserve the casing of what user has typed, but
// if he wants to autocomplete, we will replace the value with the
// actual autocomplete result.
// The user wants explicitely to use that result, so this ensures
// association of the result with the autocompleted text.
nsAutoString defaultIndexValue;
if (NS_SUCCEEDED(GetFinalDefaultCompleteValue(defaultIndexValue)))
value = defaultIndexValue;
}
if (forceComplete && value.IsEmpty() && shouldComplete) {
// See if inputValue is one of the autocomplete results. It can be an
// identical value, or if it matched the middle of a result it can be
// something like "bar >> foobar" (user entered bar and foobar is
// the result value).
// If the current search matches one of the autocomplete results, we
// should use that result, and not overwrite it with the default value.
// It's indeed possible EnterMatch gets called a second time (for example
// by the blur handler) and it should not overwrite the current match.
nsAutoString inputValue;
input->GetTextValue(inputValue);
nsAutoString suggestedValue;
int32_t pos = inputValue.Find(u" >> ");
if (pos > 0) {
inputValue.Right(suggestedValue, inputValue.Length() - pos - 4);
} else {
suggestedValue = inputValue;
}
for (uint32_t i = 0; i < mResults.Length(); ++i) {
nsIAutoCompleteResult* result = mResults[i];
if (result) {
uint32_t matchCount = 0;
result->GetMatchCount(&matchCount);
for (uint32_t j = 0; j < matchCount; ++j) {
nsAutoString matchValue;
result->GetValueAt(j, matchValue);
if (suggestedValue.Equals(matchValue,
nsCaseInsensitiveStringComparator)) {
nsAutoString finalMatchValue;
result->GetFinalCompleteValueAt(j, finalMatchValue);
value = finalMatchValue;
break;
}
}
}
}
// The value should have been set at this point. If not, then it's not
// a value that should be autocompleted.
} else if (forceComplete && value.IsEmpty() && completeSelection) {
// Since nothing was selected, and forceComplete is specified, that means
// we have to find the first default match and enter it instead.
for (uint32_t i = 0; i < mResults.Length(); ++i) {
nsIAutoCompleteResult* result = mResults[i];
if (result) {
int32_t defaultIndex;
result->GetDefaultIndex(&defaultIndex);
if (defaultIndex >= 0) {
result->GetFinalCompleteValueAt(defaultIndex, value);
break;
}
}
}
}
}
if (comment.IsEmpty()) {
comment.Assign(u"{}");
}
nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
NS_ENSURE_STATE(obsSvc);
obsSvc->NotifyObservers(input, "autocomplete-will-enter-text", comment.get());
if (!value.IsEmpty()) {
SetValueOfInputTo(value);
input->SelectTextRange(value.Length(), value.Length());
SetSearchStringInternal(value);
}
popup->SelectEntry();
obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);
input->OnTextEntered(aEvent);
ClosePopup();
return NS_OK;
}
nsresult nsAutoCompleteController::RevertTextValue() {
// StopSearch() can call PostSearchCleanup() which might result
// in a blur event, which could null out mInput, so we need to check it
// again. See bug #408463 for more details
if (!mInput) return NS_OK;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
// If current input value is different from what we have set, it means
// somebody modified the value like JS of the web content. In such case,
// we shouldn't overwrite it with the old value.
nsAutoString currentValue;
input->GetTextValue(currentValue);
if (currentValue != mSetValue) {
SetSearchStringInternal(currentValue);
return NS_OK;
}
bool cancel = false;
input->OnTextReverted(&cancel);
if (!cancel) {
nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
NS_ENSURE_STATE(obsSvc);
obsSvc->NotifyObservers(input, "autocomplete-will-revert-text", nullptr);
// Don't change the value if it is the same to prevent sending useless
// events. NOTE: how can |RevertTextValue| be called with inputValue !=
// oldValue?
if (mSearchString != currentValue) {
SetValueOfInputTo(mSearchString);
}
obsSvc->NotifyObservers(input, "autocomplete-did-revert-text", nullptr);
}
return NS_OK;
}
nsresult nsAutoCompleteController::ProcessResult(
int32_t aSearchIndex, nsIAutoCompleteResult* aResult) {
NS_ENSURE_STATE(mInput);
MOZ_ASSERT(aResult, "ProcessResult should always receive a result");
NS_ENSURE_ARG(aResult);
uint16_t searchResult = 0;
aResult->GetSearchResult(&searchResult);
// The following code supports incremental updating results in 2 ways:
// * The search may reuse the same result, just by adding entries to it.
// * The search may send a new result every time. In this case we merge
// the results and proceed on the same code path as before.
// This way both mSearches and mResults can be indexed by the search index,
// cause we'll always have only one result per search.
if (mResults.IndexOf(aResult) == -1) {
nsIAutoCompleteResult* oldResult = mResults.SafeObjectAt(aSearchIndex);
if (oldResult) {
MOZ_ASSERT(false,
"Passing new matches to OnSearchResult with a new "
"nsIAutoCompleteResult every time is deprecated, please "
"update the same result until the search is done");
// Build a new nsIAutocompleteSimpleResult and merge results into it.
RefPtr<nsAutoCompleteSimpleResult> mergedResult =
new nsAutoCompleteSimpleResult();
mergedResult->AppendResult(oldResult);
mergedResult->AppendResult(aResult);
mResults.ReplaceObjectAt(mergedResult, aSearchIndex);
} else {
// This inserts and grows the array if needed.
mResults.ReplaceObjectAt(aResult, aSearchIndex);
}
}
// When found the result should have the same index as the search.
MOZ_ASSERT_IF(mResults.IndexOf(aResult) != -1,
mResults.IndexOf(aResult) == aSearchIndex);
MOZ_ASSERT(mResults.Count() >= aSearchIndex + 1,
"aSearchIndex should always be valid for mResults");
uint32_t oldMatchCount = mMatchCount;
// If the search failed, increase the match count to include the error
// description.
if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) {
nsAutoString error;
aResult->GetErrorDescription(error);
if (!error.IsEmpty()) {
++mMatchCount;
}
} else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
// Increase the match count for all matches in this result.
uint32_t totalMatchCount = 0;
for (uint32_t i = 0; i < mResults.Length(); i++) {
nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
if (result) {
uint32_t matchCount = 0;
result->GetMatchCount(&matchCount);
totalMatchCount += matchCount;
}
}
uint32_t delta = totalMatchCount - oldMatchCount;
mMatchCount += delta;
}
// Try to autocomplete the default index for this search.
// Do this before invalidating so the binding knows about it.
CompleteDefaultIndex(aSearchIndex);
// Refresh the popup view to display the new search results
nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT);
return NS_OK;
}
nsresult nsAutoCompleteController::PostSearchCleanup() {
NS_ENSURE_STATE(mInput);
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
uint32_t minResults;
input->GetMinResultsForPopup(&minResults);
if (mMatchCount || minResults == 0) {
OpenPopup();
} else if (mSearchesOngoing == 0) {
ClosePopup();
}
if (mSearchesOngoing == 0) {
mSearchStatus = mMatchCount
? nsIAutoCompleteController::STATUS_COMPLETE_MATCH
: nsIAutoCompleteController::STATUS_COMPLETE_NO_MATCH;
// notify the input that the search is complete
input->OnSearchComplete();
}
return NS_OK;
}
nsresult nsAutoCompleteController::ClearResults(bool aIsSearching) {
int32_t oldMatchCount = mMatchCount;
mMatchCount = 0;
mResults.Clear();
if (oldMatchCount != 0) {
if (mInput) {
nsCOMPtr<nsIAutoCompletePopup> popup(GetPopup());
NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
// Clear the selection.
popup->SetSelectedIndex(-1);
}
}
return NS_OK;
}
nsresult nsAutoCompleteController::CompleteDefaultIndex(int32_t aResultIndex) {
if (mDefaultIndexCompleted || mProhibitAutoFill ||
mSearchString.Length() == 0 || !mInput)
return NS_OK;
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
int32_t selectionStart;
input->GetSelectionStart(&selectionStart);
int32_t selectionEnd;
input->GetSelectionEnd(&selectionEnd);
bool isPlaceholderSelected =
selectionEnd == (int32_t)mPlaceholderCompletionString.Length() &&
selectionStart == (int32_t)mSearchString.Length() &&
StringBeginsWith(mPlaceholderCompletionString, mSearchString,
nsCaseInsensitiveStringComparator);
// Don't try to automatically complete to the first result if there's already
// a selection or the cursor isn't at the end of the input. In case the
// selection is from the current placeholder completion value, then still
// automatically complete.
if (!isPlaceholderSelected &&
(selectionEnd != selectionStart ||
selectionEnd != (int32_t)mSearchString.Length()))
return NS_OK;
bool shouldComplete;
input->GetCompleteDefaultIndex(&shouldComplete);
if (!shouldComplete) return NS_OK;
nsAutoString resultValue;
if (NS_SUCCEEDED(GetDefaultCompleteValue(aResultIndex, true, resultValue))) {
CompleteValue(resultValue);
mDefaultIndexCompleted = true;
} else {
// Reset the search string again, in case it was completed with
// mPlaceholderCompletionString, but the actually received result doesn't
// have a default index result. Only reset the input when necessary, to
// avoid triggering unnecessary new searches.
nsAutoString inputValue;
input->GetTextValue(inputValue);
if (!inputValue.Equals(mSearchString)) {
SetValueOfInputTo(mSearchString);
input->SelectTextRange(mSearchString.Length(), mSearchString.Length());
}
mPlaceholderCompletionString.Truncate();
}
return NS_OK;
}
nsresult nsAutoCompleteController::GetDefaultCompleteResult(
int32_t aResultIndex, nsIAutoCompleteResult** _result,
int32_t* _defaultIndex) {
*_defaultIndex = -1;
int32_t resultIndex = aResultIndex;
// If a result index was not provided, find the first defaultIndex result.
for (int32_t i = 0; resultIndex < 0 && i < mResults.Count(); ++i) {
nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
if (result && NS_SUCCEEDED(result->GetDefaultIndex(_defaultIndex)) &&
*_defaultIndex >= 0) {
resultIndex = i;
}
}
if (resultIndex < 0) {
return NS_ERROR_FAILURE;
}
*_result = mResults.SafeObjectAt(resultIndex);
NS_ENSURE_TRUE(*_result, NS_ERROR_FAILURE);
if (*_defaultIndex < 0) {
// The search must explicitly provide a default index in order
// for us to be able to complete.
(*_result)->GetDefaultIndex(_defaultIndex);
}
if (*_defaultIndex < 0) {
// We were given a result index, but that result doesn't want to
// be autocompleted.
return NS_ERROR_FAILURE;
}
// If the result wrongly notifies a RESULT_SUCCESS with no matches, or
// provides a defaultIndex greater than its matchCount, avoid trying to
// complete to an empty value.
uint32_t matchCount = 0;
(*_result)->GetMatchCount(&matchCount);
// Here defaultIndex is surely non-negative, so can be cast to unsigned.
if ((uint32_t)(*_defaultIndex) >= matchCount) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
nsresult nsAutoCompleteController::GetDefaultCompleteValue(int32_t aResultIndex,
bool aPreserveCasing,
nsAString& _retval) {
nsIAutoCompleteResult* result;
int32_t defaultIndex = -1;
nsresult rv = GetDefaultCompleteResult(aResultIndex, &result, &defaultIndex);
if (NS_FAILED(rv)) return rv;
nsAutoString resultValue;
result->GetValueAt(defaultIndex, resultValue);
if (aPreserveCasing && StringBeginsWith(resultValue, mSearchString,
nsCaseInsensitiveStringComparator)) {
// We try to preserve user casing, otherwise we would end up changing
// the case of what he typed, if we have a result with a different casing.
// For example if we have result "Test", and user starts writing "tuna",
// after digiting t, we would convert it to T trying to autocomplete "Test".
// We will still complete to cased "Test" if the user explicitely choose
// that result, by either selecting it in the results popup, or with
// keyboard navigation or if autocompleting in the middle.
nsAutoString casedResultValue;
casedResultValue.Assign(mSearchString);
// Use what the user has typed so far.
casedResultValue.Append(
Substring(resultValue, mSearchString.Length(), resultValue.Length()));
_retval = casedResultValue;
} else
_retval = resultValue;
return NS_OK;
}
nsresult nsAutoCompleteController::GetFinalDefaultCompleteValue(
nsAString& _retval) {
MOZ_ASSERT(mInput, "Must have a valid input");
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
nsIAutoCompleteResult* result;
int32_t defaultIndex = -1;
nsresult rv = GetDefaultCompleteResult(-1, &result, &defaultIndex);
if (NS_FAILED(rv)) return rv;
result->GetValueAt(defaultIndex, _retval);
nsAutoString inputValue;
input->GetTextValue(inputValue);
if (!_retval.Equals(inputValue, nsCaseInsensitiveStringComparator)) {
return NS_ERROR_FAILURE;
}
nsAutoString finalCompleteValue;
rv = result->GetFinalCompleteValueAt(defaultIndex, finalCompleteValue);
if (NS_SUCCEEDED(rv)) {
_retval = finalCompleteValue;
}
return NS_OK;
}
nsresult nsAutoCompleteController::CompleteValue(nsString& aValue)
/* mInput contains mSearchString, which we want to autocomplete to aValue. If
* selectDifference is true, select the remaining portion of aValue not
* contained in mSearchString. */
{
MOZ_ASSERT(mInput, "Must have a valid input");
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
const int32_t mSearchStringLength = mSearchString.Length();
int32_t endSelect = aValue.Length(); // By default, select all of aValue.
if (aValue.IsEmpty() || StringBeginsWith(aValue, mSearchString,
nsCaseInsensitiveStringComparator)) {
// aValue is empty (we were asked to clear mInput), or mSearchString
// matches the beginning of aValue. In either case we can simply
// autocomplete to aValue.
mPlaceholderCompletionString = aValue;
SetValueOfInputTo(aValue);
} else {
nsresult rv;
nsCOMPtr<nsIIOService> ios = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString scheme;
if (NS_SUCCEEDED(
ios->ExtractScheme(NS_ConvertUTF16toUTF8(aValue), scheme))) {
// Trying to autocomplete a URI from somewhere other than the beginning.
// Only succeed if the missing portion is "http://"; otherwise do not
// autocomplete. This prevents us from "helpfully" autocompleting to a
// URI that isn't equivalent to what the user expected.
const int32_t findIndex = 7; // length of "http://"
if ((endSelect < findIndex + mSearchStringLength) ||
!scheme.EqualsLiteral("http") ||
!Substring(aValue, findIndex, mSearchStringLength)
.Equals(mSearchString, nsCaseInsensitiveStringComparator)) {
return NS_OK;
}
mPlaceholderCompletionString =
mSearchString +
Substring(aValue, mSearchStringLength + findIndex, endSelect);
SetValueOfInputTo(mPlaceholderCompletionString);
endSelect -= findIndex; // We're skipping this many characters of aValue.
} else {
// Autocompleting something other than a URI from the middle.
// Use the format "searchstring >> full string" to indicate to the user
// what we are going to replace their search string with.
SetValueOfInputTo(mSearchString + u" >> "_ns + aValue);
endSelect = mSearchString.Length() + 4 + aValue.Length();
// Reset the last search completion.
mPlaceholderCompletionString.Truncate();
}
}
input->SelectTextRange(mSearchStringLength, endSelect);
return NS_OK;
}
nsresult nsAutoCompleteController::GetResultLabelAt(int32_t aIndex,
nsAString& _retval) {
return GetResultValueLabelAt(aIndex, false, false, _retval);
}
nsresult nsAutoCompleteController::GetResultValueAt(int32_t aIndex,
bool aGetFinalValue,
nsAString& _retval) {
return GetResultValueLabelAt(aIndex, aGetFinalValue, true, _retval);
}
nsresult nsAutoCompleteController::GetResultValueLabelAt(int32_t aIndex,
bool aGetFinalValue,
bool aGetValue,
nsAString& _retval) {
NS_ENSURE_TRUE(aIndex >= 0 && static_cast<uint32_t>(aIndex) < mMatchCount,
NS_ERROR_ILLEGAL_VALUE);
int32_t matchIndex;
nsIAutoCompleteResult* result;
nsresult rv = GetResultAt(aIndex, &result, &matchIndex);
NS_ENSURE_SUCCESS(rv, rv);
uint16_t searchResult;
result->GetSearchResult(&searchResult);
if (searchResult == nsIAutoCompleteResult::RESULT_FAILURE) {
if (aGetValue) return NS_ERROR_FAILURE;
result->GetErrorDescription(_retval);
} else if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
if (aGetFinalValue) {
// Some implementations may miss finalCompleteValue, try to be backwards
// compatible.
if (NS_FAILED(result->GetFinalCompleteValueAt(matchIndex, _retval))) {
result->GetValueAt(matchIndex, _retval);
}
} else if (aGetValue) {
result->GetValueAt(matchIndex, _retval);
} else {
result->GetLabelAt(matchIndex, _retval);
}
}
return NS_OK;
}
/**
* Given the index of a match in the autocomplete popup, find the
* corresponding nsIAutoCompleteSearch index, and sub-index into
* the search's results list.
*/
nsresult nsAutoCompleteController::MatchIndexToSearch(int32_t aMatchIndex,
int32_t* aSearchIndex,
int32_t* aItemIndex) {
*aSearchIndex = -1;
*aItemIndex = -1;
uint32_t index = 0;
// Move index through the results of each registered nsIAutoCompleteSearch
// until we find the given match
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
nsIAutoCompleteResult* result = mResults.SafeObjectAt(i);
if (!result) continue;
uint32_t matchCount = 0;
uint16_t searchResult;
result->GetSearchResult(&searchResult);
// Find out how many results were provided by the
// current nsIAutoCompleteSearch.
if (searchResult == nsIAutoCompleteResult::RESULT_SUCCESS ||
searchResult == nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING) {
result->GetMatchCount(&matchCount);
}
// If the given match index is within the results range
// of the current nsIAutoCompleteSearch then return the
// search index and sub-index into the results array
if ((matchCount != 0) &&
(index + matchCount - 1 >= (uint32_t)aMatchIndex)) {
*aSearchIndex = i;
*aItemIndex = aMatchIndex - index;
return NS_OK;
}
// Advance the popup table index cursor past the
// results of the current search.
index += matchCount;
}
return NS_OK;
}