toolkit/components/autocomplete/nsAutoCompleteController.cpp
author Marco Bonardo <mbonardo@mozilla.com>
Sun, 19 Nov 2017 21:58:14 +0100
changeset 701875 1676a2ff0da45c2ec95ffda90154ab9c26bdf8fb
parent 696192 c58e3a20c6f151a1f45c593bf0b342cd9262d98c
child 702554 f955a43accb4f537a9102a2741b43af850b01ca7
permissions -rw-r--r--
Bug 1415908 - Intermittent failure (nsIAutoCompleteController.getCommentAt) in browser_ext_omnibox.js. r=adw Fixes a timing bug where in certain moments matchCount may not be in sync with the current search status, due to previous results not being cleared immediately. Still delays tree updates to avoid UI flickering. Fixes a theorical timing issue in unifiedComplete where a stopped search could notify a result. Removes the no more used OnUpdateSearchResult API. MozReview-Commit-ID: COoIN4oQT4v

/* -*- 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 "nsAutoPtr.h"
#include "nsNetCID.h"
#include "nsIIOService.h"
#include "nsToolkitCompsCID.h"
#include "nsIServiceManager.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsIScriptSecurityManager.h"
#include "nsITreeBoxObject.h"
#include "nsITreeColumns.h"
#include "nsIObserverService.h"
#include "nsIDOMKeyEvent.h"
#include "mozilla/Services.h"
#include "mozilla/ModuleUtils.h"
#include "mozilla/Unused.h"

static const char *kAutoCompleteSearchCID = "@mozilla.org/autocomplete/search;1?name=";

using namespace mozilla;

namespace {

void
SetTextValue(nsIAutoCompleteInput* aInput,
             const nsString& aValue,
             uint16_t aReason) {
  nsresult rv = aInput->SetTextValueWithReason(aValue, aReason);
  if (NS_FAILED(rv)) {
    aInput->SetTextValue(aValue);
  }
}

} // anon namespace

NS_IMPL_CYCLE_COLLECTION_CLASS(nsAutoCompleteController)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsAutoCompleteController)
  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_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, nsITreeView,
                     nsINamed)
  NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsAutoCompleteController)
NS_INTERFACE_MAP_END

nsAutoCompleteController::nsAutoCompleteController() :
  mDefaultIndexCompleted(false),
  mPopupClosedByCompositionStart(false),
  mProhibitAutoFill(false),
  mUserClearedAutoFill(false),
  mClearingAutoFillSearchesAgain(false),
  mCompositionState(eCompositionState_None),
  mSearchStatus(nsAutoCompleteController::STATUS_NONE),
  mRowCount(0),
  mSearchesOngoing(0),
  mSearchesFailed(0),
  mDelayedRowCountDelta(0),
  mImmediateSearchesCount(0),
  mCompletedSelectionIndex(-1)
{
}

nsAutoCompleteController::~nsAutoCompleteController()
{
  SetInput(nullptr);
}

////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteController

NS_IMETHODIMP
nsAutoCompleteController::GetSearchStatus(uint16_t *aSearchStatus)
{
  *aSearchStatus = mSearchStatus;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetMatchCount(uint32_t *aMatchCount)
{
  *aMatchCount = mRowCount;
  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;
  input->GetPopup(getter_AddRefs(popup));
  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.
  input->GetTextValue(mSearchString);

  // Clear out this reference in case the new input's popup has no tree
  mTree = nullptr;

  // Initialize our list of search objects
  uint32_t searchCount;
  input->GetSearchCount(&searchCount);
  mResults.SetCapacity(searchCount);
  mSearches.SetCapacity(searchCount);
  mImmediateSearchesCount = 0;

  const char *searchCID = kAutoCompleteSearchCID;

  // Since the controller can be used as a service it's important to reset this.
  mClearingAutoFillSearchesAgain = false;

  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);

      // Count immediate searches.
      nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc =
        do_QueryInterface(search);
      if (searchDesc) {
        uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED;
        if (NS_SUCCEEDED(searchDesc->GetSearchType(&searchType)) &&
            searchType == nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE) {
          mImmediateSearchesCount++;
        }

        if (!mClearingAutoFillSearchesAgain) {
          searchDesc->GetClearingAutoFillSearchesAgain(&mClearingAutoFillSearchesAgain);
        }
      }
    }
  }

  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();
    mSearchString = value;
  }

  mPlaceholderCompletionString.Truncate();
  mDefaultIndexCompleted = false;
  mProhibitAutoFill = false;
  mSearchStatus = nsIAutoCompleteController::STATUS_NONE;
  mRowCount = 0;
  mDelayedRowCountDelta = 0;
  mCompletedSelectionIndex = -1;

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::StartSearch(const nsAString &aSearchString)
{
  mSearchString = 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);
  bool searchAgainOnAutoFillClear = mUserClearedAutoFill && mClearingAutoFillSearchesAgain;

  if (!handlingCompositionCommit &&
      !searchAgainOnAutoFillClear &&
      newValue.Length() > 0 &&
      repeatingPreviousSearch) {
    return NS_OK;
  }

  if (userRemovedText || searchAgainOnAutoFillClear) {
    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;
  }

  mSearchString = newValue;

  // Don't search if the value is empty
  if (newValue.Length() == 0) {
    // 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(nsIDOMKeyEvent::DOM_VK_DOWN, &cancel);
      return NS_OK;
    }
    ClosePopup();
    return NS_OK;
  }

  *_retval = true;
  StartSearches();

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HandleEnter(bool aIsPopupSelection,
                                      nsIDOMEvent *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;
    input->GetPopup(getter_AddRefs(popup));

    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;
  input->GetPopup(getter_AddRefs(popup));
  NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);

  bool disabled;
  input->GetDisableAutoComplete(&disabled);
  NS_ENSURE_TRUE(!disabled, NS_OK);

  if (aKey == nsIDOMKeyEvent::DOM_VK_UP ||
      aKey == nsIDOMKeyEvent::DOM_VK_DOWN ||
      aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP ||
      aKey == nsIDOMKeyEvent::DOM_VK_PAGE_DOWN)
  {
    // Prevent the input from handling up/down events, as it may move
    // the cursor to home/end on some systems
    *_retval = true;

    bool isOpen = false;
    input->GetPopupOpen(&isOpen);
    if (isOpen) {
      bool reverse = aKey == nsIDOMKeyEvent::DOM_VK_UP ||
                      aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP ? true : false;
      bool page = aKey == nsIDOMKeyEvent::DOM_VK_PAGE_UP ||
                    aKey == nsIDOMKeyEvent::DOM_VK_PAGE_DOWN ? true : false;

      // 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);

      // 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;
              SetTextValue(input, value,
                           nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
            } else {
              start = value.Length();
              SetTextValue(input, value,
                           nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETESELECTED);
            }

            input->SelectTextRange(start, value.Length());
          }
          mCompletedSelectionIndex = selectedIndex;
        } else {
          // Nothing is selected, so fill in the last typed value
          SetTextValue(input, mSearchString,
                       nsIAutoCompleteInput::TEXTVALUE_REASON_REVERT);
          input->SelectTextRange(mSearchString.Length(), mSearchString.Length());
          mCompletedSelectionIndex = -1;
        }
      }
    } else {
#ifdef XP_MACOSX
      // on Mac, 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.
      int32_t start, end;
      if (aKey == nsIDOMKeyEvent::DOM_VK_UP) {
        input->GetSelectionStart(&start);
        input->GetSelectionEnd(&end);
        if (start > 0 || start != end)
          *_retval = false;
      }
      else if (aKey == nsIDOMKeyEvent::DOM_VK_DOWN) {
        nsAutoString text;
        input->GetTextValue(text);
        input->GetSelectionStart(&start);
        input->GetSelectionEnd(&end);
        if (start != end || end < (int32_t)text.Length())
          *_retval = false;
      }
#endif
      if (*_retval) {
        nsAutoString oldSearchString;
        // Open the popup if there has been a previous search, or else kick off a new search
        if (!mResults.IsEmpty() &&
            NS_SUCCEEDED(mResults[0]->GetSearchString(oldSearchString)) &&
            oldSearchString.Equals(mSearchString, nsCaseInsensitiveStringComparator())) {
          if (mRowCount) {
            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;
          }

          // 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);
          mSearchString = value;

          StartSearches();
        }
      }
    }
  } else if (   aKey == nsIDOMKeyEvent::DOM_VK_LEFT
             || aKey == nsIDOMKeyEvent::DOM_VK_RIGHT
#ifndef XP_MACOSX
             || aKey == nsIDOMKeyEvent::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 || (mRowCount > 0 && mRowCount < 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.
      if (isOpen) {
        bool noRollup;
        input->GetNoRollupOnCaretMove(&noRollup);
        if (noRollup) {
          bool completeSelection;
          input->GetCompleteSelectedIndex(&completeSelection);
          if (completeSelection) {
            return NS_OK;
          }
        }
      }

      int32_t selectedIndex;
      popup->GetSelectedIndex(&selectedIndex);
      bool shouldComplete;
      input->GetCompleteDefaultIndex(&shouldComplete);
      if (selectedIndex >= 0) {
        // The pop-up is open and has a selection, take its value
        nsAutoString value;
        if (NS_SUCCEEDED(GetResultValueAt(selectedIndex, false, value))) {
          SetTextValue(input, value,
                       nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETESELECTED);
          input->SelectTextRange(value.Length(), value.Length());
        }
      }
      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. 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(" >> ");
          if (pos > 0) {
            inputValue.Right(suggestedValue, inputValue.Length() - pos - 4);
          } else {
            suggestedValue = inputValue;
          }

          if (value.Equals(suggestedValue, nsCaseInsensitiveStringComparator())) {
            SetTextValue(input, value,
                         nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
            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);
    mSearchString = 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 || mRowCount == 0) {
    // Nothing left to delete, proceed as normal
    bool unused = false;
    HandleText(&unused);
    return NS_OK;
  }

  nsCOMPtr<nsIAutoCompletePopup> popup;
  input->GetPopup(getter_AddRefs(popup));

  int32_t index, searchIndex, rowIndex;
  popup->GetSelectedIndex(&index);
  if (index == -1) {
    // No row is selected in the list
    bool unused = false;
    HandleText(&unused);
    return NS_OK;
  }

  RowIndexToSearch(index, &searchIndex, &rowIndex);
  NS_ENSURE_TRUE(searchIndex >= 0 && rowIndex >= 0, NS_ERROR_FAILURE);

  nsIAutoCompleteResult *result = mResults.SafeObjectAt(searchIndex);
  NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);

  nsAutoString search;
  input->GetSearchParam(search);

  // Clear the row in our result and in the DB.
  result->RemoveValueAt(rowIndex, true);
  --mRowCount;

  // We removed it, so make sure we cancel the event that triggered this call.
  *_retval = true;

  // Unselect the current item.
  popup->SetSelectedIndex(-1);

  // Tell the tree that the row count changed.
  if (mTree)
    mTree->RowCountChanged(mRowCount, -1);

  // Adjust index, if needed.
  MOZ_ASSERT(index >= 0); // We verified this above, after RowIndexToSearch.
  if (static_cast<uint32_t>(index) >= mRowCount)
    index = mRowCount - 1;

  if (mRowCount > 0) {
    // There are still rows 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* aRowIndex)
{
  int32_t searchIndex;
  RowIndexToSearch(aIndex, &searchIndex, aRowIndex);
  NS_ENSURE_TRUE(searchIndex >= 0 && *aRowIndex >= 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 rowIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  return result->GetCommentAt(rowIndex, _retval);
}

NS_IMETHODIMP
nsAutoCompleteController::GetStyleAt(int32_t aIndex, nsAString & _retval)
{
  int32_t rowIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  return result->GetStyleAt(rowIndex, _retval);
}

NS_IMETHODIMP
nsAutoCompleteController::GetImageAt(int32_t aIndex, nsAString & _retval)
{
  int32_t rowIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  return result->GetImageAt(rowIndex, _retval);
}

NS_IMETHODIMP
nsAutoCompleteController::GetFinalCompleteValueAt(int32_t aIndex,
                                                  nsAString & _retval)
{
  int32_t rowIndex;
  nsIAutoCompleteResult* result;
  nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
  NS_ENSURE_SUCCESS(rv, rv);

  return result->GetFinalCompleteValueAt(rowIndex, _retval);
}

NS_IMETHODIMP
nsAutoCompleteController::SetSearchString(const nsAString &aSearchString)
{
  mSearchString = aSearchString;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetSearchString(nsAString &aSearchString)
{
  aSearchString = mSearchString;
  return NS_OK;
}

void
nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch,
                                             nsIAutoCompleteResult *aResult)
{
  // 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);
    }
  }
}


////////////////////////////////////////////////////////////////////////
//// nsIAutoCompleteObserver

NS_IMETHODIMP
nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
{
  MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch));

  // Update the tree if necessary.
  if (mTree && mDelayedRowCountDelta != 0) {
    mTree->RowCountChanged(0, mDelayedRowCountDelta);
    mDelayedRowCountDelta = 0;
  }

  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;
  }

  HandleSearchResult(aSearch, aResult);

  if (mSearchesOngoing == 0) {
    // If this is the last search to return, cleanup.
    PostSearchCleanup();
  }

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsITimerCallback

NS_IMETHODIMP
nsAutoCompleteController::Notify(nsITimer *timer)
{
  mTimer = nullptr;

  if (mImmediateSearchesCount == 0) {
    // If there were no immediate searches, BeforeSearches has not yet been
    // called, so do it now.
    nsresult rv = BeforeSearches();
    if (NS_FAILED(rv))
      return rv;
  }
  StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED);
  AfterSearches();
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsINamed

NS_IMETHODIMP
nsAutoCompleteController::GetName(nsACString& aName)
{
  aName.AssignLiteral("nsAutoCompleteController");
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
// nsITreeView

NS_IMETHODIMP
nsAutoCompleteController::GetRowCount(int32_t *aRowCount)
{
  *aRowCount = mRowCount;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetRowProperties(int32_t index, nsAString& aProps)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetCellProperties(int32_t row, nsITreeColumn* col,
                                            nsAString& aProps)
{
  if (row >= 0) {
    GetStyleAt(row, aProps);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetColumnProperties(nsITreeColumn* col, nsAString& aProps)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetImageSrc(int32_t row, nsITreeColumn* col, nsAString& _retval)
{
  const char16_t* colID;
  col->GetIdConst(&colID);

  if (NS_LITERAL_STRING("treecolAutoCompleteValue").Equals(colID))
    return GetImageAt(row, _retval);

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetProgressMode(int32_t row, nsITreeColumn* col, int32_t* _retval)
{
  NS_NOTREACHED("tree has no progress cells");
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetCellValue(int32_t row, nsITreeColumn* col, nsAString& _retval)
{
  NS_NOTREACHED("all of our cells are text");
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetCellText(int32_t row, nsITreeColumn* col, nsAString& _retval)
{
  const char16_t* colID;
  col->GetIdConst(&colID);

  if (NS_LITERAL_STRING("treecolAutoCompleteValue").Equals(colID))
    GetValueAt(row, _retval);
  else if (NS_LITERAL_STRING("treecolAutoCompleteComment").Equals(colID))
    GetCommentAt(row, _retval);

  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::IsContainer(int32_t index, bool *_retval)
{
  *_retval = false;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::IsContainerOpen(int32_t index, bool *_retval)
{
  NS_NOTREACHED("no container cells");
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::IsContainerEmpty(int32_t index, bool *_retval)
{
  NS_NOTREACHED("no container cells");
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetLevel(int32_t index, int32_t *_retval)
{
  *_retval = 0;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetParentIndex(int32_t rowIndex, int32_t *_retval)
{
  *_retval = -1;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::HasNextSibling(int32_t rowIndex, int32_t afterIndex, bool *_retval)
{
  *_retval = false;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::ToggleOpenState(int32_t index)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::SetTree(nsITreeBoxObject *tree)
{
  mTree = tree;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::GetSelection(nsITreeSelection * *aSelection)
{
  *aSelection = mSelection;
  NS_IF_ADDREF(*aSelection);
  return NS_OK;
}

NS_IMETHODIMP nsAutoCompleteController::SetSelection(nsITreeSelection * aSelection)
{
  mSelection = aSelection;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::SelectionChanged()
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::SetCellValue(int32_t row, nsITreeColumn* col, const nsAString& value)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::SetCellText(int32_t row, nsITreeColumn* col, const nsAString& value)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::CycleHeader(nsITreeColumn* col)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::CycleCell(int32_t row, nsITreeColumn* col)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::IsEditable(int32_t row, nsITreeColumn* col, bool *_retval)
{
  *_retval = false;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::IsSelectable(int32_t row, nsITreeColumn* col, bool *_retval)
{
  *_retval = false;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::IsSeparator(int32_t index, bool *_retval)
{
  *_retval = false;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::IsSorted(bool *_retval)
{
  *_retval = false;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::CanDrop(int32_t index, int32_t orientation,
                                  nsIDOMDataTransfer* dataTransfer, bool *_retval)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::Drop(int32_t row, int32_t orientation, nsIDOMDataTransfer* dataTransfer)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::PerformAction(const char16_t *action)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::PerformActionOnRow(const char16_t *action, int32_t row)
{
  return NS_OK;
}

NS_IMETHODIMP
nsAutoCompleteController::PerformActionOnCell(const char16_t* action, int32_t row, nsITreeColumn* col)
{
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////
//// nsAutoCompleteController

nsresult
nsAutoCompleteController::OpenPopup()
{
  uint32_t minResults;
  mInput->GetMinResultsForPopup(&minResults);

  if (mRowCount >= minResults) {
    return mInput->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;
  input->GetPopup(getter_AddRefs(popup));
  NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
  popup->SetSelectedIndex(-1);
  return input->SetPopupOpen(false);
}

nsresult
nsAutoCompleteController::BeforeSearches()
{
  NS_ENSURE_STATE(mInput);

  mSearchStatus = nsIAutoCompleteController::STATUS_SEARCHING;
  mDefaultIndexCompleted = false;

  // 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;
  }
  // The rowCountChanged notification is sent later, when the first result
  // from the search arrives, to avoid flickering in the tree contents.
  mDelayedRowCountDelta = 0;
  ClearResults(true);
  mSearchesOngoing = mSearches.Length();
  mSearchesFailed = 0;

  // notify the input that the search is beginning
  mInput->OnSearchBegin();

  return NS_OK;
}

nsresult
nsAutoCompleteController::StartSearch(uint16_t aSearchType)
{
  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];

    // Filter on search type.  Not all the searches implement this interface,
    // in such a case just consider them delayed.
    uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED;
    nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc =
      do_QueryInterface(search);
    if (searchDesc)
      searchDesc->GetSearchType(&searchType);
    if (searchType != aSearchType)
      continue;

    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;

    // FormFill expects the searchParam to only contain the input element id,
    // other consumers may have other expectations, so this modifies it only
    // for new consumers handling autoFill by themselves.
    if (mProhibitAutoFill && mClearingAutoFillSearchesAgain) {
      searchParam.AppendLiteral(" prohibit-autofill");
    }

    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 (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;

  // Check if the current input should be completed with the placeholder string
  // from the last completion until the actual search results come back.
  MaybeCompletePlaceholder();

  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  // Get the timeout for delayed searches.
  uint32_t timeout;
  input->GetTimeout(&timeout);

  uint32_t immediateSearchesCount = mImmediateSearchesCount;
  if (timeout == 0) {
    // All the searches should be executed immediately.
    immediateSearchesCount = mSearches.Length();
  }

  if (immediateSearchesCount > 0) {
    nsresult rv = BeforeSearches();
    if (NS_FAILED(rv))
      return rv;
    StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE);

    if (mSearches.Length() == immediateSearchesCount) {
      // Either all searches are immediate, or the timeout is 0.  In the
      // latter case we still have to execute the delayed searches, otherwise
      // this will be a no-op.
      StartSearch(nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED);

      // All the searches have been started, just finish.
      AfterSearches();
      return NS_OK;
    }
  }

  MOZ_ASSERT(timeout > 0, "Trying to delay searches with a 0 timeout!");

  // Now start the delayed searches.
  return NS_NewTimerWithCallback(getter_AddRefs(mTimer),
                                 this, timeout, nsITimer::TYPE_ONE_SHOT);
}

nsresult
nsAutoCompleteController::ClearSearchTimer()
{
  if (mTimer) {
    mTimer->Cancel();
    mTimer = nullptr;
  }
  return NS_OK;
}

nsresult
nsAutoCompleteController::EnterMatch(bool aIsPopupSelection,
                                     nsIDOMEvent *aEvent)
{
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
  nsCOMPtr<nsIAutoCompletePopup> popup;
  input->GetPopup(getter_AddRefs(popup));
  NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);

  bool forceComplete;
  input->GetForceComplete(&forceComplete);

  // Ask the popup if it wants to enter a special value into the textbox
  nsAutoString value;
  popup->GetOverrideValue(value);
  if (value.IsEmpty()) {
    bool shouldComplete;
    input->GetCompleteDefaultIndex(&shouldComplete);
    bool completeSelection;
    input->GetCompleteSelectedIndex(&completeSelection);

    int32_t selectedIndex;
    popup->GetSelectedIndex(&selectedIndex);
    if (selectedIndex >= 0) {
      nsAutoString inputValue;
      input->GetTextValue(inputValue);
      if (aIsPopupSelection || !completeSelection) {
        // We need to fill-in the value if:
        //  * completeselectedindex is false
        //  * A row in the popup was confirmed
        //
        // TODO: This is not totally correct, cause it will also confirm
        // a result selected with a simple mouseover, that could also have
        // happened accidentally, maybe touching a touchpad.
        // The reason is that autocomplete.xml sets selectedIndex on mousemove
        // making impossible, in the !completeSelection case, to distinguish if
        // the user wanted to confirm autoFill or the popup entry.
        // The solution may be to change autocomplete.xml to set selectedIndex
        // only on popupClick, but that requires changing the selection behavior.
        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;
        }
        // Note that if the user opens the popup, mouses over entries without
        // ever selecting one with the keyboard, and then hits enter, none of
        // the above cases will be hit, since mouseover doesn't activate
        // completeselectedindex and thus mCompletedSelectionIndex would be
        // -1.
      }
    } 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(" >> ");
      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;
          }
        }
      }
    }
  }

  nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
  NS_ENSURE_STATE(obsSvc);
  obsSvc->NotifyObservers(input, "autocomplete-will-enter-text", nullptr);

  if (!value.IsEmpty()) {
    SetTextValue(input, value, nsIAutoCompleteInput::TEXTVALUE_REASON_ENTERMATCH);
    input->SelectTextRange(value.Length(), value.Length());
    mSearchString = value;
  }

  obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);
  ClosePopup();

  bool cancel;
  input->OnTextEntered(aEvent, &cancel);

  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;

  nsAutoString oldValue(mSearchString);
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  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);

    nsAutoString inputValue;
    input->GetTextValue(inputValue);
    // 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 (!oldValue.Equals(inputValue)) {
      SetTextValue(input, oldValue, nsIAutoCompleteInput::TEXTVALUE_REASON_REVERT);
    }

    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);
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  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 oldRowCount = mRowCount;
  // 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()) {
      ++mRowCount;
      if (mTree) {
        mTree->RowCountChanged(oldRowCount, 1);
      }
    }
  } 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 - oldRowCount;

    mRowCount += delta;
    if (mTree) {
      mTree->RowCountChanged(oldRowCount, 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;
  input->GetPopup(getter_AddRefs(popup));
  NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
  popup->Invalidate(nsIAutoCompletePopup::INVALIDATE_REASON_NEW_RESULT);

  uint32_t minResults;
  input->GetMinResultsForPopup(&minResults);

  // Make sure the popup is open, if necessary, since we now have at least one
  // search result ready to display. Don't force the popup closed if we might
  // get results in the future to avoid unnecessarily canceling searches.
  if (mRowCount || !minResults) {
    OpenPopup();
  } else if (mSearchesOngoing == 0) {
    ClosePopup();
  }

  return NS_OK;
}

nsresult
nsAutoCompleteController::PostSearchCleanup()
{
  NS_ENSURE_STATE(mInput);
  nsCOMPtr<nsIAutoCompleteInput> input(mInput);

  uint32_t minResults;
  input->GetMinResultsForPopup(&minResults);

  // Apply a pending rowCountChanged.
  if (mTree && mDelayedRowCountDelta != 0) {
    mTree->RowCountChanged(0, mDelayedRowCountDelta);
    // mDelayedRowCountDelta will be reset by the next search.
  }

  if (mRowCount || minResults == 0) {
    OpenPopup();
    if (mRowCount)
      mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_MATCH;
    else
      mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_NO_MATCH;
  } else {
    mSearchStatus = nsIAutoCompleteController::STATUS_COMPLETE_NO_MATCH;
    ClosePopup();
  }

  // notify the input that the search is complete
  input->OnSearchComplete();

  return NS_OK;
}

nsresult
nsAutoCompleteController::ClearResults(bool aIsSearching)
{
  int32_t oldRowCount = mRowCount;
  mRowCount = 0;
  mResults.Clear();
  if (oldRowCount != 0) {
    if (mTree) {
      if (aIsSearching) {
        // Delay the notification, so the tree provides a smoother transition to
        // the new result. It will be handled as soon as we add the first result.
        mDelayedRowCountDelta = -oldRowCount;
      } else {
        // Notify immediately.
        mTree->RowCountChanged(0, -oldRowCount);
      }
    } else if (mInput) {
      nsCOMPtr<nsIAutoCompletePopup> popup;
      mInput->GetPopup(getter_AddRefs(popup));
      NS_ENSURE_TRUE(popup != nullptr, NS_ERROR_FAILURE);
      // if we had a tree, RowCountChanged() would have cleared the selection
      // when the selected row was removed.  But since we don't have a tree,
      // we need to clear the selection manually.
      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;
  }

  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;
    SetTextValue(input, aValue,
                 nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);
  } 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.LowerCaseEqualsLiteral("http") ||
          !Substring(aValue, findIndex, mSearchStringLength).Equals(
            mSearchString, nsCaseInsensitiveStringComparator())) {
        return NS_OK;
      }

      mPlaceholderCompletionString = mSearchString +
        Substring(aValue, mSearchStringLength + findIndex, endSelect);
      SetTextValue(input, mPlaceholderCompletionString,
                   nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);

      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.
      SetTextValue(input, mSearchString + NS_LITERAL_STRING(" >> ") + aValue,
                   nsIAutoCompleteInput::TEXTVALUE_REASON_COMPLETEDEFAULT);

      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) < mRowCount, NS_ERROR_ILLEGAL_VALUE);

  int32_t rowIndex;
  nsIAutoCompleteResult *result;
  nsresult rv = GetResultAt(aIndex, &result, &rowIndex);
  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(rowIndex, _retval))) {
        result->GetValueAt(rowIndex, _retval);
      }
    } else if (aGetValue) {
      result->GetValueAt(rowIndex, _retval);
    } else {
      result->GetLabelAt(rowIndex, _retval);
    }
  }

  return NS_OK;
}

/**
 * Given the index of a row in the autocomplete popup, find the
 * corresponding nsIAutoCompleteSearch index, and sub-index into
 * the search's results list.
 */
