Bug 566746 - Changes to form autocomplete to support new asynchronous FormHistory.jsm module, p=enndeakin,felix, r=dteller
authorFelix Fung <ffung@mozilla.com>
Fri, 09 Mar 2012 04:57:05 -0500
changeset 135188 5901830c7a9c063b931a30d5578519fec2c4308f
parent 135187 34598ecfe079725a8cbb716f7d81578a3851cc46
child 135189 44b897958ad4d139147ba23e2c2465966c34dd1b
push id3752
push userlsblakk@mozilla.com
push dateMon, 13 May 2013 17:21:10 +0000
treeherdermozilla-aurora@1580544aef0b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdteller
bugs566746
milestone23.0a1
Bug 566746 - Changes to form autocomplete to support new asynchronous FormHistory.jsm module, p=enndeakin,felix, r=dteller
toolkit/components/satchel/nsFormAutoComplete.js
toolkit/components/satchel/nsFormFillController.cpp
toolkit/components/satchel/nsFormFillController.h
toolkit/components/satchel/nsIFormAutoComplete.idl
--- a/toolkit/components/satchel/nsFormAutoComplete.js
+++ b/toolkit/components/satchel/nsFormAutoComplete.js
@@ -5,60 +5,60 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+                                  "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+                                  "resource://gre/modules/FormHistory.jsm");
+
 function FormAutoComplete() {
     this.init();
 }
 
 FormAutoComplete.prototype = {
     classID          : Components.ID("{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"),
     QueryInterface   : XPCOMUtils.generateQI([Ci.nsIFormAutoComplete, Ci.nsISupportsWeakReference]),
 
-    __formHistory : null,
-    get _formHistory() {
-        if (!this.__formHistory)
-            this.__formHistory = Cc["@mozilla.org/satchel/form-history;1"].
-                                 getService(Ci.nsIFormHistory2);
-        return this.__formHistory;
-    },
-
     _prefBranch         : null,
     _debug              : true, // mirrors browser.formfill.debug
     _enabled            : true, // mirrors browser.formfill.enable preference
     _agedWeight         : 2,
     _bucketSize         : 1,
     _maxTimeGroupings   : 25,
     _timeGroupingSize   : 7 * 24 * 60 * 60 * 1000 * 1000,
     _expireDays         : null,
     _boundaryWeight     : 25,
     _prefixWeight       : 5,
 
+    // Only one query is performed at a time, which will be stored in _pendingQuery
+    // while the query is being performed. It will be cleared when the query finishes,
+    // is cancelled, or an error occurs. If a new query occurs while one is already
+    // pending, the existing one is cancelled. The pending query will be an
+    // mozIStoragePendingStatement object.
+    _pendingQuery       : null,
+
     init : function() {
         // Preferences. Add observer so we get notified of changes.
         this._prefBranch = Services.prefs.getBranch("browser.formfill.");
         this._prefBranch.addObserver("", this.observer, true);
         this.observer._self = this;
 
         this._debug            = this._prefBranch.getBoolPref("debug");
         this._enabled          = this._prefBranch.getBoolPref("enable");
         this._agedWeight       = this._prefBranch.getIntPref("agedWeight");
         this._bucketSize       = this._prefBranch.getIntPref("bucketSize");
         this._maxTimeGroupings = this._prefBranch.getIntPref("maxTimeGroupings");
         this._timeGroupingSize = this._prefBranch.getIntPref("timeGroupingSize") * 1000 * 1000;
         this._expireDays       = this._prefBranch.getIntPref("expire_days");
-
-        this._dbStmts = {};
-
-        Services.obs.addObserver(this.observer, "profile-before-change", true);
     },
 
     observer : {
         _self : null,
 
         QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
                                                 Ci.nsISupportsWeakReference]),
 
@@ -91,22 +91,16 @@ FormAutoComplete.prototype = {
                         self._boundaryWeight = self._prefBranch.getIntPref(prefName);
                         break;
                     case "prefixWeight":
                         self._prefixWeight = self._prefBranch.getIntPref(prefName);
                         break;
                     default:
                         self.log("Oops! Pref not handled, change ignored.");
                 }
