Merging places and mozilla-central.
authorShawn Wilsher <sdwilsh@shawnwilsher.com>
Thu, 30 Jul 2009 08:41:55 -0700
changeset 26278 90efe188b7b1f2d8921d2be15c90c9a68dfe997b
parent 26277 b4a38c411ca8315574ff5847e40a5be448f9ab13 (diff)
parent 26272 5964c28094fa4f2038cb751ae33f65b583957908 (current diff)
child 26279 b307a643012f62992fe9cd72f1dc9db8961481b3
push id1834
push userclokep@gmail.com
push dateMon, 23 Jan 2017 21:48:40 +0000
treeherdercomm-beta@293cffe83e59 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
Merging places and mozilla-central.
new file mode 100644
--- /dev/null
+++ b/suite/common/places/src/nsPlacesAutoComplete.js
@@ -0,0 +1,1051 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+// This is just a helper for the next constant.
+function book_tag_sql_fragment(aName, aColumn, aForTag)
+{
+  return ["(",
+    "SELECT ", aColumn, " ",
+    "FROM moz_bookmarks b ",
+    "JOIN moz_bookmarks t ",
+      "ON t.id = b.parent ",
+      "AND t.parent ", (aForTag ? "" : "!"), "= :parent ",
+    "WHERE b.type = ", Ci.nsINavBookmarksService.TYPE_BOOKMARK, " ",
+      "AND b.fk = h.id ",
+    (aForTag ? "AND LENGTH(t.title) > 0" :
+               "ORDER BY b.lastModified DESC LIMIT 1"),
+  ") AS ", aName].join("");
+}
+
+// This SQL query fragment provides the following:
+//   - the parent folder for bookmarked entries (kQueryIndexParent)
+//   - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle)
+//   - the tags associated with a bookmarked entry (kQueryIndexTags)
+const kBookTagSQLFragment =
+  book_tag_sql_fragment("parent", "b.parent", false) + ", " +
+  book_tag_sql_fragment("bookmark", "b.title", false) + ", " +
+  book_tag_sql_fragment("tags", "GROUP_CONCAT(t.title, ',')", true);
+
+// observer topics
+const kQuitApplication = "quit-application";
+const kPrefChanged = "nsPref:changed";
+
+// Match type constants.  These indicate what type of search function we should
+// be using.
+const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
+const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
+const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
+const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
+
+// AutoComplete index constants.  All AutoComplete queries will provide these
+// columns in this order.
+const kQueryIndexURL = 0;
+const kQueryIndexTitle = 1;
+const kQueryIndexFaviconURL = 2;
+const kQueryIndexParentId = 3;
+const kQueryIndexBookmarkTitle = 4;
+const kQueryIndexTags = 5;
+const kQueryIndexVisitCount = 6;
+const kQueryIndexTyped = 7;
+const kQueryIndexPlaceId = 8;
+const kQueryIndexQueryType = 9;
+
+// AutoComplete query type constants.  Describes the various types of queries
+// that we can process.
+const kQueryTypeKeyword = 0;
+const kQueryTypeFiltered = 1;
+
+// This separator is used as an RTL-friendly way to split the title and tags.
+// It can also be used by an nsIAutoCompleteResult consumer to re-split the
+// "comment" back into the title and the tag.
+const kTitleTagsSeparator = " \u2013 ";
+
+const kBrowserUrlbarBranch = "browser.urlbar.";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Global Functions
+
+/**
+ * Generates the SQL subquery to get the best favicon for a given revhost.  This
+ * is the favicon for the most recent visit.
+ *
+ * @param aTableName
+ *        The table to join to the moz_favicons table with.  This must have a
+ *        column called favicon_id.
+ * @return the SQL subquery (in string form) to get the best favicon.
+ */
+function best_favicon_for_revhost(aTableName)
+{
+  return "(" +
+    "SELECT f.url " +
+    "FROM " + aTableName + " " +
+    "JOIN moz_favicons f ON f.id = favicon_id " +
+    "WHERE rev_host = IFNULL( " +
+      "(SELECT rev_host FROM moz_places_temp WHERE id = b.fk), " +
+      "(SELECT rev_host FROM moz_places WHERE id = b.fk) " +
+    ") " +
+    "ORDER BY frecency DESC " +
+    "LIMIT 1 " +
+  ")";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// AutoCompleteStatementCallbackWrapper class
+
+/**
+ * Wraps a callback and ensures that handleCompletion is not dispatched if the
+ * query is no longer tracked.
+ *
+ * @param aCallback
+ *        A reference to a nsPlacesAutoComplete.
+ * @param aDBConnection
+ *        The database connection to execute the queries on.
+ */
+function AutoCompleteStatementCallbackWrapper(aCallback,
+                                              aDBConnection)
+{
+  this._callback = aCallback;
+  this._db = aDBConnection;
+}
+
+AutoCompleteStatementCallbackWrapper.prototype = {
+  //////////////////////////////////////////////////////////////////////////////
+  //// mozIStorageStatementCallback
+
+  handleResult: function ACSCW_handleResult(aResultSet)
+  {
+    this._callback.handleResult.apply(this._callback, arguments);
+  },
+
+  handleError: function ACSCW_handleError(aError)
+  {
+    this._callback.handleError.apply(this._callback, arguments);
+  },
+
+  handleCompletion: function ACSCW_handleCompletion(aReason)
+  {
+    // Only dispatch handleCompletion if we are not done searching and are a
+    // pending search.
+    let callback = this._callback;
+    if (!callback.isSearchComplete() && callback.isPendingSearch(this._handle))
+      callback.handleCompletion.apply(callback, arguments);
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// AutoCompleteStatementCallbackWrapper
+
+  /**
+   * Executes the specified query asynchronously.  This object will notify
+   * this._callback if we should notify (logic explained in handleCompletion).
+   *
+   * @param aQueries
+   *        The queries to execute asynchronously.
+   * @return a mozIStoragePendingStatement that can be used to cancel the
+   *         queries.
+   */
+  executeAsync: function ACSCW_executeAsync(aQueries)
+  {
+    return this._handle = this._db.executeAsync(aQueries, aQueries.length,
+                                                this);
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsISupports
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.mozIStorageStatementCallback,
+  ])
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsPlacesAutoComplete class
+
+function nsPlacesAutoComplete()
+{
+  //////////////////////////////////////////////////////////////////////////////
+  //// Shared Constants for Smart Getters
+
+  // Define common pieces of various queries.
+  // TODO bug 412736 in case of a frecency tie, break it with h.typed and
+  // h.visit_count which is better than nothing.  This is slow, so not doing it
+  // yet...
+  // Note: h.frecency is only selected because we need it for ordering.
+  function sql_base_fragment(aTableName) {
+    return "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " +
+                  "h.visit_count, h.typed, h.id, :query_type, h.frecency " +
+           "FROM " + aTableName + " h " +
+           "LEFT OUTER JOIN moz_favicons f ON f.id = h.favicon_id " +
+           "WHERE h.frecency <> 0 " +
+           "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " +
+                                  "IFNULL(bookmark, h.title), tags, " +
+                                  "h.visit_count, h.typed, parent, " +
+                                  ":matchBehavior, :searchBehavior) " +
+          "{ADDITIONAL_CONDITIONS} ";
+  }
+  const SQL_BASE = sql_base_fragment("moz_places_temp") +
+                   "UNION ALL " +
+                   sql_base_fragment("moz_places") +
+                   "AND +h.id NOT IN (SELECT id FROM moz_places_temp) " +
+                   "ORDER BY h.frecency DESC " +
+                   "LIMIT :maxResults";
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// Smart Getters
+
+  this.__defineGetter__("_db", function() {
+    delete this._db;
+    return this._db = Cc["@mozilla.org/browser/nav-history-service;1"].
+                      getService(Ci.nsPIPlacesDatabase).
+                      DBConnection;
+  });
+
+  this.__defineGetter__("_bh", function() {
+    delete this._bh;
+    return this._bh = Cc["@mozilla.org/browser/global-history;2"].
+                      getService(Ci.nsIBrowserHistory);
+  });
+
+  this.__defineGetter__("_textURIService", function() {
+    delete this._textURIService;
+    return this._textURIService = Cc["@mozilla.org/intl/texttosuburi;1"].
+                                  getService(Ci.nsITextToSubURI);
+  });
+
+  this.__defineGetter__("_bs", function() {
+    delete this._bs;
+    return this._bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+                      getService(Ci.nsINavBookmarksService);
+  });
+
+  this.__defineGetter__("_ioService", function() {
+    delete this._ioService;
+    return this._ioService = Cc["@mozilla.org/network/io-service;1"].
+                             getService(Ci.nsIIOService);
+  });
+
+  this.__defineGetter__("_faviconService", function() {
+    delete this._faviconService;
+    return this._faviconService = Cc["@mozilla.org/browser/favicon-service;1"].
+                                  getService(Ci.nsIFaviconService);
+  });
+
+  this.__defineGetter__("_defaultQuery", function() {
+    delete this._defaultQuery;
+    let replacementText = "";
+    return this._defaultQuery = this._db.createStatement(
+      SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
+    );
+  });
+
+  this.__defineGetter__("_historyQuery", function() {
+    delete this._historyQuery;
+    let replacementText = "AND h.visit_count > 0";
+    return this._historyQuery = this._db.createStatement(
+      SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
+    );
+  });
+
+  this.__defineGetter__("_bookmarkQuery", function() {
+    delete this._bookmarkQuery;
+    let replacementText = "AND bookmark IS NOT NULL";
+    return this._bookmarkQuery = this._db.createStatement(
+      SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
+    );
+  });
+
+  this.__defineGetter__("_tagsQuery", function() {
+    delete this._tagsQuery;
+    let replacementText = "AND tags IS NOT NULL";
+    return this._tagsQuery = this._db.createStatement(
+      SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
+    );
+  });
+
+  this.__defineGetter__("_typedQuery", function() {
+    delete this._typedQuery;
+    let replacementText = "AND h.typed = 1";
+    return this._typedQuery = this._db.createStatement(
+      SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
+    );
+  });
+
+  this.__defineGetter__("_adaptiveQuery", function() {
+    delete this._adaptiveQuery;
+    // In this query, we are taking kBookTagSQLFragment only for h.id because it
+    // uses data from the moz_bookmarks table and we sync tables on bookmark
+    // insert.  So, most likely, h.id will always be populated when we have any
+    // bookmark.  We still need to join on moz_places_temp for other data (eg.
+    // title).
+    return this._adaptiveQuery = this._db.createStatement(
+      "/* do not warn (bug 487789) */ " +
+      "SELECT IFNULL(h_t.url, h.url), IFNULL(h_t.title, h.title), f.url, " +
+              kBookTagSQLFragment + ", IFNULL(h_t.visit_count, h.visit_count), " +
+              "IFNULL(h_t.typed, h.typed), IFNULL(h_t.id, h.id), " +
+              ":query_type, rank " +
+      "FROM ( " +
+        "SELECT ROUND(MAX(((i.input = :search_string) + " +
+                          "(SUBSTR(i.input, 1, LENGTH(:search_string)) = :search_string)) * " +
+                          "i.use_count), 1) AS rank, place_id " +
+        "FROM moz_inputhistory i " +
+        "GROUP BY i.place_id " +
+        "HAVING rank > 0 " +
+      ") AS i " +
+      "LEFT JOIN moz_places h ON h.id = i.place_id " +
+      "LEFT JOIN moz_places_temp h_t ON h_t.id = i.place_id " +
+      "LEFT JOIN moz_favicons f ON f.id = IFNULL(h_t.favicon_id, h.favicon_id) "+
+      "WHERE IFNULL(h_t.url, h.url) NOTNULL " +
+      "ORDER BY rank DESC, IFNULL(h_t.frecency, h.frecency) DESC"
+    );
+  });
+
+  this.__defineGetter__("_keywordQuery", function() {
+    delete this._keywordQuery;
+    return this._keywordQuery = this._db.createStatement(
+      "/* do not warn (bug 487787) */ " +
+      "SELECT IFNULL( " +
+          "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places_temp WHERE id = b.fk), " +
+          "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) " +
+        ") AS search_url, IFNULL(h_t.title, h.title), " +
+        "COALESCE(f.url, " + best_favicon_for_revhost("moz_places_temp") + "," +
+                  best_favicon_for_revhost("moz_places") + "), b.parent, " +
+        "b.title, NULL, IFNULL(h_t.visit_count, h.visit_count), " +
+        "IFNULL(h_t.typed, h.typed), COALESCE(h_t.id, h.id, b.fk), " +
+        ":query_type " +
+      "FROM moz_keywords k " +
+      "JOIN moz_bookmarks b ON b.keyword_id = k.id " +
+      "LEFT JOIN moz_places AS h ON h.url = search_url " +
+      "LEFT JOIN moz_places_temp AS h_t ON h_t.url = search_url " +
+      "LEFT JOIN moz_favicons f ON f.id = IFNULL(h_t.favicon_id, h.favicon_id) " +
+      "WHERE LOWER(k.keyword) = LOWER(:keyword) " +
+      "ORDER BY IFNULL(h_t.frecency, h.frecency) DESC"
+    );
+  });
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// Initialization
+
+  // load preferences
+  this._loadPrefs(true);
+
+  // register observers
+  this._os = Cc["@mozilla.org/observer-service;1"].
+              getService(Ci.nsIObserverService);
+  this._os.addObserver(this, kQuitApplication, false);
+
+}
+
+nsPlacesAutoComplete.prototype = {
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIAutoCompleteSearch
+
+  startSearch: function PAC_startSearch(aSearchString, aSearchParam,
+                                        aPreviousResult, aListener)
+  {
+    // Note: We don't use aPreviousResult to make sure ordering of results are
+    //       consistent.  See bug 412730 for more details.
+
+    // We want to store the original string with no leading or trailing
+    // whitespace for case sensitive searches.
+    this._originalSearchString = aSearchString.trim();
+
+    this._currentSearchString =
+      this._fixupSearchText(this._originalSearchString.toLowerCase());
+
+    this._listener = aListener;
+    let result = Cc["@mozilla.org/autocomplete/simple-result;1"].
+                 createInstance(Ci.nsIAutoCompleteSimpleResult);
+    result.setSearchString(aSearchString);
+    result.setListener(this);
+    this._result = result;
+
+    // If we are not enabled, we need to return now.
+    if (!this._enabled) {
+      this._finishSearch(true);
+      return;
+    }
+
+    // Reset our search behavior to the default.
+    this._behavior = this._defaultBehavior;
+
+    // If we have no search terms, this is a special search that should only
+    // look for BEHAVIOR_HISTORY and BEHAVIOR_TYPED.
+    if (!this._currentSearchString) {
+      this._setBehavior("history");
+      this._setBehavior("typed");
+    }
+
+    // For any given search, we run up to three queries:
+    // 1) keywords (this._keywordQuery)
+    // 2) adaptive learning (this._adaptiveQuery)
+    // 3) query from this._getSearch
+    // We always run (2) and (3), but (1) only gets ran if we get any filtered
+    // tokens from this._getSearch (if there are no tokens, there is nothing to
+    // match, so there is no reason to run the query).
+    let {query, tokens} =
+      this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString));
+    let queries = tokens.length ?
+      [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery(), query] :
+      [this._getBoundAdaptiveQuery(), query];
+
+    // Start executing our queries.
+    this._executeQueries(queries);
+
+    // Set up our persistent state for the duration of the search.
+    this._searchTokens = tokens;
+    this._usedPlaceIds = {};
+  },
+
+  stopSearch: function PAC_stopSearch()
+  {
+    // We need to cancel our searches so we do not get any [more] results.
+    this._stopActiveQuery();
+
+    this._finishSearch(false);
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIAutoCompleteSimpleResultListener
+
+  onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB)
+  {
+    if (aRemoveFromDB)
+      this._bh.removePage(this._ioService.newURI(aURISpec, null, null));
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// mozIStorageStatementCallback
+
+  handleResult: function PAC_handleResult(aResultSet)
+  {
+    let row, haveMatches = false;
+    while (row = aResultSet.getNextRow()) {
+      let match = this._processRow(row);
+      haveMatches = haveMatches || match;
+
+      if (this._result.matchCount == this._maxRichResults) {
+        // We have enough results, so stop running our search.
+        this._stopActiveQuery();
+
+        // And finish our search.
+        this._finishSearch(true);
+        return;
+      }
+
+    }
+
+    // Notify about results if we've gotten them.
+    if (haveMatches)
+      this._notifyResults(true);
+  },
+
+  handleError: function PAC_handleError(aError)
+  {
+    Components.utils.reportError("Places AutoComplete: " + aError);
+  },
+
+  handleCompletion: function PAC_handleCompletion(aReason)
+  {
+    // If we have already finished our search, we should bail out early.
+    if (this.isSearchComplete())
+      return;
+
+    // If we do not have enough results, and our match type is
+    // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
+    // results.
+    if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
+        this._result.matchCount < this._maxRichResults && !this._secondPass) {
+      this._secondPass = true;
+      let query = this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens);
+      this._executeQueries([query]);
+      return;
+    }
+
+    this._finishSearch(true);
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIObserver
+
+  observe: function PAC_observe(aSubject, aTopic, aData)
+  {
+    if (aTopic == kQuitApplication) {
+      this._os.removeObserver(this, kQuitApplication);
+
+      // Remove our preference observer.
+      let prefs = Cc["@mozilla.org/preferences-service;1"].
+                  getService(Ci.nsIPrefService).
+                  getBranch(kBrowserUrlbarBranch).
+                  QueryInterface(Ci.nsIPrefBranch2);
+      prefs.removeObserver("", this);
+
+      // Finalize the statements that we have used.
+      let stmts = [
+        "_defaultQuery",
+        "_historyQuery",
+        "_bookmarkQuery",
+        "_tagsQuery",
+        "_typedQuery",
+        "_adaptiveQuery",
+        "_keywordQuery",
+      ];
+      for (let i = 0; i < stmts.length; i++) {
+        // We do not want to create any query we haven't already created, so
+        // see if it is a getter first.  __lookupGetter__ returns null if it is
+        // actually a statement.
+        if (!this.__lookupGetter__(stmts[i]))
+          this[stmts[i]].finalize();
+      }
+    }
+    else if (aTopic == kPrefChanged) {
+      this._loadPrefs();
+    }
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsPlacesAutoComplete
+
+  /**
+   * Used to unescape encoded URI strings, and drop information that we do not
+   * care about for searching.
+   *
+   * @param aURIString
+   *        The text to unescape and modify.
+   * @return the modified uri.
+   */
+  _fixupSearchText: function PAC_fixupSearchText(aURIString)
+  {
+    let uri = aURIString;
+
+    if (uri.indexOf("http://") == 0)
+      uri = uri.slice(7);
+    else if (uri.indexOf("https://") == 0)
+      uri = uri.slice(8);
+    else if (uri.indexOf("ftp://") == 0)
+      uri = uri.slice(6);
+
+    return this._textURIService.unEscapeURIForUI("UTF-8", uri);
+  },
+
+  /**
+   * Generates the tokens used in searching from a given string.
+   *
+   * @param aSearchString
+   *        The string to generate tokens from.
+   * @return an array of tokens.
+   */
+  _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString)
+  {
+    // Calling split on an empty string will return an array containing one
+    // empty string.  We don't want that, as it'll break our logic, so return an
+    // empty array then.
+    return aSearchString.length ? aSearchString.split(" ") : [];
+  },
+
+  /**
+   * Properly cleans up when searching is completed.
+   *
+   * @param aNotify
+   *        Indicates if we should notify the AutoComplete listener about our
+   *        results or not.
+   */
+  _finishSearch: function PAC_finishSearch(aNotify)
+  {
+    // Notify about results if we are supposed to.
+    if (aNotify)
+      this._notifyResults(false);
+
+    // Clear our state
+    delete this._originalSearchString;
+    delete this._currentSearchString;
+    delete this._searchTokens;
+    delete this._listener;
+    delete this._result;
+    delete this._usedPlaceIds;
+    delete this._pendingQuery;
+    this._secondPass = false;
+  },
+
+  /**
+   * Executes the given queries asynchronously.
+   *
+   * @param aQueries
+   *        The queries to execute.
+   */
+  _executeQueries: function PAC_executeQueries(aQueries)
+  {
+    // Because we might get a handleCompletion for canceled queries, we want to
+    // filter out queries we no longer care about (described in the
+    // handleCompletion implementation of AutoCompleteStatementCallbackWrapper).
+
+    // Create our wrapper object and execute the queries.
+    let wrapper = new AutoCompleteStatementCallbackWrapper(this, this._db);
+    this._pendingQuery = wrapper.executeAsync(aQueries);
+  },
+
+  /**
+   * Stops executing our active query.
+   */
+  _stopActiveQuery: function PAC_stopActiveQuery()
+  {
+    this._pendingQuery.cancel();
+    delete this._pendingQuery;
+  },
+
+  /**
+   * Notifies the listener about results.
+   *
+   * @param aSearchOngoing
+   *        Indicates if the search is ongoing or not.
+   */
+  _notifyResults: function PAC_notifyResults(aSearchOngoing)
+  {
+    let result = this._result;
+    let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+    if (aSearchOngoing)
+      resultCode += "_ONGOING";
+    result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+    result.setDefaultIndex(result.matchCount ? 0 : -1);
+    this._listener.onSearchResult(this, result);
+  },
+
+  /**
+   * Loads the preferences that we care about.
+   *
+   * @param [optional] aRegisterObserver
+   *        Indicates if the preference observer should be added or not.  The
+   *        default value is false.
+   */
+  _loadPrefs: function PAC_loadPrefs(aRegisterObserver)
+  {
+    let prefs = Cc["@mozilla.org/preferences-service;1"].
+                getService(Ci.nsIPrefService).
+                getBranch(kBrowserUrlbarBranch);
+    function safeGetter(aName, aDefault) {
+      let types = {
+        boolean: "Bool",
+        number: "Int",
+        string: "Char"
+      };
+      let type = types[typeof(aDefault)];
+      if (!type)
+        throw "Unknown type!";
+
+      // If the pref isn't set, we want to use the default.
+      try {
+        return prefs["get" + type + "Pref"](aName);
+      }
+      catch (e) {
+        return aDefault;
+      }
+    }
+
+    this._enabled = safeGetter("autocomplete.enabled", true);
+    this._matchBehavior = safeGetter("matchBehavior", MATCH_BOUNDARY_ANYWHERE);
+    this._filterJavaScript = safeGetter("filter.javascript", true);
+    this._maxRichResults = safeGetter("maxRichResults", 25);
+    this._restrictHistoryToken = safeGetter("restrict.history", "^");
+    this._restrictBookmarkToken = safeGetter("restrict.bookmark", "*");
+    this._restrictTypedToken = safeGetter("restrict.typed", "~");
+    this._restrictTagToken = safeGetter("restrct.tag", "+");
+    this._matchTitleToken = safeGetter("match.title", "#");
+    this._matchURLToken = safeGetter("match.url", "@");
+    this._defaultBehavior = safeGetter("default.behavior", 0);
+
+    // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
+    if (this._matchBehavior != MATCH_ANYWHERE &&
+        this._matchBehavior != MATCH_BOUNDARY &&
+        this._matchBehavior != MATCH_BEGINNING)
+      this._matchBehavior = MATCH_BOUNDARY_ANYWHERE;
+
+    // register observer
+    if (aRegisterObserver) {
+      let pb = prefs.QueryInterface(Ci.nsIPrefBranch2);
+      pb.addObserver("", this, false);
+    }
+  },
+
+  /**
+   * Given an array of tokens, this function determines which query should be
+   * ran.  It also removes any special search tokens.
+   *
+   * @param aTokens
+   *        An array of search tokens.
+   * @return an object with two properties:
+   *         query: the correctly optimized, bound query to search the database
+   *                with.
+   *         tokens: the filtered list of tokens to search with.
+   */
+  _getSearch: function PAC_getSearch(aTokens)
+  {
+    // Set the proper behavior so our call to _getBoundSearchQuery gives us the
+    // correct query.
+    for (let i = aTokens.length - 1; i >= 0; i--) {
+      switch (aTokens[i]) {
+        case this._restrictHistoryToken:
+          this._setBehavior("history");
+          break;
+        case this._restrictBookmarkToken:
+          this._setBehavior("bookmark");
+          break;
+        case this._restrictTagToken:
+          this._setBehavior("tag");
+          break;
+        case this._matchTitleToken:
+          this._setBehavior("title");
+          break;
+        case this._matchURLToken:
+          this._setBehavior("url");
+          break;
+        case this._restrictTypedToken:
+          this._setBehavior("typed");
+          break;
+        default:
+          // We do not want to remove the token if we did not match.
+          continue;
+      };
+
+      aTokens.splice(i, 1);
+    }
+
+    // Set the right JavaScript behavior based on our preference.  Note that the
+    // preference is whether or not we should filter JavaScript, and the
+    // behavior is if we should search it or not.
+    if (!this._filterJavaScript)
+      this._setBehavior("javascript");
+
+    return {
+      query: this._getBoundSearchQuery(this._matchBehavior, aTokens),
+      tokens: aTokens
+    };
+  },
+
+  /**
+   * Obtains the search query to be used based on the previously set search
+   * behaviors (accessed by this._hasBehavior).  The query is bound and ready to
+   * execute.
+   *
+   * @param aMatchBehavior
+   *        How this query should match its tokens to the search string.
+   * @param aTokens
+   *        An array of search tokens.
+   * @return the correctly optimized query to search the database with and the
+   *         new list of tokens to search with.  The query has all the needed
+   *         parameters bound, so consumers can execute it without doing any
+   *         additional work.
+   */
+  _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior,
+                                                         aTokens)
+  {
+    // We use more optimized queries for restricted searches, so we will always
+    // return the most restrictive one to the least restrictive one if more than
+    // one token is found.
+    let query = this._hasBehavior("tag") ? this._tagsQuery :
+                this._hasBehavior("bookmark") ? this._bookmarkQuery :
+                this._hasBehavior("typed") ? this._typedQuery :
+                this._hasBehavior("history") ? this._historyQuery :
+                this._defaultQuery;
+
+    // Bind the needed parameters to the query so consumers can use it.
+    let (params = query.params) {
+      params.parent = this._bs.tagsFolder;
+      params.query_type = kQueryTypeFiltered;
+      params.matchBehavior = aMatchBehavior;
+      params.searchBehavior = this._behavior;
+
+      // We only want to search the tokens that we are left with - not the
+      // original search string.
+      params.searchString = aTokens.join(" ");
+
+      // Limit the query to the the maximum number of desired results.
+      // This way we can avoid doing more work than needed.
+      params.maxResults = this._maxRichResults;
+    }
+
+    return query;
+  },
+
+  /**
+   * Obtains the keyword query with the properly bound parameters.
+   *
+   * @param aTokens
+   *        The array of search tokens to check against.
+   * @return the bound keyword query.
+   */
+  _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens)
+  {
+    // The keyword is the first word in the search string, with the parameters
+    // following it.
+    let searchString = this._originalSearchString;
+    let queryString = searchString.substring(searchString.indexOf(" ") + 1);
+
+    // We need to escape the parameters as if they were the query in a URL
+    queryString = encodeURIComponent(queryString).replace("%20", "+", "g");
+
+    // The first word could be a keyword, so that's what we'll search.
+    let keyword = aTokens[0];
+
+    let query = this._keywordQuery;
+    let (params = query.params) {
+      params.keyword = keyword;
+      params.query_string = queryString;
+      params.query_type = kQueryTypeKeyword;
+    }
+
+    return query;
+  },
+
+  /**
+   * Obtains the adaptive query with the properly bound parameters.
+   *
+   * @return the bound adaptive query.
+   */
+  _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery()
+  {
+    let query = this._adaptiveQuery;
+    let (params = query.params) {
+      params.parent = this._bs.tagsFolder;
+      params.search_string = this._currentSearchString;
+      params.query_type = kQueryTypeFiltered;
+    }
+
+    return query;
+  },
+
+  /**
+   * Processes a mozIStorageRow to generate the proper data for the AutoComplete
+   * result.  This will add an entry to the current result if it matches the
+   * criteria.
+   *
+   * @param aRow
+   *        The row to process.
+   * @return true if the row is accepted, and false if not.
+   */
+  _processRow: function PAC_processRow(aRow)
+  {
+    // Before we do any work, make sure this entry isn't already in our results.
+    let entryId = aRow.getResultByIndex(kQueryIndexPlaceId);
+    if (this._inResults(entryId))
+      return false;
+
+    let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL);
+    let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || "";
+    let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || "";
+    let entryParentId = aRow.getResultByIndex(kQueryIndexParentId);
+    let entryBookmarkTitle = entryParentId ?
+      aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null;
+    let entryTags = aRow.getResultByIndex(kQueryIndexTags) || "";
+
+    // Always prefer the bookmark title unless it is empty
+    let title = entryBookmarkTitle || entryTitle;
+
+    let style;
+    if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) {
+      // If we do not have a title, then we must have a keyword, so let the UI
+      // know it is a keyword.  Otherwise, we found an exact page match, so just
+      // show the page like a regular result.  Because the page title is likely
+      // going to be more specific than the bookmark title (keyword title).
+      if (!entryTitle)
+        style = "keyword";
+      else
+        title = entryTitle;
+    }
+
+    // We will always prefer to show tags if we have them.
+    let showTags = !!entryTags;
+
+    // However, we'll act as if a page is not bookmarked or tagged if the user
+    // only wants only history and not bookmarks or tags.
+    if (this._hasBehavior("history") &&
+        !(this._hasBehavior("bookmark") || this._hasBehavior("tag"))) {
+      showTags = false;
+      style = "favicon";
+    }
+
+    // If we have tags and should show them, we need to add them to the title.
+    if (showTags)
+      title += kTitleTagsSeparator + entryTags;
+
+    // We have to determine the right style to display.  Tags show the tag icon,
+    // bookmarks get the bookmark icon, and keywords get the keyword icon.  If
+    // the result does not fall into any of those, it just gets the favicon.
+    if (!style) {
+      // It is possible that we already have a style set (from a keyword
+      // search), so only set it if we haven't already done so.
+      if (showTags)
+        style = "tag";
+      else if (entryParentId)
+        style = "bookmark";
+      else
+        style = "favicon";
+    }
+
+    // And finally add this to our results.
+    this._addToResults(entryId, escapedEntryURL, title, entryFavicon, style);
+    return true;
+  },
+
+  /**
+   * Checks to see if the given place has already been added to the results.
+   *
+   * @param aPlaceId
+   *        The place_id to check for.
+   * @return true if the place has been added, false otherwise.
+   */
+  _inResults: function PAC_inResults(aPlaceId)
+  {
+    return (aPlaceId in this._usedPlaceIds);
+  },
+
+  /**
+   * Adds a result to the AutoComplete results.  Also tracks that we've added
+   * this place_id into the result set.
+   *
+   * @param aPlaceId
+   *        The place_id of the item to be added to the result set.  This is
+   *        used by _inResults.
+   * @param aURISpec
+   *        The URI spec for the entry.
+   * @param aTitle
+   *        The title to give the entry.
+   * @param aFaviconSpec
+   *        The favicon to give to the entry.
+   * @param aStyle
+   *        Indicates how the entry should be styled when displayed.
+   */
+  _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle,
+                                           aFaviconSpec, aStyle)
+  {
+    // Add this to our internal tracker to ensure duplicates do not end up in
+    // the result.  _usedPlaceIds is an Object that is being used as a set.
+    this._usedPlaceIds[aPlaceId] = true;
+
+    // Obtain the favicon for this URI.
+    let favicon;
+    if (aFaviconSpec) {
+      let uri = this._ioService.newURI(aFaviconSpec, null, null);
+      favicon = this._faviconService.getFaviconLinkForIcon(uri).spec;
+    }
+    favicon = favicon || this._faviconService.defaultFavicon.spec;
+
+    this._result.appendMatch(aURISpec, aTitle, favicon, aStyle);
+  },
+
+  /**
+   * Determines if the specified AutoComplete behavior is set.
+   *
+   * @param aType
+   *        The behavior type to test for.
+   * @return true if the behavior is set, false otherwise.
+   */
+  _hasBehavior: function PAC_hasBehavior(aType)
+  {
+    return (this._behavior &
+            Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]);
+  },
+
+  /**
+   * Enables the desired AutoComplete behavior.
+   *
+   * @param aType
+   *        The behavior type to set.
+   */
+  _setBehavior: function PAC_setBehavior(aType)
+  {
+    this._behavior |=
+      Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
+  },
+
+  /**
+   * Determines if we are done searching or not.
+   *
+   * @return true if we have completed searching, false otherwise.
+   */
+  isSearchComplete: function PAC_isSearchComplete()
+  {
+    // If _pendingQuery is null, we should no longer do any work since we have
+    // already called _finishSearch.  This means we completed our search.
+    return this._pendingQuery == null;
+  },
+
+  /**
+   * Determines if the given handle of a pending statement is a pending search
+   * or not.
+   *
+   * @param aHandle
+   *        A mozIStoragePendingStatement to check and see if we are waiting for
+   *        results from it still.
+   * @return true if it is a pending query, false otherwise.
+   */
+  isPendingSearch: function PAC_isPendingSearch(aHandle)
+  {
+    return this._pendingQuery == aHandle;
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsISupports
+
+  classDescription: "AutoComplete result generator for Places.",
+  classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"),
+  contractID: "@mozilla.org/autocomplete/search;1?name=history",
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIAutoCompleteSearch,
+    Ci.nsIAutoCompleteSimpleResultListener,
+    Ci.mozIStorageStatementCallback,
+    Ci.nsIObserver,
+  ])
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Module Registration
+
+let components = [nsPlacesAutoComplete];
+function NSGetModule(compMgr, fileSpec)
+{
+  return XPCOMUtils.generateModule(components);
+}
--- a/suite/common/places/tests/autocomplete/head_autocomplete.js
+++ b/suite/common/places/tests/autocomplete/head_autocomplete.js
@@ -108,60 +108,63 @@ function ensure_results(aSearch, aExpect
     do_check_eq(numSearchesStarted, 1);
     aExpected = aExpected.slice();
 
     // Check to see the expected uris and titles match up (in any order)
     for (let i = 0; i < controller.matchCount; i++) {
       let value = controller.getValueAt(i);
       let comment = controller.getCommentAt(i);
 
-      print("Looking for " + value + ", " + comment + " in expected results...");
+      print("Looking for '" + value + "', '" + comment + "' in expected results...");
       let j;
       for (j = 0; j < aExpected.length; j++) {
         // Skip processed expected results
         if (aExpected[j] == undefined) continue;
 
         let [uri, title, tags] = gPages[aExpected[j]];
 
         // Load the real uri and titles and tags if necessary
         uri = toURI(kURIs[uri]).spec;
         title = kTitles[title];
         if (tags && appendTags)
           title += " \u2013 " + tags.map(function(aTag) kTitles[aTag]);
+        print("Checking against expected '" + uri + "', '" + title + "'...");
 
         // Got a match on both uri and title?
         if (uri == value && title == comment) {
           print("Got it at index " + j + "!!");
           // Make it undefined so we don't process it again
           aExpected[j] = undefined;
           break;
         }
       }
 
       // We didn't hit the break, so we must have not found it
       if (j == aExpected.length)
-        do_throw("Didn't find the current result (" + value + ", " + comment + ") in expected: " + aExpected);
+        do_throw("Didn't find the current result ('" + value + "', '" + comment + "') in expected: " + aExpected);
     }
 
     // Make sure we have the right number of results
+    print("Expecting " + aExpected.length + " results; got " +
+          controller.matchCount + " results");
     do_check_eq(controller.matchCount, aExpected.length);
 
     // If we expect results, make sure we got matches
     do_check_eq(controller.searchStatus, aExpected.length ?
                 Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
                 Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
 
     // Fetch the next test if we have more
     if (++current_test < gTests.length)
       run_test();
 
     do_test_finished();
   };
 
-  print("Searching for.. " + aSearch);
+  print("Searching for.. '" + aSearch + "'");
   controller.startSearch(aSearch);
 }
 
 // Get history services
 try {
   var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
                 getService(Ci.nsINavHistoryService);
   var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory);