toolkit/components/places/UnifiedComplete.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/places/UnifiedComplete.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1236 @@
     1.4 +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
     1.5 + * vim: sw=2 ts=2 sts=2 expandtab
     1.6 + * This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +"use strict";
    1.11 +
    1.12 +////////////////////////////////////////////////////////////////////////////////
    1.13 +//// Constants
    1.14 +
    1.15 +const Cc = Components.classes;
    1.16 +const Ci = Components.interfaces;
    1.17 +const Cr = Components.results;
    1.18 +const Cu = Components.utils;
    1.19 +
    1.20 +const TOPIC_SHUTDOWN = "places-shutdown";
    1.21 +const TOPIC_PREFCHANGED = "nsPref:changed";
    1.22 +
    1.23 +const DEFAULT_BEHAVIOR = 0;
    1.24 +
    1.25 +const PREF_BRANCH = "browser.urlbar";
    1.26 +
    1.27 +// Prefs are defined as [pref name, default value].
    1.28 +const PREF_ENABLED =            [ "autocomplete.enabled", true ];
    1.29 +const PREF_AUTOFILL =           [ "autoFill",             true ];
    1.30 +const PREF_AUTOFILL_TYPED =     [ "autoFill.typed",       true ];
    1.31 +const PREF_AUTOFILL_PRIORITY =  [ "autoFill.priority",    true ];
    1.32 +const PREF_DELAY =              [ "delay",                  50 ];
    1.33 +const PREF_BEHAVIOR =           [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
    1.34 +const PREF_DEFAULT_BEHAVIOR =   [ "default.behavior", DEFAULT_BEHAVIOR ];
    1.35 +const PREF_EMPTY_BEHAVIOR =     [ "default.behavior.emptyRestriction",
    1.36 +                                  Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
    1.37 +                                  Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED ];
    1.38 +const PREF_FILTER_JS =          [ "filter.javascript",    true ];
    1.39 +const PREF_MAXRESULTS =         [ "maxRichResults",         25 ];
    1.40 +const PREF_RESTRICT_HISTORY =   [ "restrict.history",      "^" ];
    1.41 +const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark",     "*" ];
    1.42 +const PREF_RESTRICT_TYPED =     [ "restrict.typed",        "~" ];
    1.43 +const PREF_RESTRICT_TAG =       [ "restrict.tag",          "+" ];
    1.44 +const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage",     "%" ];
    1.45 +const PREF_MATCH_TITLE =        [ "match.title",           "#" ];
    1.46 +const PREF_MATCH_URL =          [ "match.url",             "@" ];
    1.47 +
    1.48 +// Match type constants.
    1.49 +// These indicate what type of search function we should be using.
    1.50 +const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
    1.51 +const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
    1.52 +const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
    1.53 +const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
    1.54 +const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
    1.55 +
    1.56 +// AutoComplete query type constants.
    1.57 +// Describes the various types of queries that we can process rows for.
    1.58 +const QUERYTYPE_KEYWORD       = 0;
    1.59 +const QUERYTYPE_FILTERED      = 1;
    1.60 +const QUERYTYPE_AUTOFILL_HOST = 2;
    1.61 +const QUERYTYPE_AUTOFILL_URL  = 3;
    1.62 +
    1.63 +// This separator is used as an RTL-friendly way to split the title and tags.
    1.64 +// It can also be used by an nsIAutoCompleteResult consumer to re-split the
    1.65 +// "comment" back into the title and the tag.
    1.66 +const TITLE_TAGS_SEPARATOR = " \u2013 ";
    1.67 +
    1.68 +// Telemetry probes.
    1.69 +const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
    1.70 +
    1.71 +// The default frecency value used when inserting priority results.
    1.72 +const FRECENCY_PRIORITY_DEFAULT = 1000;
    1.73 +
    1.74 +// Sqlite result row index constants.
    1.75 +const QUERYINDEX_QUERYTYPE     = 0;
    1.76 +const QUERYINDEX_URL           = 1;
    1.77 +const QUERYINDEX_TITLE         = 2;
    1.78 +const QUERYINDEX_ICONURL       = 3;
    1.79 +const QUERYINDEX_BOOKMARKED    = 4;
    1.80 +const QUERYINDEX_BOOKMARKTITLE = 5;
    1.81 +const QUERYINDEX_TAGS          = 6;
    1.82 +const QUERYINDEX_VISITCOUNT    = 7;
    1.83 +const QUERYINDEX_TYPED         = 8;
    1.84 +const QUERYINDEX_PLACEID       = 9;
    1.85 +const QUERYINDEX_SWITCHTAB     = 10;
    1.86 +const QUERYINDEX_FRECENCY      = 11;
    1.87 +
    1.88 +// This SQL query fragment provides the following:
    1.89 +//   - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED)
    1.90 +//   - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE)
    1.91 +//   - the tags associated with a bookmarked entry (QUERYINDEX_TAGS)
    1.92 +const SQL_BOOKMARK_TAGS_FRAGMENT = sql(
    1.93 +  "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,",
    1.94 +  "( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL",
    1.95 +    "ORDER BY lastModified DESC LIMIT 1",
    1.96 +  ") AS btitle,",
    1.97 +  "( SELECT GROUP_CONCAT(t.title, ',')",
    1.98 +    "FROM moz_bookmarks b",
    1.99 +    "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent",
   1.100 +    "WHERE b.fk = h.id",
   1.101 +  ") AS tags");
   1.102 +
   1.103 +// TODO bug 412736: in case of a frecency tie, we might break it with h.typed
   1.104 +// and h.visit_count.  That is slower though, so not doing it yet...
   1.105 +const SQL_DEFAULT_QUERY = sql(
   1.106 +  "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",",
   1.107 +         "h.visit_count, h.typed, h.id, t.open_count, h.frecency",
   1.108 +  "FROM moz_places h",
   1.109 +  "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
   1.110 +  "LEFT JOIN moz_openpages_temp t ON t.url = h.url",
   1.111 +  "WHERE h.frecency <> 0",
   1.112 +    "AND AUTOCOMPLETE_MATCH(:searchString, h.url,",
   1.113 +                           "IFNULL(btitle, h.title), tags,",
   1.114 +                           "h.visit_count, h.typed,",
   1.115 +                           "bookmarked, t.open_count,",
   1.116 +                           ":matchBehavior, :searchBehavior)",
   1.117 +    "/*CONDITIONS*/",
   1.118 +  "ORDER BY h.frecency DESC, h.id DESC",
   1.119 +  "LIMIT :maxResults");
   1.120 +
   1.121 +// Enforce ignoring the visit_count index, since the frecency one is much
   1.122 +// faster in this case.  ANALYZE helps the query planner to figure out the
   1.123 +// faster path, but it may not have up-to-date information yet.
   1.124 +const SQL_HISTORY_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
   1.125 +                                                    "AND +h.visit_count > 0", "g");
   1.126 +
   1.127 +const SQL_BOOKMARK_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
   1.128 +                                                     "AND bookmarked", "g");
   1.129 +
   1.130 +const SQL_TAGS_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
   1.131 +                                                 "AND tags NOTNULL", "g");
   1.132 +
   1.133 +const SQL_TYPED_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
   1.134 +                                                  "AND h.typed = 1", "g");
   1.135 +
   1.136 +const SQL_SWITCHTAB_QUERY = sql(
   1.137 +  "SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,",
   1.138 +         "t.open_count, NULL",
   1.139 +  "FROM moz_openpages_temp t",
   1.140 +  "LEFT JOIN moz_places h ON h.url = t.url",
   1.141 +  "WHERE h.id IS NULL",
   1.142 +    "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,",
   1.143 +                            "NULL, NULL, NULL, t.open_count,",
   1.144 +                            ":matchBehavior, :searchBehavior)",
   1.145 +  "ORDER BY t.ROWID DESC",
   1.146 +  "LIMIT :maxResults");
   1.147 +
   1.148 +const SQL_ADAPTIVE_QUERY = sql(
   1.149 +  "/* do not warn (bug 487789) */",
   1.150 +  "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",",
   1.151 +         "h.visit_count, h.typed, h.id, t.open_count, h.frecency",
   1.152 +  "FROM (",
   1.153 +    "SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,",
   1.154 +           "place_id",
   1.155 +    "FROM moz_inputhistory",
   1.156 +    "WHERE input BETWEEN :search_string AND :search_string || X'FFFF'",
   1.157 +    "GROUP BY place_id",
   1.158 +  ") AS i",
   1.159 +  "JOIN moz_places h ON h.id = i.place_id",
   1.160 +  "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
   1.161 +  "LEFT JOIN moz_openpages_temp t ON t.url = h.url",
   1.162 +  "WHERE AUTOCOMPLETE_MATCH(NULL, h.url,",
   1.163 +                           "IFNULL(btitle, h.title), tags,",
   1.164 +                           "h.visit_count, h.typed, bookmarked,",
   1.165 +                           "t.open_count,",
   1.166 +                           ":matchBehavior, :searchBehavior)",
   1.167 +  "ORDER BY rank DESC, h.frecency DESC");
   1.168 +
   1.169 +const SQL_KEYWORD_QUERY = sql(
   1.170 +  "/* do not warn (bug 487787) */",
   1.171 +  "SELECT :query_type,",
   1.172 +    "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)",
   1.173 +    "AS search_url, h.title,",
   1.174 +    "IFNULL(f.url, (SELECT f.url",
   1.175 +                   "FROM moz_places",
   1.176 +                   "JOIN moz_favicons f ON f.id = favicon_id",
   1.177 +                   "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)",
   1.178 +                   "ORDER BY frecency DESC",
   1.179 +                   "LIMIT 1)",
   1.180 +          "),",
   1.181 +    "1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),",
   1.182 +    "t.open_count, h.frecency",
   1.183 +  "FROM moz_keywords k",
   1.184 +  "JOIN moz_bookmarks b ON b.keyword_id = k.id",
   1.185 +  "LEFT JOIN moz_places h ON h.url = search_url",
   1.186 +  "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
   1.187 +  "LEFT JOIN moz_openpages_temp t ON t.url = search_url",
   1.188 +  "WHERE LOWER(k.keyword) = LOWER(:keyword)",
   1.189 +  "ORDER BY h.frecency DESC");
   1.190 +
   1.191 +const SQL_HOST_QUERY = sql(
   1.192 +  "/* do not warn (bug NA): not worth to index on (typed, frecency) */",
   1.193 +  "SELECT :query_type, host || '/', prefix || host || '/',",
   1.194 +         "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency",
   1.195 +  "FROM moz_hosts",
   1.196 +  "WHERE host BETWEEN :searchString AND :searchString || X'FFFF'",
   1.197 +  "AND frecency <> 0",
   1.198 +  "/*CONDITIONS*/",
   1.199 +  "ORDER BY frecency DESC",
   1.200 +  "LIMIT 1");
   1.201 +
   1.202 +const SQL_TYPED_HOST_QUERY = SQL_HOST_QUERY.replace("/*CONDITIONS*/",
   1.203 +                                                    "AND typed = 1");
   1.204 +const SQL_URL_QUERY = sql(
   1.205 +  "/* do not warn (bug no): cannot use an index */",
   1.206 +  "SELECT :query_type, h.url,",
   1.207 +         "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, h.frecency",
   1.208 +  "FROM moz_places h",
   1.209 +  "WHERE h.frecency <> 0",
   1.210 +  "/*CONDITIONS*/",
   1.211 +  "AND AUTOCOMPLETE_MATCH(:searchString, h.url,",
   1.212 +  "h.title, '',",
   1.213 +  "h.visit_count, h.typed, 0, 0,",
   1.214 +  ":matchBehavior, :searchBehavior)",
   1.215 +  "ORDER BY h.frecency DESC, h.id DESC",
   1.216 +  "LIMIT 1");
   1.217 +
   1.218 +const SQL_TYPED_URL_QUERY = SQL_URL_QUERY.replace("/*CONDITIONS*/",
   1.219 +                                                  "AND typed = 1");
   1.220 +
   1.221 +////////////////////////////////////////////////////////////////////////////////
   1.222 +//// Getters
   1.223 +
   1.224 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
   1.225 +Cu.import("resource://gre/modules/Services.jsm");
   1.226 +
   1.227 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   1.228 +                                  "resource://gre/modules/PlacesUtils.jsm");
   1.229 +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
   1.230 +                                  "resource://gre/modules/TelemetryStopwatch.jsm");
   1.231 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   1.232 +                                  "resource://gre/modules/NetUtil.jsm");
   1.233 +XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
   1.234 +                                  "resource://gre/modules/Preferences.jsm");
   1.235 +XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
   1.236 +                                  "resource://gre/modules/Sqlite.jsm");
   1.237 +XPCOMUtils.defineLazyModuleGetter(this, "OS",
   1.238 +                                  "resource://gre/modules/osfile.jsm");
   1.239 +XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   1.240 +                                  "resource://gre/modules/Promise.jsm");
   1.241 +XPCOMUtils.defineLazyModuleGetter(this, "Task",
   1.242 +                                  "resource://gre/modules/Task.jsm");
   1.243 +XPCOMUtils.defineLazyModuleGetter(this, "PriorityUrlProvider",
   1.244 +                                  "resource://gre/modules/PriorityUrlProvider.jsm");
   1.245 +
   1.246 +XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
   1.247 +                                   "@mozilla.org/intl/texttosuburi;1",
   1.248 +                                   "nsITextToSubURI");
   1.249 +
   1.250 +/**
   1.251 + * Storage object for switch-to-tab entries.
   1.252 + * This takes care of caching and registering open pages, that will be reused
   1.253 + * by switch-to-tab queries.  It has an internal cache, so that the Sqlite
   1.254 + * store is lazy initialized only on first use.
   1.255 + * It has a simple API:
   1.256 + *   initDatabase(conn): initializes the temporary Sqlite entities to store data
   1.257 + *   add(uri): adds a given nsIURI to the store
   1.258 + *   delete(uri): removes a given nsIURI from the store
   1.259 + *   shutdown(): stops storing data to Sqlite
   1.260 + */
   1.261 +XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({
   1.262 +  _conn: null,
   1.263 +  // Temporary queue used while the database connection is not available.
   1.264 +  _queue: new Set(),
   1.265 +  initDatabase: Task.async(function* (conn) {
   1.266 +    // To reduce IO use an in-memory table for switch-to-tab tracking.
   1.267 +    // Note: this should be kept up-to-date with the definition in
   1.268 +    //       nsPlacesTables.h.
   1.269 +    yield conn.execute(sql(
   1.270 +      "CREATE TEMP TABLE moz_openpages_temp (",
   1.271 +        "url TEXT PRIMARY KEY,",
   1.272 +        "open_count INTEGER",
   1.273 +      ")"));
   1.274 +
   1.275 +    // Note: this should be kept up-to-date with the definition in
   1.276 +    //       nsPlacesTriggers.h.
   1.277 +    yield conn.execute(sql(
   1.278 +      "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger",
   1.279 +      "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW",
   1.280 +      "WHEN NEW.open_count = 0",
   1.281 +      "BEGIN",
   1.282 +        "DELETE FROM moz_openpages_temp",
   1.283 +        "WHERE url = NEW.url;",
   1.284 +      "END"));
   1.285 +
   1.286 +    this._conn = conn;
   1.287 +
   1.288 +    // Populate the table with the current cache contents...
   1.289 +    this._queue.forEach(this.add, this);
   1.290 +    // ...then clear it to avoid double additions.
   1.291 +    this._queue.clear();
   1.292 +  }),
   1.293 +
   1.294 +  add: function (uri) {
   1.295 +    if (!this._conn) {
   1.296 +      this._queue.add(uri);
   1.297 +      return;
   1.298 +    }
   1.299 +    this._conn.executeCached(sql(
   1.300 +      "INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)",
   1.301 +        "VALUES ( :url, IFNULL( (SELECT open_count + 1",
   1.302 +                                 "FROM moz_openpages_temp",
   1.303 +                                 "WHERE url = :url),",
   1.304 +                                 "1",
   1.305 +                             ")",
   1.306 +               ")"
   1.307 +    ), { url: uri.spec });
   1.308 +  },
   1.309 +
   1.310 +  delete: function (uri) {
   1.311 +    if (!this._conn) {
   1.312 +      this._queue.delete(uri);
   1.313 +      return;
   1.314 +    }
   1.315 +    this._conn.executeCached(sql(
   1.316 +      "UPDATE moz_openpages_temp",
   1.317 +      "SET open_count = open_count - 1",
   1.318 +      "WHERE url = :url"
   1.319 +    ), { url: uri.spec });
   1.320 +  },
   1.321 +
   1.322 +  shutdown: function () {
   1.323 +    this._conn = null;
   1.324 +    this._queue.clear();
   1.325 +  }
   1.326 +}));
   1.327 +
   1.328 +/**
   1.329 + * This helper keeps track of preferences and keeps their values up-to-date.
   1.330 + */
   1.331 +XPCOMUtils.defineLazyGetter(this, "Prefs", () => {
   1.332 +  let prefs = new Preferences(PREF_BRANCH);
   1.333 +
   1.334 +  function loadPrefs() {
   1.335 +    store.enabled = prefs.get(...PREF_ENABLED);
   1.336 +    store.autofill = prefs.get(...PREF_AUTOFILL);
   1.337 +    store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
   1.338 +    store.autofillPriority = prefs.get(...PREF_AUTOFILL_PRIORITY);
   1.339 +    store.delay = prefs.get(...PREF_DELAY);
   1.340 +    store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
   1.341 +    store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
   1.342 +    store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
   1.343 +    store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY);
   1.344 +    store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS);
   1.345 +    store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED);
   1.346 +    store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG);
   1.347 +    store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB);
   1.348 +    store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE);
   1.349 +    store.matchURLToken = prefs.get(...PREF_MATCH_URL);
   1.350 +    store.defaultBehavior = prefs.get(...PREF_DEFAULT_BEHAVIOR);
   1.351 +    // Further restrictions to apply for "empty searches" (i.e. searches for "").
   1.352 +    store.emptySearchDefaultBehavior = store.defaultBehavior |
   1.353 +                                       prefs.get(...PREF_EMPTY_BEHAVIOR);
   1.354 +
   1.355 +    // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
   1.356 +    if (store.matchBehavior != MATCH_ANYWHERE &&
   1.357 +        store.matchBehavior != MATCH_BOUNDARY &&
   1.358 +        store.matchBehavior != MATCH_BEGINNING) {
   1.359 +      store.matchBehavior = MATCH_BOUNDARY_ANYWHERE;
   1.360 +    }
   1.361 +
   1.362 +    store.tokenToBehaviorMap = new Map([
   1.363 +      [ store.restrictHistoryToken, "history" ],
   1.364 +      [ store.restrictBookmarkToken, "bookmark" ],
   1.365 +      [ store.restrictTagToken, "tag" ],
   1.366 +      [ store.restrictOpenPageToken, "openpage" ],
   1.367 +      [ store.matchTitleToken, "title" ],
   1.368 +      [ store.matchURLToken, "url" ],
   1.369 +      [ store.restrictTypedToken, "typed" ]
   1.370 +    ]);
   1.371 +  }
   1.372 +
   1.373 +  let store = {
   1.374 +    observe: function (subject, topic, data) {
   1.375 +      loadPrefs();
   1.376 +    },
   1.377 +    QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ])
   1.378 +  };
   1.379 +  loadPrefs();
   1.380 +  prefs.observe("", store);
   1.381 +
   1.382 +  return Object.seal(store);
   1.383 +});
   1.384 +
   1.385 +////////////////////////////////////////////////////////////////////////////////
   1.386 +//// Helper functions
   1.387 +
   1.388 +/**
   1.389 + * Joins multiple sql tokens into a single sql query.
   1.390 + */
   1.391 +function sql(...parts) parts.join(" ");
   1.392 +
   1.393 +/**
   1.394 + * Used to unescape encoded URI strings and drop information that we do not
   1.395 + * care about.
   1.396 + *
   1.397 + * @param spec
   1.398 + *        The text to unescape and modify.
   1.399 + * @return the modified spec.
   1.400 + */
   1.401 +function fixupSearchText(spec)
   1.402 +  textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec));
   1.403 +
   1.404 +/**
   1.405 + * Generates the tokens used in searching from a given string.
   1.406 + *
   1.407 + * @param searchString
   1.408 + *        The string to generate tokens from.
   1.409 + * @return an array of tokens.
   1.410 + * @note Calling split on an empty string will return an array containing one
   1.411 + *       empty string.  We don't want that, as it'll break our logic, so return
   1.412 + *       an empty array then.
   1.413 + */
   1.414 +function getUnfilteredSearchTokens(searchString)
   1.415 +  searchString.length ? searchString.split(" ") : [];
   1.416 +
   1.417 +/**
   1.418 + * Strip prefixes from the URI that we don't care about for searching.
   1.419 + *
   1.420 + * @param spec
   1.421 + *        The text to modify.
   1.422 + * @return the modified spec.
   1.423 + */
   1.424 +function stripPrefix(spec)
   1.425 +{
   1.426 +  ["http://", "https://", "ftp://"].some(scheme => {
   1.427 +    if (spec.startsWith(scheme)) {
   1.428 +      spec = spec.slice(scheme.length);
   1.429 +      return true;
   1.430 +    }
   1.431 +    return false;
   1.432 +  });
   1.433 +
   1.434 +  if (spec.startsWith("www.")) {
   1.435 +    spec = spec.slice(4);
   1.436 +  }
   1.437 +  return spec;
   1.438 +}
   1.439 +
   1.440 +////////////////////////////////////////////////////////////////////////////////
   1.441 +//// Search Class
   1.442 +//// Manages a single instance of an autocomplete search.
   1.443 +
   1.444 +function Search(searchString, searchParam, autocompleteListener,
   1.445 +                resultListener, autocompleteSearch) {
   1.446 +  // We want to store the original string with no leading or trailing
   1.447 +  // whitespace for case sensitive searches.
   1.448 +  this._originalSearchString = searchString.trim();
   1.449 +  this._searchString = fixupSearchText(this._originalSearchString.toLowerCase());
   1.450 +  this._searchTokens =
   1.451 +    this.filterTokens(getUnfilteredSearchTokens(this._searchString));
   1.452 +  // The protocol and the host are lowercased by nsIURI, so it's fine to
   1.453 +  // lowercase the typed prefix, to add it back to the results later.
   1.454 +  this._strippedPrefix = this._originalSearchString.slice(
   1.455 +    0, this._originalSearchString.length - this._searchString.length
   1.456 +  ).toLowerCase();
   1.457 +  // The URIs in the database are fixed-up, so we can match on a lowercased
   1.458 +  // host, but the path must be matched in a case sensitive way.
   1.459 +  let pathIndex =
   1.460 +    this._originalSearchString.indexOf("/", this._strippedPrefix.length);
   1.461 +  this._autofillUrlSearchString = fixupSearchText(
   1.462 +    this._originalSearchString.slice(0, pathIndex).toLowerCase() +
   1.463 +    this._originalSearchString.slice(pathIndex)
   1.464 +  );
   1.465 +
   1.466 +  this._enableActions = searchParam.split(" ").indexOf("enable-actions") != -1;
   1.467 +
   1.468 +  this._listener = autocompleteListener;
   1.469 +  this._autocompleteSearch = autocompleteSearch;
   1.470 +
   1.471 +  this._matchBehavior = Prefs.matchBehavior;
   1.472 +  // Set the default behavior for this search.
   1.473 +  this._behavior = this._searchString ? Prefs.defaultBehavior
   1.474 +                                      : Prefs.emptySearchDefaultBehavior;
   1.475 +  // Create a new result to add eventual matches.  Note we need a result
   1.476 +  // regardless having matches.
   1.477 +  let result = Cc["@mozilla.org/autocomplete/simple-result;1"]
   1.478 +                 .createInstance(Ci.nsIAutoCompleteSimpleResult);
   1.479 +  result.setSearchString(searchString);
   1.480 +  result.setListener(resultListener);
   1.481 +  // Will be set later, if needed.
   1.482 +  result.setDefaultIndex(-1);
   1.483 +  this._result = result;
   1.484 +
   1.485 +  // These are used to avoid adding duplicate entries to the results.
   1.486 +  this._usedURLs = new Set();
   1.487 +  this._usedPlaceIds = new Set();
   1.488 +}
   1.489 +
   1.490 +Search.prototype = {
   1.491 +  /**
   1.492 +   * Enables the desired AutoComplete behavior.
   1.493 +   *
   1.494 +   * @param type
   1.495 +   *        The behavior type to set.
   1.496 +   */
   1.497 +  setBehavior: function (type) {
   1.498 +    this._behavior |=
   1.499 +      Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
   1.500 +  },
   1.501 +
   1.502 +  /**
   1.503 +   * Determines if the specified AutoComplete behavior is set.
   1.504 +   *
   1.505 +   * @param aType
   1.506 +   *        The behavior type to test for.
   1.507 +   * @return true if the behavior is set, false otherwise.
   1.508 +   */
   1.509 +  hasBehavior: function (type) {
   1.510 +    return this._behavior &
   1.511 +           Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
   1.512 +  },
   1.513 +
   1.514 +  /**
   1.515 +   * Used to delay the most complex queries, to save IO while the user is
   1.516 +   * typing.
   1.517 +   */
   1.518 +  _sleepDeferred: null,
   1.519 +  _sleep: function (aTimeMs) {
   1.520 +    // Reuse a single instance to try shaving off some usless work before
   1.521 +    // the first query.
   1.522 +    if (!this._sleepTimer)
   1.523 +      this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   1.524 +    this._sleepDeferred = Promise.defer();
   1.525 +    this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(),
   1.526 +                                      aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT);
   1.527 +    return this._sleepDeferred.promise;
   1.528 +  },
   1.529 +
   1.530 +  /**
   1.531 +   * Given an array of tokens, this function determines which query should be
   1.532 +   * ran.  It also removes any special search tokens.
   1.533 +   *
   1.534 +   * @param tokens
   1.535 +   *        An array of search tokens.
   1.536 +   * @return the filtered list of tokens to search with.
   1.537 +   */
   1.538 +  filterTokens: function (tokens) {
   1.539 +    // Set the proper behavior while filtering tokens.
   1.540 +    for (let i = tokens.length - 1; i >= 0; i--) {
   1.541 +      let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]);
   1.542 +      // Don't remove the token if it didn't match, or if it's an action but
   1.543 +      // actions are not enabled.
   1.544 +      if (behavior && (behavior != "openpage" || this._enableActions)) {
   1.545 +        this.setBehavior(behavior);
   1.546 +        tokens.splice(i, 1);
   1.547 +      }
   1.548 +    }
   1.549 +
   1.550 +    // Set the right JavaScript behavior based on our preference.  Note that the
   1.551 +    // preference is whether or not we should filter JavaScript, and the
   1.552 +    // behavior is if we should search it or not.
   1.553 +    if (!Prefs.filterJavaScript) {
   1.554 +      this.setBehavior("javascript");
   1.555 +    }
   1.556 +
   1.557 +    return tokens;
   1.558 +  },
   1.559 +
   1.560 +  /**
   1.561 +   * Used to cancel this search, will stop providing results.
   1.562 +   */
   1.563 +  cancel: function () {
   1.564 +    if (this._sleepTimer)
   1.565 +      this._sleepTimer.cancel();
   1.566 +    if (this._sleepDeferred) {
   1.567 +      this._sleepDeferred.resolve();
   1.568 +      this._sleepDeferred = null;
   1.569 +    }
   1.570 +    delete this._pendingQuery;
   1.571 +  },
   1.572 +
   1.573 +  /**
   1.574 +   * Whether this search is running.
   1.575 +   */
   1.576 +  get pending() !!this._pendingQuery,
   1.577 +
   1.578 +  /**
   1.579 +   * Execute the search and populate results.
   1.580 +   * @param conn
   1.581 +   *        The Sqlite connection.
   1.582 +   */
   1.583 +  execute: Task.async(function* (conn) {
   1.584 +    this._pendingQuery = true;
   1.585 +    TelemetryStopwatch.start(TELEMETRY_1ST_RESULT);
   1.586 +
   1.587 +    // For any given search, we run many queries:
   1.588 +    // 1) priority domains
   1.589 +    // 2) inline completion
   1.590 +    // 3) keywords (this._keywordQuery)
   1.591 +    // 4) adaptive learning (this._adaptiveQuery)
   1.592 +    // 5) open pages not supported by history (this._switchToTabQuery)
   1.593 +    // 6) query based on match behavior
   1.594 +    //
   1.595 +    // (3) only gets ran if we get any filtered tokens, since if there are no
   1.596 +    // tokens, there is nothing to match.
   1.597 +
   1.598 +    // Get the final query, based on the tokens found in the search string.
   1.599 +    let queries = [ this._adaptiveQuery,
   1.600 +                    this._switchToTabQuery,
   1.601 +                    this._searchQuery ];
   1.602 +
   1.603 +    if (this._searchTokens.length == 1) {
   1.604 +      yield this._matchPriorityUrl();
   1.605 +    } else if (this._searchTokens.length > 1) {
   1.606 +      queries.unshift(this._keywordQuery);
   1.607 +    }
   1.608 +
   1.609 +    if (this._shouldAutofill) {
   1.610 +      // Hosts have no "/" in them.
   1.611 +      let lastSlashIndex = this._searchString.lastIndexOf("/");
   1.612 +      // Search only URLs if there's a slash in the search string...
   1.613 +      if (lastSlashIndex != -1) {
   1.614 +        // ...but not if it's exactly at the end of the search string.
   1.615 +        if (lastSlashIndex < this._searchString.length - 1) {
   1.616 +          queries.unshift(this._urlQuery);
   1.617 +        }
   1.618 +      } else if (this.pending) {
   1.619 +        // The host query is executed immediately, while any other is delayed
   1.620 +        // to avoid overloading the connection.
   1.621 +        let [ query, params ] = this._hostQuery;
   1.622 +        yield conn.executeCached(query, params, this._onResultRow.bind(this));
   1.623 +      }
   1.624 +    }
   1.625 +
   1.626 +    yield this._sleep(Prefs.delay);
   1.627 +    if (!this.pending)
   1.628 +      return;
   1.629 +
   1.630 +    for (let [query, params] of queries) {
   1.631 +      yield conn.executeCached(query, params, this._onResultRow.bind(this));
   1.632 +      if (!this.pending)
   1.633 +        return;
   1.634 +    }
   1.635 +
   1.636 +    // If we do not have enough results, and our match type is
   1.637 +    // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
   1.638 +    // results.
   1.639 +    if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
   1.640 +        this._result.matchCount < Prefs.maxRichResults) {
   1.641 +      this._matchBehavior = MATCH_ANYWHERE;
   1.642 +      for (let [query, params] of [ this._adaptiveQuery,
   1.643 +                                    this._searchQuery ]) {
   1.644 +        yield conn.executeCached(query, params, this._onResultRow);
   1.645 +        if (!this.pending)
   1.646 +          return;
   1.647 +      }
   1.648 +    }
   1.649 +
   1.650 +    // If we didn't find enough matches and we have some frecency-driven
   1.651 +    // matches, add them.
   1.652 +    if (this._frecencyMatches) {
   1.653 +      this._frecencyMatches.forEach(this._addMatch, this);
   1.654 +    }
   1.655 +  }),
   1.656 +
   1.657 +  _matchPriorityUrl: function* () {
   1.658 +    if (!Prefs.autofillPriority)
   1.659 +      return;
   1.660 +    let priorityMatch = yield PriorityUrlProvider.getMatch(this._searchString);
   1.661 +    if (priorityMatch) {
   1.662 +      this._result.setDefaultIndex(0);
   1.663 +      this._addFrecencyMatch({
   1.664 +        value: priorityMatch.token,
   1.665 +        comment: priorityMatch.title,
   1.666 +        icon: priorityMatch.iconUrl,
   1.667 +        style: "priority-" + priorityMatch.reason,
   1.668 +        finalCompleteValue: priorityMatch.url,
   1.669 +        frecency: FRECENCY_PRIORITY_DEFAULT
   1.670 +      });
   1.671 +    }
   1.672 +  },
   1.673 +
   1.674 +  _onResultRow: function (row) {
   1.675 +    TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT);
   1.676 +    let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
   1.677 +    let match;
   1.678 +    switch (queryType) {
   1.679 +      case QUERYTYPE_AUTOFILL_HOST:
   1.680 +        this._result.setDefaultIndex(0);
   1.681 +        match = this._processHostRow(row);
   1.682 +        break;
   1.683 +      case QUERYTYPE_AUTOFILL_URL:
   1.684 +        this._result.setDefaultIndex(0);
   1.685 +        match = this._processUrlRow(row);
   1.686 +        break;
   1.687 +      case QUERYTYPE_FILTERED:
   1.688 +      case QUERYTYPE_KEYWORD:
   1.689 +        match = this._processRow(row);
   1.690 +        break;
   1.691 +    }
   1.692 +    this._addMatch(match);
   1.693 +  },
   1.694 +
   1.695 +  /**
   1.696 +   * These matches should be mixed up with other matches, based on frecency.
   1.697 +   */
   1.698 +  _addFrecencyMatch: function (match) {
   1.699 +    if (!this._frecencyMatches)
   1.700 +      this._frecencyMatches = [];
   1.701 +    this._frecencyMatches.push(match);
   1.702 +    // We keep this array in reverse order, so we can walk it and remove stuff
   1.703 +    // from it in one pass.  Notice that for frecency reverse order means from
   1.704 +    // lower to higher.
   1.705 +    this._frecencyMatches.sort((a, b) => a.frecency - b.frecency);
   1.706 +  },
   1.707 +
   1.708 +  _addMatch: function (match) {
   1.709 +    let notifyResults = false;
   1.710 +
   1.711 +    if (this._frecencyMatches) {
   1.712 +      for (let i = this._frecencyMatches.length - 1;  i >= 0 ; i--) {
   1.713 +        if (this._frecencyMatches[i].frecency > match.frecency) {
   1.714 +          this._addMatch(this._frecencyMatches.splice(i, 1)[0]);
   1.715 +        }
   1.716 +      }
   1.717 +    }
   1.718 +
   1.719 +    // Must check both id and url, cause keywords dinamically modify the url.
   1.720 +    if ((!match.placeId || !this._usedPlaceIds.has(match.placeId)) &&
   1.721 +        !this._usedURLs.has(stripPrefix(match.value))) {
   1.722 +      // Add this to our internal tracker to ensure duplicates do not end up in
   1.723 +      // the result.
   1.724 +      // Not all entries have a place id, thus we fallback to the url for them.
   1.725 +      // We cannot use only the url since keywords entries are modified to
   1.726 +      // include the search string, and would be returned multiple times.  Ids
   1.727 +      // are faster too.
   1.728 +      if (match.placeId)
   1.729 +        this._usedPlaceIds.add(match.placeId);
   1.730 +      this._usedURLs.add(stripPrefix(match.value));
   1.731 +
   1.732 +      this._result.appendMatch(match.value,
   1.733 +                               match.comment,
   1.734 +                               match.icon || PlacesUtils.favicons.defaultFavicon.spec,
   1.735 +                               match.style || "favicon",
   1.736 +                               match.finalCompleteValue);
   1.737 +      notifyResults = true;
   1.738 +    }
   1.739 +
   1.740 +    if (this._result.matchCount == Prefs.maxRichResults || !this.pending) {
   1.741 +      // We have enough results, so stop running our search.
   1.742 +      this.cancel();
   1.743 +      // This tells Sqlite.jsm to stop providing us results and cancel the
   1.744 +      // underlying query.
   1.745 +      throw StopIteration;
   1.746 +    }
   1.747 +
   1.748 +    if (notifyResults) {
   1.749 +      // Notify about results if we've gotten them.
   1.750 +      this.notifyResults(true);
   1.751 +    }
   1.752 +  },
   1.753 +
   1.754 +  _processHostRow: function (row) {
   1.755 +    let match = {};
   1.756 +    let trimmedHost = row.getResultByIndex(QUERYINDEX_URL);
   1.757 +    let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE);
   1.758 +    let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
   1.759 +    // If the untrimmed value doesn't preserve the user's input just
   1.760 +    // ignore it and complete to the found host.
   1.761 +    if (untrimmedHost &&
   1.762 +        !untrimmedHost.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
   1.763 +      // THIS CAUSES null TO BE SHOWN AS TITLE.
   1.764 +      untrimmedHost = null;
   1.765 +    }
   1.766 +
   1.767 +    match.value = this._strippedPrefix + trimmedHost;
   1.768 +    match.comment = trimmedHost;
   1.769 +    match.finalCompleteValue = untrimmedHost;
   1.770 +    match.frecency = frecency;
   1.771 +    return match;
   1.772 +  },
   1.773 +
   1.774 +  _processUrlRow: function (row) {
   1.775 +    let match = {};
   1.776 +    let value = row.getResultByIndex(QUERYINDEX_URL);
   1.777 +    let url = fixupSearchText(value);
   1.778 +    let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
   1.779 +
   1.780 +    let prefix = value.slice(0, value.length - stripPrefix(value).length);
   1.781 +
   1.782 +    // We must complete the URL up to the next separator (which is /, ? or #).
   1.783 +    let separatorIndex = url.slice(this._searchString.length)
   1.784 +                            .search(/[\/\?\#]/);
   1.785 +    if (separatorIndex != -1) {
   1.786 +      separatorIndex += this._searchString.length;
   1.787 +      if (url[separatorIndex] == "/") {
   1.788 +        separatorIndex++; // Include the "/" separator
   1.789 +      }
   1.790 +      url = url.slice(0, separatorIndex);
   1.791 +    }
   1.792 +
   1.793 +    // If the untrimmed value doesn't preserve the user's input just
   1.794 +    // ignore it and complete to the found url.
   1.795 +    let untrimmedURL = prefix + url;
   1.796 +    if (untrimmedURL &&
   1.797 +        !untrimmedURL.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
   1.798 +      // THIS CAUSES null TO BE SHOWN AS TITLE.
   1.799 +      untrimmedURL = null;
   1.800 +     }
   1.801 +
   1.802 +    match.value = this._strippedPrefix + url;
   1.803 +    match.comment = url;
   1.804 +    match.finalCompleteValue = untrimmedURL;
   1.805 +    match.frecency = frecency;
   1.806 +    return match;
   1.807 +  },
   1.808 +
   1.809 +  _processRow: function (row) {
   1.810 +    let match = {};
   1.811 +    match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
   1.812 +    let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
   1.813 +    let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
   1.814 +    let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0;
   1.815 +    let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || "";
   1.816 +    let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || "";
   1.817 +    let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED);
   1.818 +    let bookmarkTitle = bookmarked ?
   1.819 +      row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null;
   1.820 +    let tags = row.getResultByIndex(QUERYINDEX_TAGS) || "";
   1.821 +    let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
   1.822 +
   1.823 +    // If actions are enabled and the page is open, add only the switch-to-tab
   1.824 +    // result.  Otherwise, add the normal result.
   1.825 +    let [url, action] = this._enableActions && openPageCount > 0 ?
   1.826 +                        ["moz-action:switchtab," + escapedURL, "action "] :
   1.827 +                        [escapedURL, ""];
   1.828 +
   1.829 +    // Always prefer the bookmark title unless it is empty
   1.830 +    let title = bookmarkTitle || historyTitle;
   1.831 +
   1.832 +    if (queryType == QUERYTYPE_KEYWORD) {
   1.833 +      // If we do not have a title, then we must have a keyword, so let the UI
   1.834 +      // know it is a keyword.  Otherwise, we found an exact page match, so just
   1.835 +      // show the page like a regular result.  Because the page title is likely
   1.836 +      // going to be more specific than the bookmark title (keyword title).
   1.837 +      if (!historyTitle) {
   1.838 +        match.style = "keyword";
   1.839 +      }
   1.840 +      else {
   1.841 +        title = historyTitle;
   1.842 +      }
   1.843 +    }
   1.844 +
   1.845 +    // We will always prefer to show tags if we have them.
   1.846 +    let showTags = !!tags;
   1.847 +
   1.848 +    // However, we'll act as if a page is not bookmarked or tagged if the user
   1.849 +    // only wants only history and not bookmarks or tags.
   1.850 +    if (this.hasBehavior("history") &&
   1.851 +        !(this.hasBehavior("bookmark") || this.hasBehavior("tag"))) {
   1.852 +      showTags = false;
   1.853 +      match.style = "favicon";
   1.854 +    }
   1.855 +
   1.856 +    // If we have tags and should show them, we need to add them to the title.
   1.857 +    if (showTags) {
   1.858 +      title += TITLE_TAGS_SEPARATOR + tags;
   1.859 +    }
   1.860 +
   1.861 +    // We have to determine the right style to display.  Tags show the tag icon,
   1.862 +    // bookmarks get the bookmark icon, and keywords get the keyword icon.  If
   1.863 +    // the result does not fall into any of those, it just gets the favicon.
   1.864 +    if (!match.style) {
   1.865 +      // It is possible that we already have a style set (from a keyword
   1.866 +      // search or because of the user's preferences), so only set it if we
   1.867 +      // haven't already done so.
   1.868 +      if (showTags) {
   1.869 +        match.style = "tag";
   1.870 +      }
   1.871 +      else if (bookmarked) {
   1.872 +        match.style = "bookmark";
   1.873 +      }
   1.874 +    }
   1.875 +
   1.876 +    match.value = url;
   1.877 +    match.comment = title;
   1.878 +    if (iconurl) {
   1.879 +      match.icon = PlacesUtils.favicons
   1.880 +                              .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec;
   1.881 +    }
   1.882 +    match.frecency = frecency;
   1.883 +
   1.884 +    return match;
   1.885 +  },
   1.886 +
   1.887 +  /**
   1.888 +   * Obtains the search query to be used based on the previously set search
   1.889 +   * behaviors (accessed by this.hasBehavior).
   1.890 +   *
   1.891 +   * @return an array consisting of the correctly optimized query to search the
   1.892 +   *         database with and an object containing the params to bound.
   1.893 +   */
   1.894 +  get _searchQuery() {
   1.895 +    // We use more optimized queries for restricted searches, so we will always
   1.896 +    // return the most restrictive one to the least restrictive one if more than
   1.897 +    // one token is found.
   1.898 +    // Note: "openpages" behavior is supported by the default query.
   1.899 +    //       _switchToTabQuery instead returns only pages not supported by
   1.900 +    //       history and it is always executed.
   1.901 +    let query = this.hasBehavior("tag") ? SQL_TAGS_QUERY :
   1.902 +                this.hasBehavior("bookmark") ? SQL_BOOKMARK_QUERY :
   1.903 +                this.hasBehavior("typed") ? SQL_TYPED_QUERY :
   1.904 +                this.hasBehavior("history") ? SQL_HISTORY_QUERY :
   1.905 +                SQL_DEFAULT_QUERY;
   1.906 +
   1.907 +    return [
   1.908 +      query,
   1.909 +      {
   1.910 +        parent: PlacesUtils.tagsFolderId,
   1.911 +        query_type: QUERYTYPE_FILTERED,
   1.912 +        matchBehavior: this._matchBehavior,
   1.913 +        searchBehavior: this._behavior,
   1.914 +        // We only want to search the tokens that we are left with - not the
   1.915 +        // original search string.
   1.916 +        searchString: this._searchTokens.join(" "),
   1.917 +        // Limit the query to the the maximum number of desired results.
   1.918 +        // This way we can avoid doing more work than needed.
   1.919 +        maxResults: Prefs.maxRichResults
   1.920 +      }
   1.921 +    ];
   1.922 +  },
   1.923 +
   1.924 +  /**
   1.925 +   * Obtains the query to search for keywords.
   1.926 +   *
   1.927 +   * @return an array consisting of the correctly optimized query to search the
   1.928 +   *         database with and an object containing the params to bound.
   1.929 +   */
   1.930 +  get _keywordQuery() {
   1.931 +    // The keyword is the first word in the search string, with the parameters
   1.932 +    // following it.
   1.933 +    let searchString = this._originalSearchString;
   1.934 +    let queryString = "";
   1.935 +    let queryIndex = searchString.indexOf(" ");
   1.936 +    if (queryIndex != -1) {
   1.937 +      queryString = searchString.substring(queryIndex + 1);
   1.938 +    }
   1.939 +    // We need to escape the parameters as if they were the query in a URL
   1.940 +    queryString = encodeURIComponent(queryString).replace("%20", "+", "g");
   1.941 +
   1.942 +    // The first word could be a keyword, so that's what we'll search.
   1.943 +    let keyword = this._searchTokens[0];
   1.944 +
   1.945 +    return [
   1.946 +      SQL_KEYWORD_QUERY,
   1.947 +      {
   1.948 +        keyword: keyword,
   1.949 +        query_string: queryString,
   1.950 +        query_type: QUERYTYPE_KEYWORD
   1.951 +      }
   1.952 +    ];
   1.953 +  },
   1.954 +
   1.955 +  /**
   1.956 +   * Obtains the query to search for switch-to-tab entries.
   1.957 +   *
   1.958 +   * @return an array consisting of the correctly optimized query to search the
   1.959 +   *         database with and an object containing the params to bound.
   1.960 +   */
   1.961 +  get _switchToTabQuery() [
   1.962 +    SQL_SWITCHTAB_QUERY,
   1.963 +    {
   1.964 +      query_type: QUERYTYPE_FILTERED,
   1.965 +      matchBehavior: this._matchBehavior,
   1.966 +      searchBehavior: this._behavior,
   1.967 +      // We only want to search the tokens that we are left with - not the
   1.968 +      // original search string.
   1.969 +      searchString: this._searchTokens.join(" "),
   1.970 +      maxResults: Prefs.maxRichResults
   1.971 +    }
   1.972 +  ],
   1.973 +
   1.974 +  /**
   1.975 +   * Obtains the query to search for adaptive results.
   1.976 +   *
   1.977 +   * @return an array consisting of the correctly optimized query to search the
   1.978 +   *         database with and an object containing the params to bound.
   1.979 +   */
   1.980 +  get _adaptiveQuery() [
   1.981 +    SQL_ADAPTIVE_QUERY,
   1.982 +    {
   1.983 +      parent: PlacesUtils.tagsFolderId,
   1.984 +      search_string: this._searchString,
   1.985 +      query_type: QUERYTYPE_FILTERED,
   1.986 +      matchBehavior: this._matchBehavior,
   1.987 +      searchBehavior: this._behavior
   1.988 +    }
   1.989 +  ],
   1.990 +
   1.991 +  /**
   1.992 +   * Whether we should try to autoFill.
   1.993 +   */
   1.994 +  get _shouldAutofill() {
   1.995 +    // First of all, check for the autoFill pref.
   1.996 +    if (!Prefs.autofill)
   1.997 +      return false;
   1.998 +
   1.999 +    // Then, we should not try to autofill if the behavior is not the default.
  1.1000 +    // TODO (bug 751709): Ideally we should have a more fine-grained behavior
  1.1001 +    // here, but for now it's enough to just check for default behavior.
  1.1002 +    if (Prefs.defaultBehavior != DEFAULT_BEHAVIOR)
  1.1003 +      return false;
  1.1004 +
  1.1005 +    // Don't autoFill if the search term is recognized as a keyword, otherwise
  1.1006 +    // it will override default keywords behavior.  Note that keywords are
  1.1007 +    // hashed on first use, so while the first query may delay a little bit,
  1.1008 +    // next ones will just hit the memory hash.
  1.1009 +    if (this._searchString.length == 0 ||
  1.1010 +        PlacesUtils.bookmarks.getURIForKeyword(this._searchString)) {
  1.1011 +      return false;
  1.1012 +    }
  1.1013 +
  1.1014 +    // Don't try to autofill if the search term includes any whitespace.
  1.1015 +    // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
  1.1016 +    // tokenizer ends up trimming the search string and returning a value
  1.1017 +    // that doesn't match it, or is even shorter.
  1.1018 +    if (/\s/.test(this._searchString)) {
  1.1019 +      return false;
  1.1020 +    }
  1.1021 +
  1.1022 +    return true;
  1.1023 +  },
  1.1024 +
  1.1025 +  /**
  1.1026 +   * Obtains the query to search for autoFill host results.
  1.1027 +   *
  1.1028 +   * @return an array consisting of the correctly optimized query to search the
  1.1029 +   *         database with and an object containing the params to bound.
  1.1030 +   */
  1.1031 +  get _hostQuery() [
  1.1032 +    Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY,
  1.1033 +    {
  1.1034 +      query_type: QUERYTYPE_AUTOFILL_HOST,
  1.1035 +      searchString: this._searchString.toLowerCase()
  1.1036 +    }
  1.1037 +  ],
  1.1038 +
  1.1039 +  /**
  1.1040 +   * Obtains the query to search for autoFill url results.
  1.1041 +   *
  1.1042 +   * @return an array consisting of the correctly optimized query to search the
  1.1043 +   *         database with and an object containing the params to bound.
  1.1044 +   */
  1.1045 +  get _urlQuery() [
  1.1046 +    Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY,
  1.1047 +    {
  1.1048 +      query_type: QUERYTYPE_AUTOFILL_URL,
  1.1049 +      searchString: this._autofillUrlSearchString,
  1.1050 +      matchBehavior: MATCH_BEGINNING_CASE_SENSITIVE,
  1.1051 +      searchBehavior: Ci.mozIPlacesAutoComplete.BEHAVIOR_URL
  1.1052 +    }
  1.1053 +  ],
  1.1054 +
  1.1055 + /**
  1.1056 +   * Notifies the listener about results.
  1.1057 +   *
  1.1058 +   * @param searchOngoing
  1.1059 +   *        Indicates whether the search is ongoing.
  1.1060 +   */
  1.1061 +  notifyResults: function (searchOngoing) {
  1.1062 +    let result = this._result;
  1.1063 +    let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
  1.1064 +    if (searchOngoing) {
  1.1065 +      resultCode += "_ONGOING";
  1.1066 +    }
  1.1067 +    result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
  1.1068 +    this._listener.onSearchResult(this._autocompleteSearch, result);
  1.1069 +  },
  1.1070 +}
  1.1071 +
  1.1072 +////////////////////////////////////////////////////////////////////////////////
  1.1073 +//// UnifiedComplete class
  1.1074 +//// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete
  1.1075 +
  1.1076 +function UnifiedComplete() {
  1.1077 +  Services.obs.addObserver(this, TOPIC_SHUTDOWN, true);
  1.1078 +}
  1.1079 +
  1.1080 +UnifiedComplete.prototype = {
  1.1081 +  //////////////////////////////////////////////////////////////////////////////
  1.1082 +  //// nsIObserver
  1.1083 +
  1.1084 +  observe: function (subject, topic, data) {
  1.1085 +    if (topic === TOPIC_SHUTDOWN) {
  1.1086 +      this.ensureShutdown();
  1.1087 +    }
  1.1088 +  },
  1.1089 +
  1.1090 +  //////////////////////////////////////////////////////////////////////////////
  1.1091 +  //// Database handling
  1.1092 +
  1.1093 +  /**
  1.1094 +   * Promise resolved when the database initialization has completed, or null
  1.1095 +   * if it has never been requested.
  1.1096 +   */
  1.1097 +  _promiseDatabase: null,
  1.1098 +
  1.1099 +  /**
  1.1100 +   * Gets a Sqlite database handle.
  1.1101 +   *
  1.1102 +   * @return {Promise}
  1.1103 +   * @resolves to the Sqlite database handle (according to Sqlite.jsm).
  1.1104 +   * @rejects javascript exception.
  1.1105 +   */
  1.1106 +  getDatabaseHandle: function () {
  1.1107 +    if (Prefs.enabled && !this._promiseDatabase) {
  1.1108 +      this._promiseDatabase = Task.spawn(function* () {
  1.1109 +        let conn = yield Sqlite.cloneStorageConnection({
  1.1110 +          connection: PlacesUtils.history.DBConnection,
  1.1111 +          readOnly: true
  1.1112 +        });
  1.1113 +
  1.1114 +        // Autocomplete often fallbacks to a table scan due to lack of text
  1.1115 +        // indices.  A larger cache helps reducing IO and improving performance.
  1.1116 +        // The value used here is larger than the default Storage value defined
  1.1117 +        // as MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp.
  1.1118 +        yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB
  1.1119 +
  1.1120 +        yield SwitchToTabStorage.initDatabase(conn);
  1.1121 +
  1.1122 +        return conn;
  1.1123 +      }.bind(this)).then(null, Cu.reportError);
  1.1124 +    }
  1.1125 +    return this._promiseDatabase;
  1.1126 +  },
  1.1127 +
  1.1128 +  /**
  1.1129 +   * Used to stop running queries and close the database handle.
  1.1130 +   */
  1.1131 +  ensureShutdown: function () {
  1.1132 +    if (this._promiseDatabase) {
  1.1133 +      Task.spawn(function* () {
  1.1134 +        let conn = yield this.getDatabaseHandle();
  1.1135 +        SwitchToTabStorage.shutdown();
  1.1136 +        yield conn.close()
  1.1137 +      }.bind(this)).then(null, Cu.reportError);
  1.1138 +      this._promiseDatabase = null;
  1.1139 +    }
  1.1140 +  },
  1.1141 +
  1.1142 +  //////////////////////////////////////////////////////////////////////////////
  1.1143 +  //// mozIPlacesAutoComplete
  1.1144 +
  1.1145 +  registerOpenPage: function PAC_registerOpenPage(uri) {
  1.1146 +    SwitchToTabStorage.add(uri);
  1.1147 +  },
  1.1148 +
  1.1149 +  unregisterOpenPage: function PAC_unregisterOpenPage(uri) {
  1.1150 +    SwitchToTabStorage.delete(uri);
  1.1151 +  },
  1.1152 +
  1.1153 +  //////////////////////////////////////////////////////////////////////////////
  1.1154 +  //// nsIAutoCompleteSearch
  1.1155 +
  1.1156 +  startSearch: function (searchString, searchParam, previousResult, listener) {
  1.1157 +    // Stop the search in case the controller has not taken care of it.
  1.1158 +    if (this._currentSearch) {
  1.1159 +      this.stopSearch();
  1.1160 +    }
  1.1161 +
  1.1162 +    // Note: We don't use previousResult to make sure ordering of results are
  1.1163 +    //       consistent.  See bug 412730 for more details.
  1.1164 +
  1.1165 +    this._currentSearch = new Search(searchString, searchParam, listener,
  1.1166 +                                     this, this);
  1.1167 +
  1.1168 +    // If we are not enabled, we need to return now.  Notice we need an empty
  1.1169 +    // result regardless, so we still create the Search object.
  1.1170 +    if (!Prefs.enabled) {
  1.1171 +      this.finishSearch(true);
  1.1172 +      return;
  1.1173 +    }
  1.1174 +
  1.1175 +    let search = this._currentSearch;
  1.1176 +    this.getDatabaseHandle().then(conn => search.execute(conn))
  1.1177 +                            .then(() => {
  1.1178 +                              if (search == this._currentSearch) {
  1.1179 +                                this.finishSearch(true);
  1.1180 +                              }
  1.1181 +                            }, Cu.reportError);
  1.1182 +  },
  1.1183 +
  1.1184 +  stopSearch: function () {
  1.1185 +    if (this._currentSearch) {
  1.1186 +      this._currentSearch.cancel();
  1.1187 +    }
  1.1188 +    this.finishSearch();
  1.1189 +  },
  1.1190 +
  1.1191 +  /**
  1.1192 +   * Properly cleans up when searching is completed.
  1.1193 +   *
  1.1194 +   * @param notify [optional]
  1.1195 +   *        Indicates if we should notify the AutoComplete listener about our
  1.1196 +   *        results or not.
  1.1197 +   */
  1.1198 +  finishSearch: function (notify=false) {
  1.1199 +    // Notify about results if we are supposed to.
  1.1200 +    if (notify) {
  1.1201 +      this._currentSearch.notifyResults(false);
  1.1202 +    }
  1.1203 +
  1.1204 +    // Clear our state
  1.1205 +    TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT);
  1.1206 +    delete this._currentSearch;
  1.1207 +  },
  1.1208 +
  1.1209 +  //////////////////////////////////////////////////////////////////////////////
  1.1210 +  //// nsIAutoCompleteSimpleResultListener
  1.1211 +
  1.1212 +  onValueRemoved: function (result, spec, removeFromDB) {
  1.1213 +    if (removeFromDB) {
  1.1214 +      PlacesUtils.history.removePage(NetUtil.newURI(spec));
  1.1215 +    }
  1.1216 +  },
  1.1217 +
  1.1218 +  //////////////////////////////////////////////////////////////////////////////
  1.1219 +  //// nsIAutoCompleteSearchDescriptor
  1.1220 +
  1.1221 +  get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE,
  1.1222 +
  1.1223 +  //////////////////////////////////////////////////////////////////////////////
  1.1224 +  //// nsISupports
  1.1225 +
  1.1226 +  classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"),
  1.1227 +
  1.1228 +  _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete),
  1.1229 +
  1.1230 +  QueryInterface: XPCOMUtils.generateQI([
  1.1231 +    Ci.nsIAutoCompleteSearch,
  1.1232 +    Ci.nsIAutoCompleteSimpleResultListener,
  1.1233 +    Ci.mozIPlacesAutoComplete,
  1.1234 +    Ci.nsIObserver,
  1.1235 +    Ci.nsISupportsWeakReference
  1.1236 +  ])
  1.1237 +};
  1.1238 +
  1.1239 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]);

mercurial