-            } else if (topic == "profile-before-change") {
-                for each (let stmt in self._dbStmts) {
-                    stmt.finalize();
-                }
-                self._dbStmts = {};
-                self.__formHistory = null;
             }
         }
     },
 
 
     /*
      * log
      *
@@ -116,43 +110,74 @@ FormAutoComplete.prototype = {
     log : function (message) {
         if (!this._debug)
             return;
         dump("FormAutoComplete: " + message + "\n");
         Services.console.logStringMessage("FormAutoComplete: " + message);
     },
 
 
+    autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
+      Deprecated.warning("nsIFormAutoComplete::autoCompleteSearch is deprecated", "https://bugzilla.mozilla.org/show_bug.cgi?id=697377");
+
+      let result = null;
+      let listener = {
+        onSearchCompletion: function (r) result = r
+      };
+      this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, listener);
+
+      // Just wait for the result to to be available.
+      let thread = Components.classes["@mozilla.org/thread-manager;1"].getService().currentThread;
+      while (!result && this._pendingQuery) {
+        thread.processNextEvent(true);
+      }
+
+      return result;
+    },
+
+    autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
+      this._autoCompleteSearchShared(aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener);
+    },
+
     /*
-     * autoCompleteSearch
+     * autoCompleteSearchShared
      *
      * aInputName    -- |name| attribute from the form input being autocompleted.
      * aUntrimmedSearchString -- current value of the input
      * aField -- nsIDOMHTMLInputElement being autocompleted (may be null if from chrome)
      * aPreviousResult -- previous search result, if any.
-     *
-     * Returns: an nsIAutoCompleteResult
+     * aListener -- nsIFormAutoCompleteObserver that listens for the nsIAutoCompleteResult
+     *              that may be returned asynchronously.
      */
