toolkit/components/places/nsPlacesAutoComplete.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
michael@0 2 * vim: sw=2 ts=2 sts=2 expandtab
michael@0 3 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 8 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 9 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
michael@0 10 "resource://gre/modules/PlacesUtils.jsm");
michael@0 11 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
michael@0 12 "resource://gre/modules/TelemetryStopwatch.jsm");
michael@0 13 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 14 "resource://gre/modules/NetUtil.jsm");
michael@0 15
michael@0 16 ////////////////////////////////////////////////////////////////////////////////
michael@0 17 //// Constants
michael@0 18
michael@0 19 const Cc = Components.classes;
michael@0 20 const Ci = Components.interfaces;
michael@0 21 const Cr = Components.results;
michael@0 22
michael@0 23 // This SQL query fragment provides the following:
michael@0 24 // - whether the entry is bookmarked (kQueryIndexBookmarked)
michael@0 25 // - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle)
michael@0 26 // - the tags associated with a bookmarked entry (kQueryIndexTags)
michael@0 27 const kBookTagSQLFragment =
michael@0 28 "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked, "
michael@0 29 + "( "
michael@0 30 + "SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL "
michael@0 31 + "ORDER BY lastModified DESC LIMIT 1 "
michael@0 32 + ") AS btitle, "
michael@0 33 + "( "
michael@0 34 + "SELECT GROUP_CONCAT(t.title, ',') "
michael@0 35 + "FROM moz_bookmarks b "
michael@0 36 + "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent "
michael@0 37 + "WHERE b.fk = h.id "
michael@0 38 + ") AS tags";
michael@0 39
michael@0 40 // observer topics
michael@0 41 const kTopicShutdown = "places-shutdown";
michael@0 42 const kPrefChanged = "nsPref:changed";
michael@0 43
michael@0 44 // Match type constants. These indicate what type of search function we should
michael@0 45 // be using.
michael@0 46 const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
michael@0 47 const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
michael@0 48 const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
michael@0 49 const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
michael@0 50 const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
michael@0 51
michael@0 52 // AutoComplete index constants. All AutoComplete queries will provide these
michael@0 53 // columns in this order.
michael@0 54 const kQueryIndexURL = 0;
michael@0 55 const kQueryIndexTitle = 1;
michael@0 56 const kQueryIndexFaviconURL = 2;
michael@0 57 const kQueryIndexBookmarked = 3;
michael@0 58 const kQueryIndexBookmarkTitle = 4;
michael@0 59 const kQueryIndexTags = 5;
michael@0 60 const kQueryIndexVisitCount = 6;
michael@0 61 const kQueryIndexTyped = 7;
michael@0 62 const kQueryIndexPlaceId = 8;
michael@0 63 const kQueryIndexQueryType = 9;
michael@0 64 const kQueryIndexOpenPageCount = 10;
michael@0 65
michael@0 66 // AutoComplete query type constants. Describes the various types of queries
michael@0 67 // that we can process.
michael@0 68 const kQueryTypeKeyword = 0;
michael@0 69 const kQueryTypeFiltered = 1;
michael@0 70
michael@0 71 // This separator is used as an RTL-friendly way to split the title and tags.
michael@0 72 // It can also be used by an nsIAutoCompleteResult consumer to re-split the
michael@0 73 // "comment" back into the title and the tag.
michael@0 74 const kTitleTagsSeparator = " \u2013 ";
michael@0 75
michael@0 76 const kBrowserUrlbarBranch = "browser.urlbar.";
michael@0 77 // Toggle autocomplete.
michael@0 78 const kBrowserUrlbarAutocompleteEnabledPref = "autocomplete.enabled";
michael@0 79 // Toggle autoFill.
michael@0 80 const kBrowserUrlbarAutofillPref = "autoFill";
michael@0 81 // Whether to search only typed entries.
michael@0 82 const kBrowserUrlbarAutofillTypedPref = "autoFill.typed";
michael@0 83
michael@0 84 // The Telemetry histogram for urlInlineComplete query on domain
michael@0 85 const DOMAIN_QUERY_TELEMETRY = "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS";
michael@0 86
michael@0 87 ////////////////////////////////////////////////////////////////////////////////
michael@0 88 //// Globals
michael@0 89
michael@0 90 XPCOMUtils.defineLazyServiceGetter(this, "gTextURIService",
michael@0 91 "@mozilla.org/intl/texttosuburi;1",
michael@0 92 "nsITextToSubURI");
michael@0 93
michael@0 94 ////////////////////////////////////////////////////////////////////////////////
michael@0 95 //// Helpers
michael@0 96
michael@0 97 /**
michael@0 98 * Initializes our temporary table on a given database.
michael@0 99 *
michael@0 100 * @param aDatabase
michael@0 101 * The mozIStorageConnection to set up the temp table on.
michael@0 102 */
michael@0 103 function initTempTable(aDatabase)
michael@0 104 {
michael@0 105 // Note: this should be kept up-to-date with the definition in
michael@0 106 // nsPlacesTables.h.
michael@0 107 let stmt = aDatabase.createAsyncStatement(
michael@0 108 "CREATE TEMP TABLE moz_openpages_temp ( "
michael@0 109 + " url TEXT PRIMARY KEY "
michael@0 110 + ", open_count INTEGER "
michael@0 111 + ") "
michael@0 112 );
michael@0 113 stmt.executeAsync();
michael@0 114 stmt.finalize();
michael@0 115
michael@0 116 // Note: this should be kept up-to-date with the definition in
michael@0 117 // nsPlacesTriggers.h.
michael@0 118 stmt = aDatabase.createAsyncStatement(
michael@0 119 "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger "
michael@0 120 + "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW "
michael@0 121 + "WHEN NEW.open_count = 0 "
michael@0 122 + "BEGIN "
michael@0 123 + "DELETE FROM moz_openpages_temp "
michael@0 124 + "WHERE url = NEW.url; "
michael@0 125 + "END "
michael@0 126 );
michael@0 127 stmt.executeAsync();
michael@0 128 stmt.finalize();
michael@0 129 }
michael@0 130
michael@0 131 /**
michael@0 132 * Used to unescape encoded URI strings, and drop information that we do not
michael@0 133 * care about for searching.
michael@0 134 *
michael@0 135 * @param aURIString
michael@0 136 * The text to unescape and modify.
michael@0 137 * @return the modified uri.
michael@0 138 */
michael@0 139 function fixupSearchText(aURIString)
michael@0 140 {
michael@0 141 let uri = stripPrefix(aURIString);
michael@0 142 return gTextURIService.unEscapeURIForUI("UTF-8", uri);
michael@0 143 }
michael@0 144
michael@0 145 /**
michael@0 146 * Strip prefixes from the URI that we don't care about for searching.
michael@0 147 *
michael@0 148 * @param aURIString
michael@0 149 * The text to modify.
michael@0 150 * @return the modified uri.
michael@0 151 */
michael@0 152 function stripPrefix(aURIString)
michael@0 153 {
michael@0 154 let uri = aURIString;
michael@0 155
michael@0 156 if (uri.indexOf("http://") == 0) {
michael@0 157 uri = uri.slice(7);
michael@0 158 }
michael@0 159 else if (uri.indexOf("https://") == 0) {
michael@0 160 uri = uri.slice(8);
michael@0 161 }
michael@0 162 else if (uri.indexOf("ftp://") == 0) {
michael@0 163 uri = uri.slice(6);
michael@0 164 }
michael@0 165
michael@0 166 if (uri.indexOf("www.") == 0) {
michael@0 167 uri = uri.slice(4);
michael@0 168 }
michael@0 169 return uri;
michael@0 170 }
michael@0 171
michael@0 172 /**
michael@0 173 * safePrefGetter get the pref with typo safety.
michael@0 174 * This will return the default value provided if no pref is set.
michael@0 175 *
michael@0 176 * @param aPrefBranch
michael@0 177 * The nsIPrefBranch containing the required preference
michael@0 178 * @param aName
michael@0 179 * A preference name
michael@0 180 * @param aDefault
michael@0 181 * The preference's default value
michael@0 182 * @return the preference value or provided default
michael@0 183 */
michael@0 184
michael@0 185 function safePrefGetter(aPrefBranch, aName, aDefault) {
michael@0 186 let types = {
michael@0 187 boolean: "Bool",
michael@0 188 number: "Int",
michael@0 189 string: "Char"
michael@0 190 };
michael@0 191 let type = types[typeof(aDefault)];
michael@0 192 if (!type) {
michael@0 193 throw "Unknown type!";
michael@0 194 }
michael@0 195 // If the pref isn't set, we want to use the default.
michael@0 196 try {
michael@0 197 return aPrefBranch["get" + type + "Pref"](aName);
michael@0 198 }
michael@0 199 catch (e) {
michael@0 200 return aDefault;
michael@0 201 }
michael@0 202 }
michael@0 203
michael@0 204
michael@0 205 ////////////////////////////////////////////////////////////////////////////////
michael@0 206 //// AutoCompleteStatementCallbackWrapper class
michael@0 207
michael@0 208 /**
michael@0 209 * Wraps a callback and ensures that handleCompletion is not dispatched if the
michael@0 210 * query is no longer tracked.
michael@0 211 *
michael@0 212 * @param aAutocomplete
michael@0 213 * A reference to a nsPlacesAutoComplete.
michael@0 214 * @param aCallback
michael@0 215 * A reference to a mozIStorageStatementCallback
michael@0 216 * @param aDBConnection
michael@0 217 * The database connection to execute the queries on.
michael@0 218 */
michael@0 219 function AutoCompleteStatementCallbackWrapper(aAutocomplete, aCallback,
michael@0 220 aDBConnection)
michael@0 221 {
michael@0 222 this._autocomplete = aAutocomplete;
michael@0 223 this._callback = aCallback;
michael@0 224 this._db = aDBConnection;
michael@0 225 }
michael@0 226
michael@0 227 AutoCompleteStatementCallbackWrapper.prototype = {
michael@0 228 //////////////////////////////////////////////////////////////////////////////
michael@0 229 //// mozIStorageStatementCallback
michael@0 230
michael@0 231 handleResult: function ACSCW_handleResult(aResultSet)
michael@0 232 {
michael@0 233 this._callback.handleResult.apply(this._callback, arguments);
michael@0 234 },
michael@0 235
michael@0 236 handleError: function ACSCW_handleError(aError)
michael@0 237 {
michael@0 238 this._callback.handleError.apply(this._callback, arguments);
michael@0 239 },
michael@0 240
michael@0 241 handleCompletion: function ACSCW_handleCompletion(aReason)
michael@0 242 {
michael@0 243 // Only dispatch handleCompletion if we are not done searching and are a
michael@0 244 // pending search.
michael@0 245 if (!this._autocomplete.isSearchComplete() &&
michael@0 246 this._autocomplete.isPendingSearch(this._handle)) {
michael@0 247 this._callback.handleCompletion.apply(this._callback, arguments);
michael@0 248 }
michael@0 249 },
michael@0 250
michael@0 251 //////////////////////////////////////////////////////////////////////////////
michael@0 252 //// AutoCompleteStatementCallbackWrapper
michael@0 253
michael@0 254 /**
michael@0 255 * Executes the specified query asynchronously. This object will notify
michael@0 256 * this._callback if we should notify (logic explained in handleCompletion).
michael@0 257 *
michael@0 258 * @param aQueries
michael@0 259 * The queries to execute asynchronously.
michael@0 260 * @return a mozIStoragePendingStatement that can be used to cancel the
michael@0 261 * queries.
michael@0 262 */
michael@0 263 executeAsync: function ACSCW_executeAsync(aQueries)
michael@0 264 {
michael@0 265 return this._handle = this._db.executeAsync(aQueries, aQueries.length,
michael@0 266 this);
michael@0 267 },
michael@0 268
michael@0 269 //////////////////////////////////////////////////////////////////////////////
michael@0 270 //// nsISupports
michael@0 271
michael@0 272 QueryInterface: XPCOMUtils.generateQI([
michael@0 273 Ci.mozIStorageStatementCallback,
michael@0 274 ])
michael@0 275 };
michael@0 276
michael@0 277 ////////////////////////////////////////////////////////////////////////////////
michael@0 278 //// nsPlacesAutoComplete class
michael@0 279 //// @mozilla.org/autocomplete/search;1?name=history
michael@0 280
michael@0 281 function nsPlacesAutoComplete()
michael@0 282 {
michael@0 283 //////////////////////////////////////////////////////////////////////////////
michael@0 284 //// Shared Constants for Smart Getters
michael@0 285
michael@0 286 // TODO bug 412736 in case of a frecency tie, break it with h.typed and
michael@0 287 // h.visit_count which is better than nothing. This is slow, so not doing it
michael@0 288 // yet...
michael@0 289 const SQL_BASE = "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", "
michael@0 290 + "h.visit_count, h.typed, h.id, :query_type, "
michael@0 291 + "t.open_count "
michael@0 292 + "FROM moz_places h "
michael@0 293 + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id "
michael@0 294 + "LEFT JOIN moz_openpages_temp t ON t.url = h.url "
michael@0 295 + "WHERE h.frecency <> 0 "
michael@0 296 + "AND AUTOCOMPLETE_MATCH(:searchString, h.url, "
michael@0 297 + "IFNULL(btitle, h.title), tags, "
michael@0 298 + "h.visit_count, h.typed, "
michael@0 299 + "bookmarked, t.open_count, "
michael@0 300 + ":matchBehavior, :searchBehavior) "
michael@0 301 + "{ADDITIONAL_CONDITIONS} "
michael@0 302 + "ORDER BY h.frecency DESC, h.id DESC "
michael@0 303 + "LIMIT :maxResults";
michael@0 304
michael@0 305 //////////////////////////////////////////////////////////////////////////////
michael@0 306 //// Smart Getters
michael@0 307
michael@0 308 XPCOMUtils.defineLazyGetter(this, "_db", function() {
michael@0 309 // Get a cloned, read-only version of the database. We'll only ever write
michael@0 310 // to our own in-memory temp table, and having a cloned copy means we do not
michael@0 311 // run the risk of our queries taking longer due to the main database
michael@0 312 // connection performing a long-running task.
michael@0 313 let db = PlacesUtils.history.DBConnection.clone(true);
michael@0 314
michael@0 315 // Autocomplete often fallbacks to a table scan due to lack of text indices.
michael@0 316 // In such cases a larger cache helps reducing IO. The default Storage
michael@0 317 // value is MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp.
michael@0 318 let stmt = db.createAsyncStatement("PRAGMA cache_size = -6144"); // 6MiB
michael@0 319 stmt.executeAsync();
michael@0 320 stmt.finalize();
michael@0 321
michael@0 322 // Create our in-memory tables for tab tracking.
michael@0 323 initTempTable(db);
michael@0 324
michael@0 325 // Populate the table with current open pages cache contents.
michael@0 326 if (this._openPagesCache.length > 0) {
michael@0 327 // Avoid getter re-entrance from the _registerOpenPageQuery lazy getter.
michael@0 328 let stmt = this._registerOpenPageQuery =
michael@0 329 db.createAsyncStatement(this._registerOpenPageQuerySQL);
michael@0 330 let params = stmt.newBindingParamsArray();
michael@0 331 for (let i = 0; i < this._openPagesCache.length; i++) {
michael@0 332 let bp = params.newBindingParams();
michael@0 333 bp.bindByName("page_url", this._openPagesCache[i]);
michael@0 334 params.addParams(bp);
michael@0 335 }
michael@0 336 stmt.bindParameters(params);
michael@0 337 stmt.executeAsync();
michael@0 338 stmt.finalize();
michael@0 339 delete this._openPagesCache;
michael@0 340 }
michael@0 341
michael@0 342 return db;
michael@0 343 });
michael@0 344
michael@0 345 XPCOMUtils.defineLazyGetter(this, "_defaultQuery", function() {
michael@0 346 let replacementText = "";
michael@0 347 return this._db.createAsyncStatement(
michael@0 348 SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
michael@0 349 );
michael@0 350 });
michael@0 351
michael@0 352 XPCOMUtils.defineLazyGetter(this, "_historyQuery", function() {
michael@0 353 // Enforce ignoring the visit_count index, since the frecency one is much
michael@0 354 // faster in this case. ANALYZE helps the query planner to figure out the
michael@0 355 // faster path, but it may not have run yet.
michael@0 356 let replacementText = "AND +h.visit_count > 0";
michael@0 357 return this._db.createAsyncStatement(
michael@0 358 SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
michael@0 359 );
michael@0 360 });
michael@0 361
michael@0 362 XPCOMUtils.defineLazyGetter(this, "_bookmarkQuery", function() {
michael@0 363 let replacementText = "AND bookmarked";
michael@0 364 return this._db.createAsyncStatement(
michael@0 365 SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
michael@0 366 );
michael@0 367 });
michael@0 368
michael@0 369 XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() {
michael@0 370 let replacementText = "AND tags IS NOT NULL";
michael@0 371 return this._db.createAsyncStatement(
michael@0 372 SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
michael@0 373 );
michael@0 374 });
michael@0 375
michael@0 376 XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() {
michael@0 377 return this._db.createAsyncStatement(
michael@0 378 "SELECT t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL, "
michael@0 379 + ":query_type, t.open_count, NULL "
michael@0 380 + "FROM moz_openpages_temp t "
michael@0 381 + "LEFT JOIN moz_places h ON h.url = t.url "
michael@0 382 + "WHERE h.id IS NULL "
michael@0 383 + "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL, "
michael@0 384 + "NULL, NULL, NULL, t.open_count, "
michael@0 385 + ":matchBehavior, :searchBehavior) "
michael@0 386 + "ORDER BY t.ROWID DESC "
michael@0 387 + "LIMIT :maxResults "
michael@0 388 );
michael@0 389 });
michael@0 390
michael@0 391 XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() {
michael@0 392 let replacementText = "AND h.typed = 1";
michael@0 393 return this._db.createAsyncStatement(
michael@0 394 SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
michael@0 395 );
michael@0 396 });
michael@0 397
michael@0 398 XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() {
michael@0 399 return this._db.createAsyncStatement(
michael@0 400 "/* do not warn (bug 487789) */ "
michael@0 401 + "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", "
michael@0 402 + "h.visit_count, h.typed, h.id, :query_type, t.open_count "
michael@0 403 + "FROM ( "
michael@0 404 + "SELECT ROUND( "
michael@0 405 + "MAX(use_count) * (1 + (input = :search_string)), 1 "
michael@0 406 + ") AS rank, place_id "
michael@0 407 + "FROM moz_inputhistory "
michael@0 408 + "WHERE input BETWEEN :search_string AND :search_string || X'FFFF' "
michael@0 409 + "GROUP BY place_id "
michael@0 410 + ") AS i "
michael@0 411 + "JOIN moz_places h ON h.id = i.place_id "
michael@0 412 + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id "
michael@0 413 + "LEFT JOIN moz_openpages_temp t ON t.url = h.url "
michael@0 414 + "WHERE AUTOCOMPLETE_MATCH(NULL, h.url, "
michael@0 415 + "IFNULL(btitle, h.title), tags, "
michael@0 416 + "h.visit_count, h.typed, bookmarked, "
michael@0 417 + "t.open_count, "
michael@0 418 + ":matchBehavior, :searchBehavior) "
michael@0 419 + "ORDER BY rank DESC, h.frecency DESC "
michael@0 420 );
michael@0 421 });
michael@0 422
michael@0 423 XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() {
michael@0 424 return this._db.createAsyncStatement(
michael@0 425 "/* do not warn (bug 487787) */ "
michael@0 426 + "SELECT "
michael@0 427 + "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) "
michael@0 428 + "AS search_url, h.title, "
michael@0 429 + "IFNULL(f.url, (SELECT f.url "
michael@0 430 + "FROM moz_places "
michael@0 431 + "JOIN moz_favicons f ON f.id = favicon_id "
michael@0 432 + "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk) "
michael@0 433 + "ORDER BY frecency DESC "
michael@0 434 + "LIMIT 1) "
michael@0 435 + "), 1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk), "
michael@0 436 + ":query_type, t.open_count "
michael@0 437 + "FROM moz_keywords k "
michael@0 438 + "JOIN moz_bookmarks b ON b.keyword_id = k.id "
michael@0 439 + "LEFT JOIN moz_places h ON h.url = search_url "
michael@0 440 + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id "
michael@0 441 + "LEFT JOIN moz_openpages_temp t ON t.url = search_url "
michael@0 442 + "WHERE LOWER(k.keyword) = LOWER(:keyword) "
michael@0 443 + "ORDER BY h.frecency DESC "
michael@0 444 );
michael@0 445 });
michael@0 446
michael@0 447 this._registerOpenPageQuerySQL = "INSERT OR REPLACE INTO moz_openpages_temp "
michael@0 448 + "(url, open_count) "
michael@0 449 + "VALUES (:page_url, "
michael@0 450 + "IFNULL("
michael@0 451 + "("
michael@0 452 + "SELECT open_count + 1 "
michael@0 453 + "FROM moz_openpages_temp "
michael@0 454 + "WHERE url = :page_url "
michael@0 455 + "), "
michael@0 456 + "1"
michael@0 457 + ")"
michael@0 458 + ")";
michael@0 459 XPCOMUtils.defineLazyGetter(this, "_registerOpenPageQuery", function() {
michael@0 460 return this._db.createAsyncStatement(this._registerOpenPageQuerySQL);
michael@0 461 });
michael@0 462
michael@0 463 XPCOMUtils.defineLazyGetter(this, "_unregisterOpenPageQuery", function() {
michael@0 464 return this._db.createAsyncStatement(
michael@0 465 "UPDATE moz_openpages_temp "
michael@0 466 + "SET open_count = open_count - 1 "
michael@0 467 + "WHERE url = :page_url"
michael@0 468 );
michael@0 469 });
michael@0 470
michael@0 471 //////////////////////////////////////////////////////////////////////////////
michael@0 472 //// Initialization
michael@0 473
michael@0 474 // load preferences
michael@0 475 this._prefs = Cc["@mozilla.org/preferences-service;1"].
michael@0 476 getService(Ci.nsIPrefService).
michael@0 477 getBranch(kBrowserUrlbarBranch);
michael@0 478 this._loadPrefs(true);
michael@0 479
michael@0 480 // register observers
michael@0 481 this._os = Cc["@mozilla.org/observer-service;1"].
michael@0 482 getService(Ci.nsIObserverService);
michael@0 483 this._os.addObserver(this, kTopicShutdown, false);
michael@0 484
michael@0 485 }
michael@0 486
michael@0 487 nsPlacesAutoComplete.prototype = {
michael@0 488 //////////////////////////////////////////////////////////////////////////////
michael@0 489 //// nsIAutoCompleteSearch
michael@0 490
michael@0 491 startSearch: function PAC_startSearch(aSearchString, aSearchParam,
michael@0 492 aPreviousResult, aListener)
michael@0 493 {
michael@0 494 // Stop the search in case the controller has not taken care of it.
michael@0 495 this.stopSearch();
michael@0 496
michael@0 497 // Note: We don't use aPreviousResult to make sure ordering of results are
michael@0 498 // consistent. See bug 412730 for more details.
michael@0 499
michael@0 500 // We want to store the original string with no leading or trailing
michael@0 501 // whitespace for case sensitive searches.
michael@0 502 this._originalSearchString = aSearchString.trim();
michael@0 503
michael@0 504 this._currentSearchString =
michael@0 505 fixupSearchText(this._originalSearchString.toLowerCase());
michael@0 506
michael@0 507 let searchParamParts = aSearchParam.split(" ");
michael@0 508 this._enableActions = searchParamParts.indexOf("enable-actions") != -1;
michael@0 509
michael@0 510 this._listener = aListener;
michael@0 511 let result = Cc["@mozilla.org/autocomplete/simple-result;1"].
michael@0 512 createInstance(Ci.nsIAutoCompleteSimpleResult);
michael@0 513 result.setSearchString(aSearchString);
michael@0 514 result.setListener(this);
michael@0 515 this._result = result;
michael@0 516
michael@0 517 // If we are not enabled, we need to return now.
michael@0 518 if (!this._enabled) {
michael@0 519 this._finishSearch(true);
michael@0 520 return;
michael@0 521 }
michael@0 522
michael@0 523 // Reset our search behavior to the default.
michael@0 524 if (this._currentSearchString) {
michael@0 525 this._behavior = this._defaultBehavior;
michael@0 526 }
michael@0 527 else {
michael@0 528 this._behavior = this._emptySearchDefaultBehavior;
michael@0 529 }
michael@0 530 // For any given search, we run up to four queries:
michael@0 531 // 1) keywords (this._keywordQuery)
michael@0 532 // 2) adaptive learning (this._adaptiveQuery)
michael@0 533 // 3) open pages not supported by history (this._openPagesQuery)
michael@0 534 // 4) query from this._getSearch
michael@0 535 // (1) only gets ran if we get any filtered tokens from this._getSearch,
michael@0 536 // since if there are no tokens, there is nothing to match, so there is no
michael@0 537 // reason to run the query).
michael@0 538 let {query, tokens} =
michael@0 539 this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString));
michael@0 540 let queries = tokens.length ?
michael@0 541 [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery(), this._getBoundOpenPagesQuery(tokens), query] :
michael@0 542 [this._getBoundAdaptiveQuery(), this._getBoundOpenPagesQuery(tokens), query];
michael@0 543
michael@0 544 // Start executing our queries.
michael@0 545 this._telemetryStartTime = Date.now();
michael@0 546 this._executeQueries(queries);
michael@0 547
michael@0 548 // Set up our persistent state for the duration of the search.
michael@0 549 this._searchTokens = tokens;
michael@0 550 this._usedPlaces = {};
michael@0 551 },
michael@0 552
michael@0 553 stopSearch: function PAC_stopSearch()
michael@0 554 {
michael@0 555 // We need to cancel our searches so we do not get any [more] results.
michael@0 556 // However, it's possible we haven't actually started any searches, so this
michael@0 557 // method may throw because this._pendingQuery may be undefined.
michael@0 558 if (this._pendingQuery) {
michael@0 559 this._stopActiveQuery();
michael@0 560 }
michael@0 561
michael@0 562 this._finishSearch(false);
michael@0 563 },
michael@0 564
michael@0 565 //////////////////////////////////////////////////////////////////////////////
michael@0 566 //// nsIAutoCompleteSimpleResultListener
michael@0 567
michael@0 568 onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB)
michael@0 569 {
michael@0 570 if (aRemoveFromDB) {
michael@0 571 PlacesUtils.history.removePage(NetUtil.newURI(aURISpec));
michael@0 572 }
michael@0 573 },
michael@0 574
michael@0 575 //////////////////////////////////////////////////////////////////////////////
michael@0 576 //// mozIPlacesAutoComplete
michael@0 577
michael@0 578 // If the connection has not yet been started, use this local cache. This
michael@0 579 // prevents autocomplete from initing the database till the first search.
michael@0 580 _openPagesCache: [],
michael@0 581 registerOpenPage: function PAC_registerOpenPage(aURI)
michael@0 582 {
michael@0 583 if (!this._databaseInitialized) {
michael@0 584 this._openPagesCache.push(aURI.spec);
michael@0 585 return;
michael@0 586 }
michael@0 587
michael@0 588 let stmt = this._registerOpenPageQuery;
michael@0 589 stmt.params.page_url = aURI.spec;
michael@0 590 stmt.executeAsync();
michael@0 591 },
michael@0 592
michael@0 593 unregisterOpenPage: function PAC_unregisterOpenPage(aURI)
michael@0 594 {
michael@0 595 if (!this._databaseInitialized) {
michael@0 596 let index = this._openPagesCache.indexOf(aURI.spec);
michael@0 597 if (index != -1) {
michael@0 598 this._openPagesCache.splice(index, 1);
michael@0 599 }
michael@0 600 return;
michael@0 601 }
michael@0 602
michael@0 603 let stmt = this._unregisterOpenPageQuery;
michael@0 604 stmt.params.page_url = aURI.spec;
michael@0 605 stmt.executeAsync();
michael@0 606 },
michael@0 607
michael@0 608 //////////////////////////////////////////////////////////////////////////////
michael@0 609 //// mozIStorageStatementCallback
michael@0 610
michael@0 611 handleResult: function PAC_handleResult(aResultSet)
michael@0 612 {
michael@0 613 let row, haveMatches = false;
michael@0 614 while ((row = aResultSet.getNextRow())) {
michael@0 615 let match = this._processRow(row);
michael@0 616 haveMatches = haveMatches || match;
michael@0 617
michael@0 618 if (this._result.matchCount == this._maxRichResults) {
michael@0 619 // We have enough results, so stop running our search.
michael@0 620 this._stopActiveQuery();
michael@0 621
michael@0 622 // And finish our search.
michael@0 623 this._finishSearch(true);
michael@0 624 return;
michael@0 625 }
michael@0 626
michael@0 627 }
michael@0 628
michael@0 629 // Notify about results if we've gotten them.
michael@0 630 if (haveMatches) {
michael@0 631 this._notifyResults(true);
michael@0 632 }
michael@0 633 },
michael@0 634
michael@0 635 handleError: function PAC_handleError(aError)
michael@0 636 {
michael@0 637 Components.utils.reportError("Places AutoComplete: An async statement encountered an " +
michael@0 638 "error: " + aError.result + ", '" + aError.message + "'");
michael@0 639 },
michael@0 640
michael@0 641 handleCompletion: function PAC_handleCompletion(aReason)
michael@0 642 {
michael@0 643 // If we have already finished our search, we should bail out early.
michael@0 644 if (this.isSearchComplete()) {
michael@0 645 return;
michael@0 646 }
michael@0 647
michael@0 648 // If we do not have enough results, and our match type is
michael@0 649 // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
michael@0 650 // results.
michael@0 651 if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
michael@0 652 this._result.matchCount < this._maxRichResults && !this._secondPass) {
michael@0 653 this._secondPass = true;
michael@0 654 let queries = [
michael@0 655 this._getBoundAdaptiveQuery(MATCH_ANYWHERE),
michael@0 656 this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens),
michael@0 657 ];
michael@0 658 this._executeQueries(queries);
michael@0 659 return;
michael@0 660 }
michael@0 661
michael@0 662 this._finishSearch(true);
michael@0 663 },
michael@0 664
michael@0 665 //////////////////////////////////////////////////////////////////////////////
michael@0 666 //// nsIObserver
michael@0 667
michael@0 668 observe: function PAC_observe(aSubject, aTopic, aData)
michael@0 669 {
michael@0 670 if (aTopic == kTopicShutdown) {
michael@0 671 this._os.removeObserver(this, kTopicShutdown);
michael@0 672
michael@0 673 // Remove our preference observer.
michael@0 674 this._prefs.removeObserver("", this);
michael@0 675 delete this._prefs;
michael@0 676
michael@0 677 // Finalize the statements that we have used.
michael@0 678 let stmts = [
michael@0 679 "_defaultQuery",
michael@0 680 "_historyQuery",
michael@0 681 "_bookmarkQuery",
michael@0 682 "_tagsQuery",
michael@0 683 "_openPagesQuery",
michael@0 684 "_typedQuery",
michael@0 685 "_adaptiveQuery",
michael@0 686 "_keywordQuery",
michael@0 687 "_registerOpenPageQuery",
michael@0 688 "_unregisterOpenPageQuery",
michael@0 689 ];
michael@0 690 for (let i = 0; i < stmts.length; i++) {
michael@0 691 // We do not want to create any query we haven't already created, so
michael@0 692 // see if it is a getter first.
michael@0 693 if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) {
michael@0 694 this[stmts[i]].finalize();
michael@0 695 }
michael@0 696 }
michael@0 697
michael@0 698 if (this._databaseInitialized) {
michael@0 699 this._db.asyncClose();
michael@0 700 }
michael@0 701 }
michael@0 702 else if (aTopic == kPrefChanged) {
michael@0 703 this._loadPrefs();
michael@0 704 }
michael@0 705 },
michael@0 706
michael@0 707 //////////////////////////////////////////////////////////////////////////////
michael@0 708 //// nsPlacesAutoComplete
michael@0 709
michael@0 710 get _databaseInitialized()
michael@0 711 Object.getOwnPropertyDescriptor(this, "_db").value !== undefined,
michael@0 712
michael@0 713 /**
michael@0 714 * Generates the tokens used in searching from a given string.
michael@0 715 *
michael@0 716 * @param aSearchString
michael@0 717 * The string to generate tokens from.
michael@0 718 * @return an array of tokens.
michael@0 719 */
michael@0 720 _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString)
michael@0 721 {
michael@0 722 // Calling split on an empty string will return an array containing one
michael@0 723 // empty string. We don't want that, as it'll break our logic, so return an
michael@0 724 // empty array then.
michael@0 725 return aSearchString.length ? aSearchString.split(" ") : [];
michael@0 726 },
michael@0 727
michael@0 728 /**
michael@0 729 * Properly cleans up when searching is completed.
michael@0 730 *
michael@0 731 * @param aNotify
michael@0 732 * Indicates if we should notify the AutoComplete listener about our
michael@0 733 * results or not.
michael@0 734 */
michael@0 735 _finishSearch: function PAC_finishSearch(aNotify)
michael@0 736 {
michael@0 737 // Notify about results if we are supposed to.
michael@0 738 if (aNotify) {
michael@0 739 this._notifyResults(false);
michael@0 740 }
michael@0 741
michael@0 742 // Clear our state
michael@0 743 delete this._originalSearchString;
michael@0 744 delete this._currentSearchString;
michael@0 745 delete this._strippedPrefix;
michael@0 746 delete this._searchTokens;
michael@0 747 delete this._listener;
michael@0 748 delete this._result;
michael@0 749 delete this._usedPlaces;
michael@0 750 delete this._pendingQuery;
michael@0 751 this._secondPass = false;
michael@0 752 this._enableActions = false;
michael@0 753 },
michael@0 754
michael@0 755 /**
michael@0 756 * Executes the given queries asynchronously.
michael@0 757 *
michael@0 758 * @param aQueries
michael@0 759 * The queries to execute.
michael@0 760 */
michael@0 761 _executeQueries: function PAC_executeQueries(aQueries)
michael@0 762 {
michael@0 763 // Because we might get a handleCompletion for canceled queries, we want to
michael@0 764 // filter out queries we no longer care about (described in the
michael@0 765 // handleCompletion implementation of AutoCompleteStatementCallbackWrapper).
michael@0 766
michael@0 767 // Create our wrapper object and execute the queries.
michael@0 768 let wrapper = new AutoCompleteStatementCallbackWrapper(this, this, this._db);
michael@0 769 this._pendingQuery = wrapper.executeAsync(aQueries);
michael@0 770 },
michael@0 771
michael@0 772 /**
michael@0 773 * Stops executing our active query.
michael@0 774 */
michael@0 775 _stopActiveQuery: function PAC_stopActiveQuery()
michael@0 776 {
michael@0 777 this._pendingQuery.cancel();
michael@0 778 delete this._pendingQuery;
michael@0 779 },
michael@0 780
michael@0 781 /**
michael@0 782 * Notifies the listener about results.
michael@0 783 *
michael@0 784 * @param aSearchOngoing
michael@0 785 * Indicates if the search is ongoing or not.
michael@0 786 */
michael@0 787 _notifyResults: function PAC_notifyResults(aSearchOngoing)
michael@0 788 {
michael@0 789 let result = this._result;
michael@0 790 let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
michael@0 791 if (aSearchOngoing) {
michael@0 792 resultCode += "_ONGOING";
michael@0 793 }
michael@0 794 result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
michael@0 795 this._listener.onSearchResult(this, result);
michael@0 796 if (this._telemetryStartTime) {
michael@0 797 let elapsed = Date.now() - this._telemetryStartTime;
michael@0 798 if (elapsed > 50) {
michael@0 799 try {
michael@0 800 Services.telemetry
michael@0 801 .getHistogramById("PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS")
michael@0 802 .add(elapsed);
michael@0 803 } catch (ex) {
michael@0 804 Components.utils.reportError("Unable to report telemetry.");
michael@0 805 }
michael@0 806 }
michael@0 807 this._telemetryStartTime = null;
michael@0 808 }
michael@0 809 },
michael@0 810
michael@0 811 /**
michael@0 812 * Loads the preferences that we care about.
michael@0 813 *
michael@0 814 * @param [optional] aRegisterObserver
michael@0 815 * Indicates if the preference observer should be added or not. The
michael@0 816 * default value is false.
michael@0 817 */
michael@0 818 _loadPrefs: function PAC_loadPrefs(aRegisterObserver)
michael@0 819 {
michael@0 820 this._enabled = safePrefGetter(this._prefs,
michael@0 821 kBrowserUrlbarAutocompleteEnabledPref,
michael@0 822 true);
michael@0 823 this._matchBehavior = safePrefGetter(this._prefs,
michael@0 824 "matchBehavior",
michael@0 825 MATCH_BOUNDARY_ANYWHERE);
michael@0 826 this._filterJavaScript = safePrefGetter(this._prefs, "filter.javascript", true);
michael@0 827 this._maxRichResults = safePrefGetter(this._prefs, "maxRichResults", 25);
michael@0 828 this._restrictHistoryToken = safePrefGetter(this._prefs,
michael@0 829 "restrict.history", "^");
michael@0 830 this._restrictBookmarkToken = safePrefGetter(this._prefs,
michael@0 831 "restrict.bookmark", "*");
michael@0 832 this._restrictTypedToken = safePrefGetter(this._prefs, "restrict.typed", "~");
michael@0 833 this._restrictTagToken = safePrefGetter(this._prefs, "restrict.tag", "+");
michael@0 834 this._restrictOpenPageToken = safePrefGetter(this._prefs,
michael@0 835 "restrict.openpage", "%");
michael@0 836 this._matchTitleToken = safePrefGetter(this._prefs, "match.title", "#");
michael@0 837 this._matchURLToken = safePrefGetter(this._prefs, "match.url", "@");
michael@0 838 this._defaultBehavior = safePrefGetter(this._prefs, "default.behavior", 0);
michael@0 839 // Further restrictions to apply for "empty searches" (i.e. searches for "").
michael@0 840 this._emptySearchDefaultBehavior =
michael@0 841 this._defaultBehavior |
michael@0 842 safePrefGetter(this._prefs, "default.behavior.emptyRestriction",
michael@0 843 Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
michael@0 844 Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED);
michael@0 845
michael@0 846 // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
michael@0 847 if (this._matchBehavior != MATCH_ANYWHERE &&
michael@0 848 this._matchBehavior != MATCH_BOUNDARY &&
michael@0 849 this._matchBehavior != MATCH_BEGINNING) {
michael@0 850 this._matchBehavior = MATCH_BOUNDARY_ANYWHERE;
michael@0 851 }
michael@0 852 // register observer
michael@0 853 if (aRegisterObserver) {
michael@0 854 this._prefs.addObserver("", this, false);
michael@0 855 }
michael@0 856 },
michael@0 857
michael@0 858 /**
michael@0 859 * Given an array of tokens, this function determines which query should be
michael@0 860 * ran. It also removes any special search tokens.
michael@0 861 *
michael@0 862 * @param aTokens
michael@0 863 * An array of search tokens.
michael@0 864 * @return an object with two properties:
michael@0 865 * query: the correctly optimized, bound query to search the database
michael@0 866 * with.
michael@0 867 * tokens: the filtered list of tokens to search with.
michael@0 868 */
michael@0 869 _getSearch: function PAC_getSearch(aTokens)
michael@0 870 {
michael@0 871 // Set the proper behavior so our call to _getBoundSearchQuery gives us the
michael@0 872 // correct query.
michael@0 873 for (let i = aTokens.length - 1; i >= 0; i--) {
michael@0 874 switch (aTokens[i]) {
michael@0 875 case this._restrictHistoryToken:
michael@0 876 this._setBehavior("history");
michael@0 877 break;
michael@0 878 case this._restrictBookmarkToken:
michael@0 879 this._setBehavior("bookmark");
michael@0 880 break;
michael@0 881 case this._restrictTagToken:
michael@0 882 this._setBehavior("tag");
michael@0 883 break;
michael@0 884 case this._restrictOpenPageToken:
michael@0 885 if (!this._enableActions) {
michael@0 886 continue;
michael@0 887 }
michael@0 888 this._setBehavior("openpage");
michael@0 889 break;
michael@0 890 case this._matchTitleToken:
michael@0 891 this._setBehavior("title");
michael@0 892 break;
michael@0 893 case this._matchURLToken:
michael@0 894 this._setBehavior("url");
michael@0 895 break;
michael@0 896 case this._restrictTypedToken:
michael@0 897 this._setBehavior("typed");
michael@0 898 break;
michael@0 899 default:
michael@0 900 // We do not want to remove the token if we did not match.
michael@0 901 continue;
michael@0 902 };
michael@0 903
michael@0 904 aTokens.splice(i, 1);
michael@0 905 }
michael@0 906
michael@0 907 // Set the right JavaScript behavior based on our preference. Note that the
michael@0 908 // preference is whether or not we should filter JavaScript, and the
michael@0 909 // behavior is if we should search it or not.
michael@0 910 if (!this._filterJavaScript) {
michael@0 911 this._setBehavior("javascript");
michael@0 912 }
michael@0 913
michael@0 914 return {
michael@0 915 query: this._getBoundSearchQuery(this._matchBehavior, aTokens),
michael@0 916 tokens: aTokens
michael@0 917 };
michael@0 918 },
michael@0 919
michael@0 920 /**
michael@0 921 * Obtains the search query to be used based on the previously set search
michael@0 922 * behaviors (accessed by this._hasBehavior). The query is bound and ready to
michael@0 923 * execute.
michael@0 924 *
michael@0 925 * @param aMatchBehavior
michael@0 926 * How this query should match its tokens to the search string.
michael@0 927 * @param aTokens
michael@0 928 * An array of search tokens.
michael@0 929 * @return the correctly optimized query to search the database with and the
michael@0 930 * new list of tokens to search with. The query has all the needed
michael@0 931 * parameters bound, so consumers can execute it without doing any
michael@0 932 * additional work.
michael@0 933 */
michael@0 934 _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior,
michael@0 935 aTokens)
michael@0 936 {
michael@0 937 // We use more optimized queries for restricted searches, so we will always
michael@0 938 // return the most restrictive one to the least restrictive one if more than
michael@0 939 // one token is found.
michael@0 940 // Note: "openpages" behavior is supported by the default query.
michael@0 941 // _openPagesQuery instead returns only pages not supported by
michael@0 942 // history and it is always executed.
michael@0 943 let query = this._hasBehavior("tag") ? this._tagsQuery :
michael@0 944 this._hasBehavior("bookmark") ? this._bookmarkQuery :
michael@0 945 this._hasBehavior("typed") ? this._typedQuery :
michael@0 946 this._hasBehavior("history") ? this._historyQuery :
michael@0 947 this._defaultQuery;
michael@0 948
michael@0 949 // Bind the needed parameters to the query so consumers can use it.
michael@0 950 let (params = query.params) {
michael@0 951 params.parent = PlacesUtils.tagsFolderId;
michael@0 952 params.query_type = kQueryTypeFiltered;
michael@0 953 params.matchBehavior = aMatchBehavior;
michael@0 954 params.searchBehavior = this._behavior;
michael@0 955
michael@0 956 // We only want to search the tokens that we are left with - not the
michael@0 957 // original search string.
michael@0 958 params.searchString = aTokens.join(" ");
michael@0 959
michael@0 960 // Limit the query to the the maximum number of desired results.
michael@0 961 // This way we can avoid doing more work than needed.
michael@0 962 params.maxResults = this._maxRichResults;
michael@0 963 }
michael@0 964
michael@0 965 return query;
michael@0 966 },
michael@0 967
michael@0 968 _getBoundOpenPagesQuery: function PAC_getBoundOpenPagesQuery(aTokens)
michael@0 969 {
michael@0 970 let query = this._openPagesQuery;
michael@0 971
michael@0 972 // Bind the needed parameters to the query so consumers can use it.
michael@0 973 let (params = query.params) {
michael@0 974 params.query_type = kQueryTypeFiltered;
michael@0 975 params.matchBehavior = this._matchBehavior;
michael@0 976 params.searchBehavior = this._behavior;
michael@0 977 // We only want to search the tokens that we are left with - not the
michael@0 978 // original search string.
michael@0 979 params.searchString = aTokens.join(" ");
michael@0 980 params.maxResults = this._maxRichResults;
michael@0 981 }
michael@0 982
michael@0 983 return query;
michael@0 984 },
michael@0 985
michael@0 986 /**
michael@0 987 * Obtains the keyword query with the properly bound parameters.
michael@0 988 *
michael@0 989 * @param aTokens
michael@0 990 * The array of search tokens to check against.
michael@0 991 * @return the bound keyword query.
michael@0 992 */
michael@0 993 _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens)
michael@0 994 {
michael@0 995 // The keyword is the first word in the search string, with the parameters
michael@0 996 // following it.
michael@0 997 let searchString = this._originalSearchString;
michael@0 998 let queryString = "";
michael@0 999 let queryIndex = searchString.indexOf(" ");
michael@0 1000 if (queryIndex != -1) {
michael@0 1001 queryString = searchString.substring(queryIndex + 1);
michael@0 1002 }
michael@0 1003 // We need to escape the parameters as if they were the query in a URL
michael@0 1004 queryString = encodeURIComponent(queryString).replace("%20", "+", "g");
michael@0 1005
michael@0 1006 // The first word could be a keyword, so that's what we'll search.
michael@0 1007 let keyword = aTokens[0];
michael@0 1008
michael@0 1009 let query = this._keywordQuery;
michael@0 1010 let (params = query.params) {
michael@0 1011 params.keyword = keyword;
michael@0 1012 params.query_string = queryString;
michael@0 1013 params.query_type = kQueryTypeKeyword;
michael@0 1014 }
michael@0 1015
michael@0 1016 return query;
michael@0 1017 },
michael@0 1018
michael@0 1019 /**
michael@0 1020 * Obtains the adaptive query with the properly bound parameters.
michael@0 1021 *
michael@0 1022 * @return the bound adaptive query.
michael@0 1023 */
michael@0 1024 _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior)
michael@0 1025 {
michael@0 1026 // If we were not given a match behavior, use the stored match behavior.
michael@0 1027 if (arguments.length == 0) {
michael@0 1028 aMatchBehavior = this._matchBehavior;
michael@0 1029 }
michael@0 1030
michael@0 1031 let query = this._adaptiveQuery;
michael@0 1032 let (params = query.params) {
michael@0 1033 params.parent = PlacesUtils.tagsFolderId;
michael@0 1034 params.search_string = this._currentSearchString;
michael@0 1035 params.query_type = kQueryTypeFiltered;
michael@0 1036 params.matchBehavior = aMatchBehavior;
michael@0 1037 params.searchBehavior = this._behavior;
michael@0 1038 }
michael@0 1039
michael@0 1040 return query;
michael@0 1041 },
michael@0 1042
michael@0 1043 /**
michael@0 1044 * Processes a mozIStorageRow to generate the proper data for the AutoComplete
michael@0 1045 * result. This will add an entry to the current result if it matches the
michael@0 1046 * criteria.
michael@0 1047 *
michael@0 1048 * @param aRow
michael@0 1049 * The row to process.
michael@0 1050 * @return true if the row is accepted, and false if not.
michael@0 1051 */
michael@0 1052 _processRow: function PAC_processRow(aRow)
michael@0 1053 {
michael@0 1054 // Before we do any work, make sure this entry isn't already in our results.
michael@0 1055 let entryId = aRow.getResultByIndex(kQueryIndexPlaceId);
michael@0 1056 let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL);
michael@0 1057 let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0;
michael@0 1058
michael@0 1059 // If actions are enabled and the page is open, add only the switch-to-tab
michael@0 1060 // result. Otherwise, add the normal result.
michael@0 1061 let [url, action] = this._enableActions && openPageCount > 0 ?
michael@0 1062 ["moz-action:switchtab," + escapedEntryURL, "action "] :
michael@0 1063 [escapedEntryURL, ""];
michael@0 1064
michael@0 1065 if (this._inResults(entryId, url)) {
michael@0 1066 return false;
michael@0 1067 }
michael@0 1068
michael@0 1069 let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || "";
michael@0 1070 let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || "";
michael@0 1071 let entryBookmarked = aRow.getResultByIndex(kQueryIndexBookmarked);
michael@0 1072 let entryBookmarkTitle = entryBookmarked ?
michael@0 1073 aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null;
michael@0 1074 let entryTags = aRow.getResultByIndex(kQueryIndexTags) || "";
michael@0 1075
michael@0 1076 // Always prefer the bookmark title unless it is empty
michael@0 1077 let title = entryBookmarkTitle || entryTitle;
michael@0 1078
michael@0 1079 let style;
michael@0 1080 if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) {
michael@0 1081 // If we do not have a title, then we must have a keyword, so let the UI
michael@0 1082 // know it is a keyword. Otherwise, we found an exact page match, so just
michael@0 1083 // show the page like a regular result. Because the page title is likely
michael@0 1084 // going to be more specific than the bookmark title (keyword title).
michael@0 1085 if (!entryTitle) {
michael@0 1086 style = "keyword";
michael@0 1087 }
michael@0 1088 else {
michael@0 1089 title = entryTitle;
michael@0 1090 }
michael@0 1091 }
michael@0 1092
michael@0 1093 // We will always prefer to show tags if we have them.
michael@0 1094 let showTags = !!entryTags;
michael@0 1095
michael@0 1096 // However, we'll act as if a page is not bookmarked or tagged if the user
michael@0 1097 // only wants only history and not bookmarks or tags.
michael@0 1098 if (this._hasBehavior("history") &&
michael@0 1099 !(this._hasBehavior("bookmark") || this._hasBehavior("tag"))) {
michael@0 1100 showTags = false;
michael@0 1101 style = "favicon";
michael@0 1102 }
michael@0 1103
michael@0 1104 // If we have tags and should show them, we need to add them to the title.
michael@0 1105 if (showTags) {
michael@0 1106 title += kTitleTagsSeparator + entryTags;
michael@0 1107 }
michael@0 1108 // We have to determine the right style to display. Tags show the tag icon,
michael@0 1109 // bookmarks get the bookmark icon, and keywords get the keyword icon. If
michael@0 1110 // the result does not fall into any of those, it just gets the favicon.
michael@0 1111 if (!style) {
michael@0 1112 // It is possible that we already have a style set (from a keyword
michael@0 1113 // search or because of the user's preferences), so only set it if we
michael@0 1114 // haven't already done so.
michael@0 1115 if (showTags) {
michael@0 1116 style = "tag";
michael@0 1117 }
michael@0 1118 else if (entryBookmarked) {
michael@0 1119 style = "bookmark";
michael@0 1120 }
michael@0 1121 else {
michael@0 1122 style = "favicon";
michael@0 1123 }
michael@0 1124 }
michael@0 1125
michael@0 1126 this._addToResults(entryId, url, title, entryFavicon, action + style);
michael@0 1127 return true;
michael@0 1128 },
michael@0 1129
michael@0 1130 /**
michael@0 1131 * Checks to see if the given place has already been added to the results.
michael@0 1132 *
michael@0 1133 * @param aPlaceId
michael@0 1134 * The place id to check for, may be null.
michael@0 1135 * @param aUrl
michael@0 1136 * The url to check for.
michael@0 1137 * @return true if the place has been added, false otherwise.
michael@0 1138 *
michael@0 1139 * @note Must check both the id and the url for a negative match, since
michael@0 1140 * autocomplete may run in the middle of a new page addition. In such
michael@0 1141 * a case the switch-to-tab query would hash the page by url, then a
michael@0 1142 * next query, running after the page addition, would hash it by id.
michael@0 1143 * It's not possible to just rely on url though, since keywords
michael@0 1144 * dynamically modify the url to include their search string.
michael@0 1145 */
michael@0 1146 _inResults: function PAC_inResults(aPlaceId, aUrl)
michael@0 1147 {
michael@0 1148 if (aPlaceId && aPlaceId in this._usedPlaces) {
michael@0 1149 return true;
michael@0 1150 }
michael@0 1151 return aUrl in this._usedPlaces;
michael@0 1152 },
michael@0 1153
michael@0 1154 /**
michael@0 1155 * Adds a result to the AutoComplete results. Also tracks that we've added
michael@0 1156 * this place_id into the result set.
michael@0 1157 *
michael@0 1158 * @param aPlaceId
michael@0 1159 * The place_id of the item to be added to the result set. This is
michael@0 1160 * used by _inResults.
michael@0 1161 * @param aURISpec
michael@0 1162 * The URI spec for the entry.
michael@0 1163 * @param aTitle
michael@0 1164 * The title to give the entry.
michael@0 1165 * @param aFaviconSpec
michael@0 1166 * The favicon to give to the entry.
michael@0 1167 * @param aStyle
michael@0 1168 * Indicates how the entry should be styled when displayed.
michael@0 1169 */
michael@0 1170 _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle,
michael@0 1171 aFaviconSpec, aStyle)
michael@0 1172 {
michael@0 1173 // Add this to our internal tracker to ensure duplicates do not end up in
michael@0 1174 // the result. _usedPlaces is an Object that is being used as a set.
michael@0 1175 // Not all entries have a place id, thus we fallback to the url for them.
michael@0 1176 // We cannot use only the url since keywords entries are modified to
michael@0 1177 // include the search string, and would be returned multiple times. Ids
michael@0 1178 // are faster too.
michael@0 1179 this._usedPlaces[aPlaceId || aURISpec] = true;
michael@0 1180
michael@0 1181 // Obtain the favicon for this URI.
michael@0 1182 let favicon;
michael@0 1183 if (aFaviconSpec) {
michael@0 1184 let uri = NetUtil.newURI(aFaviconSpec);
michael@0 1185 favicon = PlacesUtils.favicons.getFaviconLinkForIcon(uri).spec;
michael@0 1186 }
michael@0 1187 favicon = favicon || PlacesUtils.favicons.defaultFavicon.spec;
michael@0 1188
michael@0 1189 this._result.appendMatch(aURISpec, aTitle, favicon, aStyle);
michael@0 1190 },
michael@0 1191
michael@0 1192 /**
michael@0 1193 * Determines if the specified AutoComplete behavior is set.
michael@0 1194 *
michael@0 1195 * @param aType
michael@0 1196 * The behavior type to test for.
michael@0 1197 * @return true if the behavior is set, false otherwise.
michael@0 1198 */
michael@0 1199 _hasBehavior: function PAC_hasBehavior(aType)
michael@0 1200 {
michael@0 1201 return (this._behavior &
michael@0 1202 Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]);
michael@0 1203 },
michael@0 1204
michael@0 1205 /**
michael@0 1206 * Enables the desired AutoComplete behavior.
michael@0 1207 *
michael@0 1208 * @param aType
michael@0 1209 * The behavior type to set.
michael@0 1210 */
michael@0 1211 _setBehavior: function PAC_setBehavior(aType)
michael@0 1212 {
michael@0 1213 this._behavior |=
michael@0 1214 Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
michael@0 1215 },
michael@0 1216
michael@0 1217 /**
michael@0 1218 * Determines if we are done searching or not.
michael@0 1219 *
michael@0 1220 * @return true if we have completed searching, false otherwise.
michael@0 1221 */
michael@0 1222 isSearchComplete: function PAC_isSearchComplete()
michael@0 1223 {
michael@0 1224 // If _pendingQuery is null, we should no longer do any work since we have
michael@0 1225 // already called _finishSearch. This means we completed our search.
michael@0 1226 return this._pendingQuery == null;
michael@0 1227 },
michael@0 1228
michael@0 1229 /**
michael@0 1230 * Determines if the given handle of a pending statement is a pending search
michael@0 1231 * or not.
michael@0 1232 *
michael@0 1233 * @param aHandle
michael@0 1234 * A mozIStoragePendingStatement to check and see if we are waiting for
michael@0 1235 * results from it still.
michael@0 1236 * @return true if it is a pending query, false otherwise.
michael@0 1237 */
michael@0 1238 isPendingSearch: function PAC_isPendingSearch(aHandle)
michael@0 1239 {
michael@0 1240 return this._pendingQuery == aHandle;
michael@0 1241 },
michael@0 1242
michael@0 1243 //////////////////////////////////////////////////////////////////////////////
michael@0 1244 //// nsISupports
michael@0 1245
michael@0 1246 classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"),
michael@0 1247
michael@0 1248 _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesAutoComplete),
michael@0 1249
michael@0 1250 QueryInterface: XPCOMUtils.generateQI([
michael@0 1251 Ci.nsIAutoCompleteSearch,
michael@0 1252 Ci.nsIAutoCompleteSimpleResultListener,
michael@0 1253 Ci.mozIPlacesAutoComplete,
michael@0 1254 Ci.mozIStorageStatementCallback,
michael@0 1255 Ci.nsIObserver,
michael@0 1256 Ci.nsISupportsWeakReference,
michael@0 1257 ])
michael@0 1258 };
michael@0 1259
michael@0 1260 ////////////////////////////////////////////////////////////////////////////////
michael@0 1261 //// urlInlineComplete class
michael@0 1262 //// component @mozilla.org/autocomplete/search;1?name=urlinline
michael@0 1263
michael@0 1264 function urlInlineComplete()
michael@0 1265 {
michael@0 1266 this._loadPrefs(true);
michael@0 1267 Services.obs.addObserver(this, kTopicShutdown, true);
michael@0 1268 }
michael@0 1269
michael@0 1270 urlInlineComplete.prototype = {
michael@0 1271
michael@0 1272 /////////////////////////////////////////////////////////////////////////////////
michael@0 1273 //// Database and query getters
michael@0 1274
michael@0 1275 __db: null,
michael@0 1276
michael@0 1277 get _db()
michael@0 1278 {
michael@0 1279 if (!this.__db && this._autofillEnabled) {
michael@0 1280 this.__db = PlacesUtils.history.DBConnection.clone(true);
michael@0 1281 }
michael@0 1282 return this.__db;
michael@0 1283 },
michael@0 1284
michael@0 1285 __hostQuery: null,
michael@0 1286
michael@0 1287 get _hostQuery()
michael@0 1288 {
michael@0 1289 if (!this.__hostQuery) {
michael@0 1290 // Add a trailing slash at the end of the hostname, since we always
michael@0 1291 // want to complete up to and including a URL separator.
michael@0 1292 this.__hostQuery = this._db.createAsyncStatement(
michael@0 1293 "/* do not warn (bug no): could index on (typed,frecency) but not worth it */ "
michael@0 1294 + "SELECT host || '/', prefix || host || '/' "
michael@0 1295 + "FROM moz_hosts "
michael@0 1296 + "WHERE host BETWEEN :search_string AND :search_string || X'FFFF' "
michael@0 1297 + "AND frecency <> 0 "
michael@0 1298 + (this._autofillTyped ? "AND typed = 1 " : "")
michael@0 1299 + "ORDER BY frecency DESC "
michael@0 1300 + "LIMIT 1"
michael@0 1301 );
michael@0 1302 }
michael@0 1303 return this.__hostQuery;
michael@0 1304 },
michael@0 1305
michael@0 1306 __urlQuery: null,
michael@0 1307
michael@0 1308 get _urlQuery()
michael@0 1309 {
michael@0 1310 if (!this.__urlQuery) {
michael@0 1311 this.__urlQuery = this._db.createAsyncStatement(
michael@0 1312 "/* do not warn (bug no): can't use an index */ "
michael@0 1313 + "SELECT h.url "
michael@0 1314 + "FROM moz_places h "
michael@0 1315 + "WHERE h.frecency <> 0 "
michael@0 1316 + (this._autofillTyped ? "AND h.typed = 1 " : "")
michael@0 1317 + "AND AUTOCOMPLETE_MATCH(:searchString, h.url, "
michael@0 1318 + "h.title, '', "
michael@0 1319 + "h.visit_count, h.typed, 0, 0, "
michael@0 1320 + ":matchBehavior, :searchBehavior) "
michael@0 1321 + "ORDER BY h.frecency DESC, h.id DESC "
michael@0 1322 + "LIMIT 1"
michael@0 1323 );
michael@0 1324 }
michael@0 1325 return this.__urlQuery;
michael@0 1326 },
michael@0 1327
michael@0 1328 //////////////////////////////////////////////////////////////////////////////
michael@0 1329 //// nsIAutoCompleteSearch
michael@0 1330
michael@0 1331 startSearch: function UIC_startSearch(aSearchString, aSearchParam,
michael@0 1332 aPreviousResult, aListener)
michael@0 1333 {
michael@0 1334 // Stop the search in case the controller has not taken care of it.
michael@0 1335 if (this._pendingQuery) {
michael@0 1336 this.stopSearch();
michael@0 1337 }
michael@0 1338
michael@0 1339 // We want to store the original string with no leading or trailing
michael@0 1340 // whitespace for case sensitive searches.
michael@0 1341 this._originalSearchString = aSearchString;
michael@0 1342 this._currentSearchString =
michael@0 1343 fixupSearchText(this._originalSearchString.toLowerCase());
michael@0 1344 // The protocol and the host are lowercased by nsIURI, so it's fine to
michael@0 1345 // lowercase the typed prefix to add it back to the results later.
michael@0 1346 this._strippedPrefix = this._originalSearchString.slice(
michael@0 1347 0, this._originalSearchString.length - this._currentSearchString.length
michael@0 1348 ).toLowerCase();
michael@0 1349
michael@0 1350 this._result = Cc["@mozilla.org/autocomplete/simple-result;1"].
michael@0 1351 createInstance(Ci.nsIAutoCompleteSimpleResult);
michael@0 1352 this._result.setSearchString(aSearchString);
michael@0 1353 this._result.setTypeAheadResult(true);
michael@0 1354
michael@0 1355 this._listener = aListener;
michael@0 1356
michael@0 1357 // Don't autoFill if the search term is recognized as a keyword, otherwise
michael@0 1358 // it will override default keywords behavior. Note that keywords are
michael@0 1359 // hashed on first use, so while the first query may delay a little bit,
michael@0 1360 // next ones will just hit the memory hash.
michael@0 1361 if (this._currentSearchString.length == 0 || !this._db ||
michael@0 1362 PlacesUtils.bookmarks.getURIForKeyword(this._currentSearchString)) {
michael@0 1363 this._finishSearch();
michael@0 1364 return;
michael@0 1365 }
michael@0 1366
michael@0 1367 // Don't try to autofill if the search term includes any whitespace.
michael@0 1368 // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
michael@0 1369 // tokenizer ends up trimming the search string and returning a value
michael@0 1370 // that doesn't match it, or is even shorter.
michael@0 1371 if (/\s/.test(this._currentSearchString)) {
michael@0 1372 this._finishSearch();
michael@0 1373 return;
michael@0 1374 }
michael@0 1375
michael@0 1376 // Hosts have no "/" in them.
michael@0 1377 let lastSlashIndex = this._currentSearchString.lastIndexOf("/");
michael@0 1378
michael@0 1379 // Search only URLs if there's a slash in the search string...
michael@0 1380 if (lastSlashIndex != -1) {
michael@0 1381 // ...but not if it's exactly at the end of the search string.
michael@0 1382 if (lastSlashIndex < this._currentSearchString.length - 1)
michael@0 1383 this._queryURL();
michael@0 1384 else
michael@0 1385 this._finishSearch();
michael@0 1386 return;
michael@0 1387 }
michael@0 1388
michael@0 1389 // Do a synchronous search on the table of hosts.
michael@0 1390 let query = this._hostQuery;
michael@0 1391 query.params.search_string = this._currentSearchString.toLowerCase();
michael@0 1392 // This is just to measure the delay to reach the UI, not the query time.
michael@0 1393 TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY);
michael@0 1394 let ac = this;
michael@0 1395 let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
michael@0 1396 handleResult: function (aResultSet) {
michael@0 1397 let row = aResultSet.getNextRow();
michael@0 1398 let trimmedHost = row.getResultByIndex(0);
michael@0 1399 let untrimmedHost = row.getResultByIndex(1);
michael@0 1400 // If the untrimmed value doesn't preserve the user's input just
michael@0 1401 // ignore it and complete to the found host.
michael@0 1402 if (untrimmedHost &&
michael@0 1403 !untrimmedHost.toLowerCase().contains(ac._originalSearchString.toLowerCase())) {
michael@0 1404 untrimmedHost = null;
michael@0 1405 }
michael@0 1406
michael@0 1407 ac._result.appendMatch(ac._strippedPrefix + trimmedHost, "", "", "", untrimmedHost);
michael@0 1408
michael@0 1409 // handleCompletion() will cause the result listener to be called, and
michael@0 1410 // will display the result in the UI.
michael@0 1411 },
michael@0 1412
michael@0 1413 handleError: function (aError) {
michael@0 1414 Components.utils.reportError(
michael@0 1415 "URL Inline Complete: An async statement encountered an " +
michael@0 1416 "error: " + aError.result + ", '" + aError.message + "'");
michael@0 1417 },
michael@0 1418
michael@0 1419 handleCompletion: function (aReason) {
michael@0 1420 TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY);
michael@0 1421 ac._finishSearch();
michael@0 1422 }
michael@0 1423 }, this._db);
michael@0 1424 this._pendingQuery = wrapper.executeAsync([query]);
michael@0 1425 },
michael@0 1426
michael@0 1427 /**
michael@0 1428 * Execute an asynchronous search through places, and complete
michael@0 1429 * up to the next URL separator.
michael@0 1430 */
michael@0 1431 _queryURL: function UIC__queryURL()
michael@0 1432 {
michael@0 1433 // The URIs in the database are fixed up, so we can match on a lowercased
michael@0 1434 // host, but the path must be matched in a case sensitive way.
michael@0 1435 let pathIndex =
michael@0 1436 this._originalSearchString.indexOf("/", this._strippedPrefix.length);
michael@0 1437 this._currentSearchString = fixupSearchText(
michael@0 1438 this._originalSearchString.slice(0, pathIndex).toLowerCase() +
michael@0 1439 this._originalSearchString.slice(pathIndex)
michael@0 1440 );
michael@0 1441
michael@0 1442 // Within the standard autocomplete query, we only search the beginning
michael@0 1443 // of URLs for 1 result.
michael@0 1444 let query = this._urlQuery;
michael@0 1445 let (params = query.params) {
michael@0 1446 params.matchBehavior = MATCH_BEGINNING_CASE_SENSITIVE;
michael@0 1447 params.searchBehavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_URL"];
michael@0 1448 params.searchString = this._currentSearchString;
michael@0 1449 }
michael@0 1450
michael@0 1451 // Execute the query.
michael@0 1452 let ac = this;
michael@0 1453 let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
michael@0 1454 handleResult: function(aResultSet) {
michael@0 1455 let row = aResultSet.getNextRow();
michael@0 1456 let value = row.getResultByIndex(0);
michael@0 1457 let url = fixupSearchText(value);
michael@0 1458
michael@0 1459 let prefix = value.slice(0, value.length - stripPrefix(value).length);
michael@0 1460
michael@0 1461 // We must complete the URL up to the next separator (which is /, ? or #).
michael@0 1462 let separatorIndex = url.slice(ac._currentSearchString.length)
michael@0 1463 .search(/[\/\?\#]/);
michael@0 1464 if (separatorIndex != -1) {
michael@0 1465 separatorIndex += ac._currentSearchString.length;
michael@0 1466 if (url[separatorIndex] == "/") {
michael@0 1467 separatorIndex++; // Include the "/" separator
michael@0 1468 }
michael@0 1469 url = url.slice(0, separatorIndex);
michael@0 1470 }
michael@0 1471
michael@0 1472 // Add the result.
michael@0 1473 // If the untrimmed value doesn't preserve the user's input just
michael@0 1474 // ignore it and complete to the found url.
michael@0 1475 let untrimmedURL = prefix + url;
michael@0 1476 if (untrimmedURL &&
michael@0 1477 !untrimmedURL.toLowerCase().contains(ac._originalSearchString.toLowerCase())) {
michael@0 1478 untrimmedURL = null;
michael@0 1479 }
michael@0 1480
michael@0 1481 ac._result.appendMatch(ac._strippedPrefix + url, "", "", "", untrimmedURL);
michael@0 1482
michael@0 1483 // handleCompletion() will cause the result listener to be called, and
michael@0 1484 // will display the result in the UI.
michael@0 1485 },
michael@0 1486
michael@0 1487 handleError: function(aError) {
michael@0 1488 Components.utils.reportError(
michael@0 1489 "URL Inline Complete: An async statement encountered an " +
michael@0 1490 "error: " + aError.result + ", '" + aError.message + "'");
michael@0 1491 },
michael@0 1492
michael@0 1493 handleCompletion: function(aReason) {
michael@0 1494 ac._finishSearch();
michael@0 1495 }
michael@0 1496 }, this._db);
michael@0 1497 this._pendingQuery = wrapper.executeAsync([query]);
michael@0 1498 },
michael@0 1499
michael@0 1500 stopSearch: function UIC_stopSearch()
michael@0 1501 {
michael@0 1502 delete this._originalSearchString;
michael@0 1503 delete this._currentSearchString;
michael@0 1504 delete this._result;
michael@0 1505 delete this._listener;
michael@0 1506
michael@0 1507 if (this._pendingQuery) {
michael@0 1508 this._pendingQuery.cancel();
michael@0 1509 delete this._pendingQuery;
michael@0 1510 }
michael@0 1511 },
michael@0 1512
michael@0 1513 /**
michael@0 1514 * Loads the preferences that we care about.
michael@0 1515 *
michael@0 1516 * @param [optional] aRegisterObserver
michael@0 1517 * Indicates if the preference observer should be added or not. The
michael@0 1518 * default value is false.
michael@0 1519 */
michael@0 1520 _loadPrefs: function UIC_loadPrefs(aRegisterObserver)
michael@0 1521 {
michael@0 1522 let prefBranch = Services.prefs.getBranch(kBrowserUrlbarBranch);
michael@0 1523 let autocomplete = safePrefGetter(prefBranch,
michael@0 1524 kBrowserUrlbarAutocompleteEnabledPref,
michael@0 1525 true);
michael@0 1526 let autofill = safePrefGetter(prefBranch,
michael@0 1527 kBrowserUrlbarAutofillPref,
michael@0 1528 true);
michael@0 1529 this._autofillEnabled = autocomplete && autofill;
michael@0 1530 this._autofillTyped = safePrefGetter(prefBranch,
michael@0 1531 kBrowserUrlbarAutofillTypedPref,
michael@0 1532 true);
michael@0 1533 if (aRegisterObserver) {
michael@0 1534 Services.prefs.addObserver(kBrowserUrlbarBranch, this, true);
michael@0 1535 }
michael@0 1536 },
michael@0 1537
michael@0 1538 //////////////////////////////////////////////////////////////////////////////
michael@0 1539 //// nsIAutoCompleteSearchDescriptor
michael@0 1540 get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE,
michael@0 1541
michael@0 1542 //////////////////////////////////////////////////////////////////////////////
michael@0 1543 //// nsIObserver
michael@0 1544
michael@0 1545 observe: function UIC_observe(aSubject, aTopic, aData)
michael@0 1546 {
michael@0 1547 if (aTopic == kTopicShutdown) {
michael@0 1548 this._closeDatabase();
michael@0 1549 }
michael@0 1550 else if (aTopic == kPrefChanged &&
michael@0 1551 (aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillPref ||
michael@0 1552 aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutocompleteEnabledPref ||
michael@0 1553 aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillTypedPref)) {
michael@0 1554 let previousAutofillTyped = this._autofillTyped;
michael@0 1555 this._loadPrefs();
michael@0 1556 if (!this._autofillEnabled) {
michael@0 1557 this.stopSearch();
michael@0 1558 this._closeDatabase();
michael@0 1559 }
michael@0 1560 else if (this._autofillTyped != previousAutofillTyped) {
michael@0 1561 // Invalidate the statements to update them for the new typed status.
michael@0 1562 this._invalidateStatements();
michael@0 1563 }
michael@0 1564 }
michael@0 1565 },
michael@0 1566
michael@0 1567 /**
michael@0 1568 * Finalizes and invalidates cached statements.
michael@0 1569 */
michael@0 1570 _invalidateStatements: function UIC_invalidateStatements()
michael@0 1571 {
michael@0 1572 // Finalize the statements that we have used.
michael@0 1573 let stmts = [
michael@0 1574 "__hostQuery",
michael@0 1575 "__urlQuery",
michael@0 1576 ];
michael@0 1577 for (let i = 0; i < stmts.length; i++) {
michael@0 1578 // We do not want to create any query we haven't already created, so
michael@0 1579 // see if it is a getter first.
michael@0 1580 if (this[stmts[i]]) {
michael@0 1581 this[stmts[i]].finalize();
michael@0 1582 this[stmts[i]] = null;
michael@0 1583 }
michael@0 1584 }
michael@0 1585 },
michael@0 1586
michael@0 1587 /**
michael@0 1588 * Closes the database.
michael@0 1589 */
michael@0 1590 _closeDatabase: function UIC_closeDatabase()
michael@0 1591 {
michael@0 1592 this._invalidateStatements();
michael@0 1593 if (this.__db) {
michael@0 1594 this._db.asyncClose();
michael@0 1595 this.__db = null;
michael@0 1596 }
michael@0 1597 },
michael@0 1598
michael@0 1599 //////////////////////////////////////////////////////////////////////////////
michael@0 1600 //// urlInlineComplete
michael@0 1601
michael@0 1602 _finishSearch: function UIC_finishSearch()
michael@0 1603 {
michael@0 1604 // Notify the result object
michael@0 1605 let result = this._result;
michael@0 1606
michael@0 1607 if (result.matchCount) {
michael@0 1608 result.setDefaultIndex(0);
michael@0 1609 result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_SUCCESS"]);
michael@0 1610 } else {
michael@0 1611 result.setDefaultIndex(-1);
michael@0 1612 result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_NOMATCH"]);
michael@0 1613 }
michael@0 1614
michael@0 1615 this._listener.onSearchResult(this, result);
michael@0 1616 this.stopSearch();
michael@0 1617 },
michael@0 1618
michael@0 1619 isSearchComplete: function UIC_isSearchComplete()
michael@0 1620 {
michael@0 1621 return this._pendingQuery == null;
michael@0 1622 },
michael@0 1623
michael@0 1624 isPendingSearch: function UIC_isPendingSearch(aHandle)
michael@0 1625 {
michael@0 1626 return this._pendingQuery == aHandle;
michael@0 1627 },
michael@0 1628
michael@0 1629 //////////////////////////////////////////////////////////////////////////////
michael@0 1630 //// nsISupports
michael@0 1631
michael@0 1632 classID: Components.ID("c88fae2d-25cf-4338-a1f4-64a320ea7440"),
michael@0 1633
michael@0 1634 _xpcom_factory: XPCOMUtils.generateSingletonFactory(urlInlineComplete),
michael@0 1635
michael@0 1636 QueryInterface: XPCOMUtils.generateQI([
michael@0 1637 Ci.nsIAutoCompleteSearch,
michael@0 1638 Ci.nsIAutoCompleteSearchDescriptor,
michael@0 1639 Ci.nsIObserver,
michael@0 1640 Ci.nsISupportsWeakReference,
michael@0 1641 ])
michael@0 1642 };
michael@0 1643
michael@0 1644 let components = [nsPlacesAutoComplete, urlInlineComplete];
michael@0 1645 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);

mercurial