nsresult
nsAutoCompleteController::RowIndexToSearch(int32_t aRowIndex, 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 row
  for (uint32_t i = 0; i < mSearches.Length(); ++i) {
    nsIAutoCompleteResult *result = mResults.SafeObjectAt(i);
    if (!result)
      continue;

    uint32_t rowCount = 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(&rowCount);
    }

    // If the given row index is within the results range
    // of the current nsIAutoCompleteSearch then return the
    // search index and sub-index into the results array
    if ((rowCount != 0) && (index + rowCount-1 >= (uint32_t) aRowIndex)) {
      *aSearchIndex = i;
      *aItemIndex = aRowIndex - index;
      return NS_OK;
    }

    // Advance the popup table index cursor past the
    // results of the current search.
    index += rowCount;
  }

  return NS_OK;
}

NS_GENERIC_FACTORY_CONSTRUCTOR(nsAutoCompleteController)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsAutoCompleteSimpleResult)

NS_DEFINE_NAMED_CID(NS_AUTOCOMPLETECONTROLLER_CID);
NS_DEFINE_NAMED_CID(NS_AUTOCOMPLETESIMPLERESULT_CID);

static const mozilla::Module::CIDEntry kAutoCompleteCIDs[] = {
  { &kNS_AUTOCOMPLETECONTROLLER_CID, false, nullptr, nsAutoCompleteControllerConstructor },
  { &kNS_AUTOCOMPLETESIMPLERESULT_CID, false, nullptr, nsAutoCompleteSimpleResultConstructor },
  { nullptr }
};

static const mozilla::Module::ContractIDEntry kAutoCompleteContracts[] = {
  { NS_AUTOCOMPLETECONTROLLER_CONTRACTID, &kNS_AUTOCOMPLETECONTROLLER_CID },
  { NS_AUTOCOMPLETESIMPLERESULT_CONTRACTID, &kNS_AUTOCOMPLETESIMPLERESULT_CID },
  { nullptr }
};

static const mozilla::Module kAutoCompleteModule = {
  mozilla::Module::kVersion,
  kAutoCompleteCIDs,
  kAutoCompleteContracts
};

NSMODULE_DEFN(tkAutoCompleteModule) = &kAutoCompleteModule;