-    autoCompleteSearch : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult) {
+    _autoCompleteSearchShared : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
         function sortBytotalScore (a, b) {
             return b.totalScore - a.totalScore;
         }
 
-        if (!this._enabled)
-            return null;
+        let result = null;
+        if (!this._enabled) {
+            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
+            if (aListener) {
+              aListener.onSearchCompletion(result);
+            }
+            return;
+        }
 
         // don't allow form inputs (aField != null) to get results from search bar history
         if (aInputName == 'searchbar-history' && aField) {
             this.log('autoCompleteSearch for input name "' + aInputName + '" is denied');
-            return null;
+            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
+            if (aListener) {
+              aListener.onSearchCompletion(result);
+            }
+            return;
         }
 
         this.log("AutoCompleteSearch invoked. Search is: " + aUntrimmedSearchString);
         let searchString = aUntrimmedSearchString.trim().toLowerCase();
-        let result = null;
 
         // reuse previous results if:
         // a) length greater than one character (others searches are special cases) AND
         // b) the the new results will be a subset of the previous results
         if (aPreviousResult && aPreviousResult.searchString.trim().length > 1 &&
             searchString.indexOf(aPreviousResult.searchString.trim().toLowerCase()) >= 0) {
             this.log("Using previous autocomplete result");
             result = aPreviousResult;
@@ -171,155 +196,86 @@ FormAutoComplete.prototype = {
                     continue;
                 this._calculateScore(entry, searchString, searchTokens);
                 this.log("Reusing autocomplete entry '" + entry.text +
                          "' (" + entry.frecency +" / " + entry.totalScore + ")");
                 filteredEntries.push(entry);
             }
             filteredEntries.sort(sortBytotalScore);
             result.wrappedJSObject.entries = filteredEntries;
+
+            if (aListener) {
+              aListener.onSearchCompletion(result);
+            }
         } else {
             this.log("Creating new autocomplete search result.");
-            let entries = this.getAutoCompleteValues(aInputName, searchString);
-            result = new FormAutoCompleteResult(this._formHistory, entries, aInputName, aUntrimmedSearchString);
-            if (aField && aField.maxLength > -1) {
-                let original = result.wrappedJSObject.entries;
-                let filtered = original.filter(function (el) el.text.length <= this.maxLength, aField);
-                result.wrappedJSObject.entries = filtered;
+
+            // Start with an empty list.
+            result = new FormAutoCompleteResult(FormHistory, [], aInputName, aUntrimmedSearchString);
+
+            let processEntry = function(aEntries) {
+              if (aField && aField.maxLength > -1) {
+                result.entries =
+                  aEntries.filter(function (el) { return el.text.length <= aField.maxLength; });
+              } else {
+                result.entries = aEntries;
+              }
+
+              if (aListener) {
+                aListener.onSearchCompletion(result);
+              }
             }
+
+            this.getAutoCompleteValues(aInputName, searchString, processEntry);
         }
-
-        return result;
     },
 
-    getAutoCompleteValues : function (fieldName, searchString) {
-        let values = [];
-        let searchTokens;
+    stopAutoCompleteSearch : function () {
+        if (this._pendingQuery) {
+            this._pendingQuery.cancel();
+            this._pendingQuery = null;
+        }
+    },
 
+    /*
+     * Get the values for an autocomplete list given a search string.
+     *
+     *  fieldName - fieldname field within form history (the form input name)
+     *  searchString - string to search for
+     *  callback - called when the values are available. Passed an array of objects,
+     *             containing properties for each result. The callback is only called
+     *             when successful.
+     */
+    getAutoCompleteValues : function (fieldName, searchString, callback) {
         let params = {
             agedWeight:         this._agedWeight,
             bucketSize:         this._bucketSize,
             expiryDate:         1000 * (Date.now() - this._expireDays * 24 * 60 * 60 * 1000),
             fieldname:          fieldName,
             maxTimeGroupings:   this._maxTimeGroupings,
-            now:                Date.now() * 1000,          // convert from ms to microseconds
-            timeGroupingSize:   this._timeGroupingSize
+            timeGroupingSize:   this._timeGroupingSize,
+            prefixWeight:       this._prefixWeight,
+            boundaryWeight:     this._boundaryWeight
         }
 
-        // only do substring matching when more than one character is typed
-        let where = ""
-        let boundaryCalc = "";
-        if (searchString.length > 1) {
-            searchTokens = searchString.split(/\s+/);
-
-            // build up the word boundary and prefix match bonus calculation
-            boundaryCalc = "MAX(1, :prefixWeight * (value LIKE :valuePrefix ESCAPE '/') + (";
-            // for each word, calculate word boundary weights for the SELECT clause and
-            // add word to the WHERE clause of the query
-            let tokenCalc = [];
-            for (let i = 0; i < searchTokens.length; i++) {
-                tokenCalc.push("(value LIKE :tokenBegin" + i + " ESCAPE '/') + " +
-                                "(value LIKE :tokenBoundary" + i + " ESCAPE '/')");
-                where += "AND (value LIKE :tokenContains" + i + " ESCAPE '/') ";
-            }
-            // add more weight if we have a traditional prefix match and
-            // multiply boundary bonuses by boundary weight
-            boundaryCalc += tokenCalc.join(" + ") + ") * :boundaryWeight)";
-            params.prefixWeight = this._prefixWeight;
-            params.boundaryWeight = this._boundaryWeight;
-        } else if (searchString.length == 1) {
-            where = "AND (value LIKE :valuePrefix ESCAPE '/') ";
-            boundaryCalc = "1";
-        } else {
-            where = "";
-            boundaryCalc = "1";
-        }
-        /* Three factors in the frecency calculation for an entry (in order of use in calculation):
-         * 1) average number of times used - items used more are ranked higher
-         * 2) how recently it was last used - items used recently are ranked higher
-         * 3) additional weight for aged entries surviving expiry - these entries are relevant
-         *    since they have been used multiple times over a large time span so rank them higher
-         * The score is then divided by the bucket size and we round the result so that entries
-         * with a very similar frecency are bucketed together with an alphabetical sort. This is
-         * to reduce the amount of moving around by entries while typing.
-         */
-
-        let query = "/* do not warn (bug 496471): can't use an index */ " +
-                    "SELECT value, " +
-                    "ROUND( " +
-                        "timesUsed / MAX(1.0, (lastUsed - firstUsed) / :timeGroupingSize) * " +
-                        "MAX(1.0, :maxTimeGroupings - (:now - lastUsed) / :timeGroupingSize) * "+
-                        "MAX(1.0, :agedWeight * (firstUsed < :expiryDate)) / " +
-                        ":bucketSize "+
-                    ", 3) AS frecency, " +
-                    boundaryCalc + " AS boundaryBonuses " +
-                    "FROM moz_formhistory " +
-                    "WHERE fieldname=:fieldname " + where +
-                    "ORDER BY ROUND(frecency * boundaryBonuses) DESC, UPPER(value) ASC";
-
-        let stmt;
-        try {
-            stmt = this._dbCreateStatement(query, params);
+        this.stopAutoCompleteSearch();
 
-            // Chicken and egg problem: Need the statement to escape the params we
-            // pass to the function that gives us the statement. So, fix it up now.
-            if (searchString.length >= 1)
-                stmt.params.valuePrefix = stmt.escapeStringForLIKE(searchString, "/") + "%";
-            if (searchString.length > 1) {
-                for (let i = 0; i < searchTokens.length; i++) {
-                    let escapedToken = stmt.escapeStringForLIKE(searchTokens[i], "/");
-                    stmt.params["tokenBegin" + i] = escapedToken + "%";
-                    stmt.params["tokenBoundary" + i] =  "% " + escapedToken + "%";
-                    stmt.params["tokenContains" + i] = "%" + escapedToken + "%";
-                }
-            } else {
-                // no addional params need to be substituted into the query when the
-                // length is zero or one
-            }
-
-            while (stmt.executeStep()) {
-                let entry = {
-                    text:           stmt.row.value,
-                    textLowerCase:  stmt.row.value.toLowerCase(),
-                    frecency:       stmt.row.frecency,
-                    totalScore:     Math.round(stmt.row.frecency * stmt.row.boundaryBonuses)
-                }
-                values.push(entry);
-            }
+        let self = this;
+        let processResults = {
+          onSuccess: function(aResults) {
+            self._pendingQuery = null;
+            callback(aResults);
+          },
+          onFailure: function(aError) {
+            self.log("getAutocompleteValues failed: " + aError.message);
+            self._pendingQuery = null;
+          }
+        };
 
-        } catch (e) {
-            this.log("getValues failed: " + e.name + " : " + e.message);
-            throw "DB failed getting form autocomplete values";
-        } finally {
-            if (stmt) {
-                stmt.reset();
-            }
-        }
-
-        return values;
-    },
-
-
-    _dbStmts      : null,
-
-    _dbCreateStatement : function (query, params) {
-        let stmt = this._dbStmts[query];
-        // Memoize the statements
-        if (!stmt) {
-            this.log("Creating new statement for query: " + query);
-            stmt = this._formHistory.DBConnection.createStatement(query);
-            this._dbStmts[query] = stmt;
-        }
-        // Replace parameters, must be done 1 at a time
-        if (params) {
-            let stmtparams = stmt.params;
-            for (let i in params)
-                stmtparams[i] = params[i];
-        }
-        return stmt;
+        self._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
     },
 
     /*
      * _calculateScore
      *
      * entry    -- an nsIAutoCompleteResult entry
      * aSearchString -- current value of the input (lowercase)
      * searchTokens -- array of tokens of the search string
@@ -417,15 +373,18 @@ FormAutoCompleteResult.prototype = {
         return "";
     },
 
     removeValueAt : function (index, removeFromDB) {
         this._checkIndexBounds(index);
 
         let [removedEntry] = this.entries.splice(index, 1);
 
-        if (removeFromDB)
-            this.formHistory.removeEntry(this.fieldName, removedEntry.text);
+        if (removeFromDB) {
+          this.formHistory.update({ op: "remove",
+                                    fieldname: this.fieldName,
+                                    value: removedEntry.text });
+        }
     }
 };
 
 let component = [FormAutoComplete];
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -31,21 +31,22 @@
 #include "nsIDOMMouseEvent.h"
 #include "mozilla/ModuleUtils.h"
 #include "nsToolkitCompsCID.h"
 #include "nsEmbedCID.h"
 #include "nsIDOMNSEditableElement.h"
 #include "mozilla/dom/Element.h"
 #include "nsContentUtils.h"
 
-NS_IMPL_ISUPPORTS5(nsFormFillController,
+NS_IMPL_ISUPPORTS6(nsFormFillController,
                    nsIFormFillController,
                    nsIAutoCompleteInput,
                    nsIAutoCompleteSearch,
                    nsIDOMEventListener,
+                   nsIFormAutoCompleteObserver,
                    nsIMutationObserver)
 
 nsFormFillController::nsFormFillController() :
   mFocusedInput(nullptr),
   mFocusedInputNode(nullptr),
   mListNode(nullptr),
   mTimeout(50),
   mMinResultsForPopup(1),
@@ -595,71 +596,92 @@ nsFormFillController::StartSearch(const 
 
   // If the login manager has indicated it's responsible for this field, let it
   // handle the autocomplete. Otherwise, handle with form history.
   bool dummy;
   if (mPwmgrInputs.Get(mFocusedInputNode, &dummy)) {
     // XXX aPreviousResult shouldn't ever be a historyResult type, since we're not letting
     // satchel manage the field?
     rv = mLoginManager->AutoCompleteSearch(aSearchString,
-                                         aPreviousResult,
-                                         mFocusedInput,
-                                         getter_AddRefs(result));
+                                           aPreviousResult,
+                                           mFocusedInput,
+                                           getter_AddRefs(result));
+    NS_ENSURE_SUCCESS(rv, rv);
+    if (aListener) {
+      aListener->OnSearchResult(this, result);
+    }
   } else {
-    nsCOMPtr<nsIAutoCompleteResult> formHistoryResult;
+    mLastListener = aListener;
 
     // It appears that mFocusedInput is always null when we are focusing a XUL
     // element. Scary :)
     if (!mFocusedInput || nsContentUtils::IsAutocompleteEnabled(mFocusedInput)) {
       nsCOMPtr <nsIFormAutoComplete> formAutoComplete =
         do_GetService("@mozilla.org/satchel/form-autocomplete;1", &rv);
       NS_ENSURE_SUCCESS(rv, rv);
 
-      rv = formAutoComplete->AutoCompleteSearch(aSearchParam,
+      formAutoComplete->AutoCompleteSearchAsync(aSearchParam,
                                                 aSearchString,
                                                 mFocusedInput,
                                                 aPreviousResult,
-                                                getter_AddRefs(formHistoryResult));
+                                                this);
+      mLastFormAutoComplete = formAutoComplete;
+    } else {
+      mLastSearchString = aSearchString;
 
-      NS_ENSURE_SUCCESS(rv, rv);
+      // Even if autocomplete is disabled, handle the inputlist anyway as that was
+      // specifically requested by the page. This is so a field can have the default
+      // autocomplete disabled and replaced with a custom inputlist autocomplete.
+      return PerformInputListAutoComplete(aPreviousResult);
     }
+  }
 
-    mLastSearchResult = formHistoryResult;
-    mLastListener = aListener;
-    mLastSearchString = aSearchString;
+  return NS_OK;
+}
 
-    nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete =
-      do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv);
-    NS_ENSURE_SUCCESS(rv, rv);
+nsresult
+nsFormFillController::PerformInputListAutoComplete(nsIAutoCompleteResult* aPreviousResult)
+{
+  // If an <input> is focused, check if it has a list="<datalist>" which can
+  // provide the list of suggestions.
+
+  nsresult rv;
+  nsCOMPtr<nsIAutoCompleteResult> result;
 
-    rv = inputListAutoComplete->AutoCompleteSearch(formHistoryResult,
-                                                   aSearchString,
-                                                   mFocusedInput,
-                                                   getter_AddRefs(result));
-
-    if (mFocusedInput) {
-      nsCOMPtr<nsIDOMHTMLElement> list;
-      mFocusedInput->GetList(getter_AddRefs(list));
+  nsCOMPtr <nsIInputListAutoComplete> inputListAutoComplete =
+    do_GetService("@mozilla.org/satchel/inputlist-autocomplete;1", &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = inputListAutoComplete->AutoCompleteSearch(aPreviousResult,
+                                                 mLastSearchString,
+                                                 mFocusedInput,
+                                                 getter_AddRefs(result));
+  NS_ENSURE_SUCCESS(rv, rv);
 
-      nsCOMPtr<nsINode> node = do_QueryInterface(list);
-      if (mListNode != node) {
-        if (mListNode) {
-          mListNode->RemoveMutationObserver(this);
-          mListNode = nullptr;
-        }
-        if (node) {
-          node->AddMutationObserverUnlessExists(this);
-          mListNode = node;
-        }
+  if (mFocusedInput) {
+    nsCOMPtr<nsIDOMHTMLElement> list;
+    mFocusedInput->GetList(getter_AddRefs(list));
+
+    // Add a mutation observer to check for changes to the items in the <datalist>
+    // and update the suggestions accordingly.
+    nsCOMPtr<nsINode> node = do_QueryInterface(list);
+    if (mListNode != node) {
+      if (mListNode) {
+        mListNode->RemoveMutationObserver(this);
+        mListNode = nullptr;
+      }
+      if (node) {
+        node->AddMutationObserverUnlessExists(this);
+        mListNode = node;
       }
     }
   }
-  NS_ENSURE_SUCCESS(rv, rv);
 
-  aListener->OnSearchResult(this, result);
+  if (mLastListener) {
+    mLastListener->OnSearchResult(this, result);
+  }
 
   return NS_OK;
 }
 
 class UpdateSearchResultRunnable : public nsRunnable
 {
 public:
   UpdateSearchResultRunnable(nsIAutoCompleteObserver* aObserver,
@@ -702,20 +724,42 @@ void nsFormFillController::RevalidateDat
   nsCOMPtr<nsIRunnable> event =
     new UpdateSearchResultRunnable(mLastListener, this, result);
   NS_DispatchToCurrentThread(event);
 }
 
 NS_IMETHODIMP
 nsFormFillController::StopSearch()
 {
+  // Make sure to stop and clear this, otherwise the controller will prevent
+  // mLastFormAutoComplete from being deleted.
+  if (mLastFormAutoComplete) {
+    mLastFormAutoComplete->StopAutoCompleteSearch();
+    mLastFormAutoComplete = nullptr;
+  }
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////
+//// nsIFormAutoCompleteObserver
+
+NS_IMETHODIMP
+nsFormFillController::OnSearchCompletion(nsIAutoCompleteResult *aResult)
+{
+  nsCOMPtr<nsIAutoCompleteResult> resultParam = do_QueryInterface(aResult);
+
+  nsAutoString searchString;
+  resultParam->GetSearchString(searchString);
+  mLastSearchResult = aResult;
+  mLastSearchString = searchString;
+
+  return PerformInputListAutoComplete(resultParam);
+}
+
+////////////////////////////////////////////////////////////////////////
 //// nsIDOMEventListener
 
 NS_IMETHODIMP
 nsFormFillController::HandleEvent(nsIDOMEvent* aEvent)
 {
   nsAutoString type;
   aEvent->GetType(type);
 
@@ -1173,9 +1217,8 @@ static const mozilla::Module::ContractID
 
 static const mozilla::Module kSatchelModule = {
   mozilla::Module::kVersion,
   kSatchelCIDs,
   kSatchelContracts
 };
 
 NSMODULE_DEFN(satchel) = &kSatchelModule;
-
--- a/toolkit/components/satchel/nsFormFillController.h
+++ b/toolkit/components/satchel/nsFormFillController.h
@@ -6,16 +6,17 @@
 #ifndef __nsFormFillController__
 #define __nsFormFillController__
 
 #include "nsIFormFillController.h"
 #include "nsIAutoCompleteInput.h"
 #include "nsIAutoCompleteSearch.h"
 #include "nsIAutoCompleteController.h"
 #include "nsIAutoCompletePopup.h"
+#include "nsIFormAutoComplete.h"
 #include "nsIDOMEventListener.h"
 #include "nsCOMPtr.h"
 #include "nsDataHashtable.h"
 #include "nsIDocShell.h"
 #include "nsIDOMWindow.h"
 #include "nsIDOMHTMLInputElement.h"
 #include "nsILoginManager.h"
 #include "nsIMutationObserver.h"
@@ -28,23 +29,25 @@
 
 class nsFormHistory;
 class nsINode;
 
 class nsFormFillController : public nsIFormFillController,
                              public nsIAutoCompleteInput,
                              public nsIAutoCompleteSearch,
                              public nsIDOMEventListener,
+                             public nsIFormAutoCompleteObserver,
                              public nsIMutationObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIFORMFILLCONTROLLER
   NS_DECL_NSIAUTOCOMPLETESEARCH
   NS_DECL_NSIAUTOCOMPLETEINPUT
+  NS_DECL_NSIFORMAUTOCOMPLETEOBSERVER
   NS_DECL_NSIDOMEVENTLISTENER
   NS_DECL_NSIMUTATIONOBSERVER
 
   nsresult Focus(nsIDOMEvent* aEvent);
   nsresult KeyPress(nsIDOMEvent* aKeyEvent);
   nsresult MouseDown(nsIDOMEvent* aMouseEvent);
 
   nsFormFillController();
@@ -55,16 +58,18 @@ protected:
   void RemoveWindowListeners(nsIDOMWindow *aWindow);
 
   void AddKeyListener(nsIDOMHTMLInputElement *aInput);
   void RemoveKeyListener();
 
   void StartControllingInput(nsIDOMHTMLInputElement *aInput);
   void StopControllingInput();
 
+  nsresult PerformInputListAutoComplete(nsIAutoCompleteResult* aPreviousResult);
+
   void RevalidateDataList();
   bool RowMatch(nsFormHistory *aHistory, uint32_t aIndex, const nsAString &aInputName, const nsAString &aInputValue);
 
   inline nsIDocShell *GetDocShellForInput(nsIDOMHTMLInputElement *aInput);
   inline nsIDOMWindow *GetWindowForDocShell(nsIDocShell *aDocShell);
   inline int32_t GetIndexOfDocShell(nsIDocShell *aDocShell);
 
   void MaybeRemoveMutationObserver(nsINode* aNode);
@@ -74,25 +79,34 @@ protected:
                                                      void* aUserData);
   bool IsEventTrusted(nsIDOMEvent *aEvent);
   // members //////////////////////////////////////////
 
   nsCOMPtr<nsIAutoCompleteController> mController;
   nsCOMPtr<nsILoginManager> mLoginManager;
   nsIDOMHTMLInputElement* mFocusedInput;
   nsINode* mFocusedInputNode;
+
+  // mListNode is a <datalist> element which, is set, has the form fill controller
+  // as a mutation observer for it.
   nsINode* mListNode;
   nsCOMPtr<nsIAutoCompletePopup> mFocusedPopup;
 
   nsTArray<nsCOMPtr<nsIDocShell> > mDocShells;
   nsTArray<nsCOMPtr<nsIAutoCompletePopup> > mPopups;
 
   //these are used to dynamically update the autocomplete
   nsCOMPtr<nsIAutoCompleteResult> mLastSearchResult;
+
+  // The observer passed to StartSearch. It will be notified when the search is
+  // complete or the data from a datalist changes.
   nsCOMPtr<nsIAutoCompleteObserver> mLastListener;
+
+  // This is cleared by StopSearch().
+  nsCOMPtr<nsIFormAutoComplete> mLastFormAutoComplete;
   nsString mLastSearchString;
 
   nsDataHashtable<nsPtrHashKey<const nsINode>, bool> mPwmgrInputs;
 
   uint32_t mTimeout;
   uint32_t mMinResultsForPopup;
   uint32_t mMaxRows;
   bool mDisableAutoComplete;
--- a/toolkit/components/satchel/nsIFormAutoComplete.idl
+++ b/toolkit/components/satchel/nsIFormAutoComplete.idl
@@ -1,22 +1,52 @@
 /* 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 "nsISupports.idl"
 
 interface nsIAutoCompleteResult;
+interface nsIFormAutoCompleteObserver;
 interface nsIDOMHTMLInputElement;
 
-[scriptable, uuid(997c0c05-5d1d-47e5-9cbc-765c0b8ec699)]
+[scriptable, uuid(c079f18f-40ab-409d-800e-878889b83b58)]
 
 interface nsIFormAutoComplete: nsISupports {
+
     /**
-     * Generate results for a form input autocomplete menu.
+     * Generate results for a form input autocomplete menu synchronously.
+     * This method is deprecated in favour of autoCompleteSearchAsync.
+     */
+    nsIAutoCompleteResult autoCompleteSearch(in AString aInputName,
+                                             in AString aSearchString,
+                                             in nsIDOMHTMLInputElement aField,
+                                             in nsIAutoCompleteResult aPreviousResult);
+
+    /**
+     * Generate results for a form input autocomplete menu asynchronously.
      */
-    nsIAutoCompleteResult autoCompleteSearch(
-                                    in AString aInputName,
-                                    in AString aSearchString,
-                                    in nsIDOMHTMLInputElement aField,
-                                    in nsIAutoCompleteResult aPreviousResult);
+    void autoCompleteSearchAsync(in AString aInputName,
+                                 in AString aSearchString,
+                                 in nsIDOMHTMLInputElement aField,
+                                 in nsIAutoCompleteResult aPreviousResult,
+                                 in nsIFormAutoCompleteObserver aListener);
+
+    /**
+     * If a search is in progress, stop it. Otherwise, do nothing. This is used
+     * to cancel an existing search, for example, in preparation for a new search.
+     */
+    void stopAutoCompleteSearch();
 };
+
+[scriptable, function, uuid(604419ab-55a0-4831-9eca-1b9e67cc4751)]
+interface nsIFormAutoCompleteObserver : nsISupports
+{
+  /*
+   * Called when a search is complete and the results are ready even if the
+   * result set is empty. If the search is cancelled or a new search is
+   * started, this is not called.
+   *
+   * @param result - The search result object
+   */
+  void onSearchCompletion(in nsIAutoCompleteResult result);
+};