1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/places/nsPlacesAutoComplete.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1645 @@ 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 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.11 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.12 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", 1.13 + "resource://gre/modules/PlacesUtils.jsm"); 1.14 +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", 1.15 + "resource://gre/modules/TelemetryStopwatch.jsm"); 1.16 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.17 + "resource://gre/modules/NetUtil.jsm"); 1.18 + 1.19 +//////////////////////////////////////////////////////////////////////////////// 1.20 +//// Constants 1.21 + 1.22 +const Cc = Components.classes; 1.23 +const Ci = Components.interfaces; 1.24 +const Cr = Components.results; 1.25 + 1.26 +// This SQL query fragment provides the following: 1.27 +// - whether the entry is bookmarked (kQueryIndexBookmarked) 1.28 +// - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle) 1.29 +// - the tags associated with a bookmarked entry (kQueryIndexTags) 1.30 +const kBookTagSQLFragment = 1.31 + "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked, " 1.32 ++ "( " 1.33 ++ "SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL " 1.34 ++ "ORDER BY lastModified DESC LIMIT 1 " 1.35 ++ ") AS btitle, " 1.36 ++ "( " 1.37 ++ "SELECT GROUP_CONCAT(t.title, ',') " 1.38 ++ "FROM moz_bookmarks b " 1.39 ++ "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent " 1.40 ++ "WHERE b.fk = h.id " 1.41 ++ ") AS tags"; 1.42 + 1.43 +// observer topics 1.44 +const kTopicShutdown = "places-shutdown"; 1.45 +const kPrefChanged = "nsPref:changed"; 1.46 + 1.47 +// Match type constants. These indicate what type of search function we should 1.48 +// be using. 1.49 +const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE; 1.50 +const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE; 1.51 +const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY; 1.52 +const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING; 1.53 +const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE; 1.54 + 1.55 +// AutoComplete index constants. All AutoComplete queries will provide these 1.56 +// columns in this order. 1.57 +const kQueryIndexURL = 0; 1.58 +const kQueryIndexTitle = 1; 1.59 +const kQueryIndexFaviconURL = 2; 1.60 +const kQueryIndexBookmarked = 3; 1.61 +const kQueryIndexBookmarkTitle = 4; 1.62 +const kQueryIndexTags = 5; 1.63 +const kQueryIndexVisitCount = 6; 1.64 +const kQueryIndexTyped = 7; 1.65 +const kQueryIndexPlaceId = 8; 1.66 +const kQueryIndexQueryType = 9; 1.67 +const kQueryIndexOpenPageCount = 10; 1.68 + 1.69 +// AutoComplete query type constants. Describes the various types of queries 1.70 +// that we can process. 1.71 +const kQueryTypeKeyword = 0; 1.72 +const kQueryTypeFiltered = 1; 1.73 + 1.74 +// This separator is used as an RTL-friendly way to split the title and tags. 1.75 +// It can also be used by an nsIAutoCompleteResult consumer to re-split the 1.76 +// "comment" back into the title and the tag. 1.77 +const kTitleTagsSeparator = " \u2013 "; 1.78 + 1.79 +const kBrowserUrlbarBranch = "browser.urlbar."; 1.80 +// Toggle autocomplete. 1.81 +const kBrowserUrlbarAutocompleteEnabledPref = "autocomplete.enabled"; 1.82 +// Toggle autoFill. 1.83 +const kBrowserUrlbarAutofillPref = "autoFill"; 1.84 +// Whether to search only typed entries. 1.85 +const kBrowserUrlbarAutofillTypedPref = "autoFill.typed"; 1.86 + 1.87 +// The Telemetry histogram for urlInlineComplete query on domain 1.88 +const DOMAIN_QUERY_TELEMETRY = "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS"; 1.89 + 1.90 +//////////////////////////////////////////////////////////////////////////////// 1.91 +//// Globals 1.92 + 1.93 +XPCOMUtils.defineLazyServiceGetter(this, "gTextURIService", 1.94 + "@mozilla.org/intl/texttosuburi;1", 1.95 + "nsITextToSubURI"); 1.96 + 1.97 +//////////////////////////////////////////////////////////////////////////////// 1.98 +//// Helpers 1.99 + 1.100 +/** 1.101 + * Initializes our temporary table on a given database. 1.102 + * 1.103 + * @param aDatabase 1.104 + * The mozIStorageConnection to set up the temp table on. 1.105 + */ 1.106 +function initTempTable(aDatabase) 1.107 +{ 1.108 + // Note: this should be kept up-to-date with the definition in 1.109 + // nsPlacesTables.h. 1.110 + let stmt = aDatabase.createAsyncStatement( 1.111 + "CREATE TEMP TABLE moz_openpages_temp ( " 1.112 + + " url TEXT PRIMARY KEY " 1.113 + + ", open_count INTEGER " 1.114 + + ") " 1.115 + ); 1.116 + stmt.executeAsync(); 1.117 + stmt.finalize(); 1.118 + 1.119 + // Note: this should be kept up-to-date with the definition in 1.120 + // nsPlacesTriggers.h. 1.121 + stmt = aDatabase.createAsyncStatement( 1.122 + "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger " 1.123 + + "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW " 1.124 + + "WHEN NEW.open_count = 0 " 1.125 + + "BEGIN " 1.126 + + "DELETE FROM moz_openpages_temp " 1.127 + + "WHERE url = NEW.url; " 1.128 + + "END " 1.129 + ); 1.130 + stmt.executeAsync(); 1.131 + stmt.finalize(); 1.132 +} 1.133 + 1.134 +/** 1.135 + * Used to unescape encoded URI strings, and drop information that we do not 1.136 + * care about for searching. 1.137 + * 1.138 + * @param aURIString 1.139 + * The text to unescape and modify. 1.140 + * @return the modified uri. 1.141 + */ 1.142 +function fixupSearchText(aURIString) 1.143 +{ 1.144 + let uri = stripPrefix(aURIString); 1.145 + return gTextURIService.unEscapeURIForUI("UTF-8", uri); 1.146 +} 1.147 + 1.148 +/** 1.149 + * Strip prefixes from the URI that we don't care about for searching. 1.150 + * 1.151 + * @param aURIString 1.152 + * The text to modify. 1.153 + * @return the modified uri. 1.154 + */ 1.155 +function stripPrefix(aURIString) 1.156 +{ 1.157 + let uri = aURIString; 1.158 + 1.159 + if (uri.indexOf("http://") == 0) { 1.160 + uri = uri.slice(7); 1.161 + } 1.162 + else if (uri.indexOf("https://") == 0) { 1.163 + uri = uri.slice(8); 1.164 + } 1.165 + else if (uri.indexOf("ftp://") == 0) { 1.166 + uri = uri.slice(6); 1.167 + } 1.168 + 1.169 + if (uri.indexOf("www.") == 0) { 1.170 + uri = uri.slice(4); 1.171 + } 1.172 + return uri; 1.173 +} 1.174 + 1.175 +/** 1.176 + * safePrefGetter get the pref with typo safety. 1.177 + * This will return the default value provided if no pref is set. 1.178 + * 1.179 + * @param aPrefBranch 1.180 + * The nsIPrefBranch containing the required preference 1.181 + * @param aName 1.182 + * A preference name 1.183 + * @param aDefault 1.184 + * The preference's default value 1.185 + * @return the preference value or provided default 1.186 + */ 1.187 + 1.188 +function safePrefGetter(aPrefBranch, aName, aDefault) { 1.189 + let types = { 1.190 + boolean: "Bool", 1.191 + number: "Int", 1.192 + string: "Char" 1.193 + }; 1.194 + let type = types[typeof(aDefault)]; 1.195 + if (!type) { 1.196 + throw "Unknown type!"; 1.197 + } 1.198 + // If the pref isn't set, we want to use the default. 1.199 + try { 1.200 + return aPrefBranch["get" + type + "Pref"](aName); 1.201 + } 1.202 + catch (e) { 1.203 + return aDefault; 1.204 + } 1.205 +} 1.206 + 1.207 + 1.208 +//////////////////////////////////////////////////////////////////////////////// 1.209 +//// AutoCompleteStatementCallbackWrapper class 1.210 + 1.211 +/** 1.212 + * Wraps a callback and ensures that handleCompletion is not dispatched if the 1.213 + * query is no longer tracked. 1.214 + * 1.215 + * @param aAutocomplete 1.216 + * A reference to a nsPlacesAutoComplete. 1.217 + * @param aCallback 1.218 + * A reference to a mozIStorageStatementCallback 1.219 + * @param aDBConnection 1.220 + * The database connection to execute the queries on. 1.221 + */ 1.222 +function AutoCompleteStatementCallbackWrapper(aAutocomplete, aCallback, 1.223 + aDBConnection) 1.224 +{ 1.225 + this._autocomplete = aAutocomplete; 1.226 + this._callback = aCallback; 1.227 + this._db = aDBConnection; 1.228 +} 1.229 + 1.230 +AutoCompleteStatementCallbackWrapper.prototype = { 1.231 + ////////////////////////////////////////////////////////////////////////////// 1.232 + //// mozIStorageStatementCallback 1.233 + 1.234 + handleResult: function ACSCW_handleResult(aResultSet) 1.235 + { 1.236 + this._callback.handleResult.apply(this._callback, arguments); 1.237 + }, 1.238 + 1.239 + handleError: function ACSCW_handleError(aError) 1.240 + { 1.241 + this._callback.handleError.apply(this._callback, arguments); 1.242 + }, 1.243 + 1.244 + handleCompletion: function ACSCW_handleCompletion(aReason) 1.245 + { 1.246 + // Only dispatch handleCompletion if we are not done searching and are a 1.247 + // pending search. 1.248 + if (!this._autocomplete.isSearchComplete() && 1.249 + this._autocomplete.isPendingSearch(this._handle)) { 1.250 + this._callback.handleCompletion.apply(this._callback, arguments); 1.251 + } 1.252 + }, 1.253 + 1.254 + ////////////////////////////////////////////////////////////////////////////// 1.255 + //// AutoCompleteStatementCallbackWrapper 1.256 + 1.257 + /** 1.258 + * Executes the specified query asynchronously. This object will notify 1.259 + * this._callback if we should notify (logic explained in handleCompletion). 1.260 + * 1.261 + * @param aQueries 1.262 + * The queries to execute asynchronously. 1.263 + * @return a mozIStoragePendingStatement that can be used to cancel the 1.264 + * queries. 1.265 + */ 1.266 + executeAsync: function ACSCW_executeAsync(aQueries) 1.267 + { 1.268 + return this._handle = this._db.executeAsync(aQueries, aQueries.length, 1.269 + this); 1.270 + }, 1.271 + 1.272 + ////////////////////////////////////////////////////////////////////////////// 1.273 + //// nsISupports 1.274 + 1.275 + QueryInterface: XPCOMUtils.generateQI([ 1.276 + Ci.mozIStorageStatementCallback, 1.277 + ]) 1.278 +}; 1.279 + 1.280 +//////////////////////////////////////////////////////////////////////////////// 1.281 +//// nsPlacesAutoComplete class 1.282 +//// @mozilla.org/autocomplete/search;1?name=history 1.283 + 1.284 +function nsPlacesAutoComplete() 1.285 +{ 1.286 + ////////////////////////////////////////////////////////////////////////////// 1.287 + //// Shared Constants for Smart Getters 1.288 + 1.289 + // TODO bug 412736 in case of a frecency tie, break it with h.typed and 1.290 + // h.visit_count which is better than nothing. This is slow, so not doing it 1.291 + // yet... 1.292 + const SQL_BASE = "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " 1.293 + + "h.visit_count, h.typed, h.id, :query_type, " 1.294 + + "t.open_count " 1.295 + + "FROM moz_places h " 1.296 + + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " 1.297 + + "LEFT JOIN moz_openpages_temp t ON t.url = h.url " 1.298 + + "WHERE h.frecency <> 0 " 1.299 + + "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " 1.300 + + "IFNULL(btitle, h.title), tags, " 1.301 + + "h.visit_count, h.typed, " 1.302 + + "bookmarked, t.open_count, " 1.303 + + ":matchBehavior, :searchBehavior) " 1.304 + + "{ADDITIONAL_CONDITIONS} " 1.305 + + "ORDER BY h.frecency DESC, h.id DESC " 1.306 + + "LIMIT :maxResults"; 1.307 + 1.308 + ////////////////////////////////////////////////////////////////////////////// 1.309 + //// Smart Getters 1.310 + 1.311 + XPCOMUtils.defineLazyGetter(this, "_db", function() { 1.312 + // Get a cloned, read-only version of the database. We'll only ever write 1.313 + // to our own in-memory temp table, and having a cloned copy means we do not 1.314 + // run the risk of our queries taking longer due to the main database 1.315 + // connection performing a long-running task. 1.316 + let db = PlacesUtils.history.DBConnection.clone(true); 1.317 + 1.318 + // Autocomplete often fallbacks to a table scan due to lack of text indices. 1.319 + // In such cases a larger cache helps reducing IO. The default Storage 1.320 + // value is MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp. 1.321 + let stmt = db.createAsyncStatement("PRAGMA cache_size = -6144"); // 6MiB 1.322 + stmt.executeAsync(); 1.323 + stmt.finalize(); 1.324 + 1.325 + // Create our in-memory tables for tab tracking. 1.326 + initTempTable(db); 1.327 + 1.328 + // Populate the table with current open pages cache contents. 1.329 + if (this._openPagesCache.length > 0) { 1.330 + // Avoid getter re-entrance from the _registerOpenPageQuery lazy getter. 1.331 + let stmt = this._registerOpenPageQuery = 1.332 + db.createAsyncStatement(this._registerOpenPageQuerySQL); 1.333 + let params = stmt.newBindingParamsArray(); 1.334 + for (let i = 0; i < this._openPagesCache.length; i++) { 1.335 + let bp = params.newBindingParams(); 1.336 + bp.bindByName("page_url", this._openPagesCache[i]); 1.337 + params.addParams(bp); 1.338 + } 1.339 + stmt.bindParameters(params); 1.340 + stmt.executeAsync(); 1.341 + stmt.finalize(); 1.342 + delete this._openPagesCache; 1.343 + } 1.344 + 1.345 + return db; 1.346 + }); 1.347 + 1.348 + XPCOMUtils.defineLazyGetter(this, "_defaultQuery", function() { 1.349 + let replacementText = ""; 1.350 + return this._db.createAsyncStatement( 1.351 + SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") 1.352 + ); 1.353 + }); 1.354 + 1.355 + XPCOMUtils.defineLazyGetter(this, "_historyQuery", function() { 1.356 + // Enforce ignoring the visit_count index, since the frecency one is much 1.357 + // faster in this case. ANALYZE helps the query planner to figure out the 1.358 + // faster path, but it may not have run yet. 1.359 + let replacementText = "AND +h.visit_count > 0"; 1.360 + return this._db.createAsyncStatement( 1.361 + SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") 1.362 + ); 1.363 + }); 1.364 + 1.365 + XPCOMUtils.defineLazyGetter(this, "_bookmarkQuery", function() { 1.366 + let replacementText = "AND bookmarked"; 1.367 + return this._db.createAsyncStatement( 1.368 + SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") 1.369 + ); 1.370 + }); 1.371 + 1.372 + XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() { 1.373 + let replacementText = "AND tags IS NOT NULL"; 1.374 + return this._db.createAsyncStatement( 1.375 + SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") 1.376 + ); 1.377 + }); 1.378 + 1.379 + XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() { 1.380 + return this._db.createAsyncStatement( 1.381 + "SELECT t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL, " 1.382 + + ":query_type, t.open_count, NULL " 1.383 + + "FROM moz_openpages_temp t " 1.384 + + "LEFT JOIN moz_places h ON h.url = t.url " 1.385 + + "WHERE h.id IS NULL " 1.386 + + "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL, " 1.387 + + "NULL, NULL, NULL, t.open_count, " 1.388 + + ":matchBehavior, :searchBehavior) " 1.389 + + "ORDER BY t.ROWID DESC " 1.390 + + "LIMIT :maxResults " 1.391 + ); 1.392 + }); 1.393 + 1.394 + XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() { 1.395 + let replacementText = "AND h.typed = 1"; 1.396 + return this._db.createAsyncStatement( 1.397 + SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g") 1.398 + ); 1.399 + }); 1.400 + 1.401 + XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() { 1.402 + return this._db.createAsyncStatement( 1.403 + "/* do not warn (bug 487789) */ " 1.404 + + "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " 1.405 + + "h.visit_count, h.typed, h.id, :query_type, t.open_count " 1.406 + + "FROM ( " 1.407 + + "SELECT ROUND( " 1.408 + + "MAX(use_count) * (1 + (input = :search_string)), 1 " 1.409 + + ") AS rank, place_id " 1.410 + + "FROM moz_inputhistory " 1.411 + + "WHERE input BETWEEN :search_string AND :search_string || X'FFFF' " 1.412 + + "GROUP BY place_id " 1.413 + + ") AS i " 1.414 + + "JOIN moz_places h ON h.id = i.place_id " 1.415 + + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " 1.416 + + "LEFT JOIN moz_openpages_temp t ON t.url = h.url " 1.417 + + "WHERE AUTOCOMPLETE_MATCH(NULL, h.url, " 1.418 + + "IFNULL(btitle, h.title), tags, " 1.419 + + "h.visit_count, h.typed, bookmarked, " 1.420 + + "t.open_count, " 1.421 + + ":matchBehavior, :searchBehavior) " 1.422 + + "ORDER BY rank DESC, h.frecency DESC " 1.423 + ); 1.424 + }); 1.425 + 1.426 + XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() { 1.427 + return this._db.createAsyncStatement( 1.428 + "/* do not warn (bug 487787) */ " 1.429 + + "SELECT " 1.430 + + "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) " 1.431 + + "AS search_url, h.title, " 1.432 + + "IFNULL(f.url, (SELECT f.url " 1.433 + + "FROM moz_places " 1.434 + + "JOIN moz_favicons f ON f.id = favicon_id " 1.435 + + "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk) " 1.436 + + "ORDER BY frecency DESC " 1.437 + + "LIMIT 1) " 1.438 + + "), 1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk), " 1.439 + + ":query_type, t.open_count " 1.440 + + "FROM moz_keywords k " 1.441 + + "JOIN moz_bookmarks b ON b.keyword_id = k.id " 1.442 + + "LEFT JOIN moz_places h ON h.url = search_url " 1.443 + + "LEFT JOIN moz_favicons f ON f.id = h.favicon_id " 1.444 + + "LEFT JOIN moz_openpages_temp t ON t.url = search_url " 1.445 + + "WHERE LOWER(k.keyword) = LOWER(:keyword) " 1.446 + + "ORDER BY h.frecency DESC " 1.447 + ); 1.448 + }); 1.449 + 1.450 + this._registerOpenPageQuerySQL = "INSERT OR REPLACE INTO moz_openpages_temp " 1.451 + + "(url, open_count) " 1.452 + + "VALUES (:page_url, " 1.453 + + "IFNULL(" 1.454 + + "(" 1.455 + + "SELECT open_count + 1 " 1.456 + + "FROM moz_openpages_temp " 1.457 + + "WHERE url = :page_url " 1.458 + + "), " 1.459 + + "1" 1.460 + + ")" 1.461 + + ")"; 1.462 + XPCOMUtils.defineLazyGetter(this, "_registerOpenPageQuery", function() { 1.463 + return this._db.createAsyncStatement(this._registerOpenPageQuerySQL); 1.464 + }); 1.465 + 1.466 + XPCOMUtils.defineLazyGetter(this, "_unregisterOpenPageQuery", function() { 1.467 + return this._db.createAsyncStatement( 1.468 + "UPDATE moz_openpages_temp " 1.469 + + "SET open_count = open_count - 1 " 1.470 + + "WHERE url = :page_url" 1.471 + ); 1.472 + }); 1.473 + 1.474 + ////////////////////////////////////////////////////////////////////////////// 1.475 + //// Initialization 1.476 + 1.477 + // load preferences 1.478 + this._prefs = Cc["@mozilla.org/preferences-service;1"]. 1.479 + getService(Ci.nsIPrefService). 1.480 + getBranch(kBrowserUrlbarBranch); 1.481 + this._loadPrefs(true); 1.482 + 1.483 + // register observers 1.484 + this._os = Cc["@mozilla.org/observer-service;1"]. 1.485 + getService(Ci.nsIObserverService); 1.486 + this._os.addObserver(this, kTopicShutdown, false); 1.487 + 1.488 +} 1.489 + 1.490 +nsPlacesAutoComplete.prototype = { 1.491 + ////////////////////////////////////////////////////////////////////////////// 1.492 + //// nsIAutoCompleteSearch 1.493 + 1.494 + startSearch: function PAC_startSearch(aSearchString, aSearchParam, 1.495 + aPreviousResult, aListener) 1.496 + { 1.497 + // Stop the search in case the controller has not taken care of it. 1.498 + this.stopSearch(); 1.499 + 1.500 + // Note: We don't use aPreviousResult to make sure ordering of results are 1.501 + // consistent. See bug 412730 for more details. 1.502 + 1.503 + // We want to store the original string with no leading or trailing 1.504 + // whitespace for case sensitive searches. 1.505 + this._originalSearchString = aSearchString.trim(); 1.506 + 1.507 + this._currentSearchString = 1.508 + fixupSearchText(this._originalSearchString.toLowerCase()); 1.509 + 1.510 + let searchParamParts = aSearchParam.split(" "); 1.511 + this._enableActions = searchParamParts.indexOf("enable-actions") != -1; 1.512 + 1.513 + this._listener = aListener; 1.514 + let result = Cc["@mozilla.org/autocomplete/simple-result;1"]. 1.515 + createInstance(Ci.nsIAutoCompleteSimpleResult); 1.516 + result.setSearchString(aSearchString); 1.517 + result.setListener(this); 1.518 + this._result = result; 1.519 + 1.520 + // If we are not enabled, we need to return now. 1.521 + if (!this._enabled) { 1.522 + this._finishSearch(true); 1.523 + return; 1.524 + } 1.525 + 1.526 + // Reset our search behavior to the default. 1.527 + if (this._currentSearchString) { 1.528 + this._behavior = this._defaultBehavior; 1.529 + } 1.530 + else { 1.531 + this._behavior = this._emptySearchDefaultBehavior; 1.532 + } 1.533 + // For any given search, we run up to four queries: 1.534 + // 1) keywords (this._keywordQuery) 1.535 + // 2) adaptive learning (this._adaptiveQuery) 1.536 + // 3) open pages not supported by history (this._openPagesQuery) 1.537 + // 4) query from this._getSearch 1.538 + // (1) only gets ran if we get any filtered tokens from this._getSearch, 1.539 + // since if there are no tokens, there is nothing to match, so there is no 1.540 + // reason to run the query). 1.541 + let {query, tokens} = 1.542 + this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString)); 1.543 + let queries = tokens.length ? 1.544 + [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery(), this._getBoundOpenPagesQuery(tokens), query] : 1.545 + [this._getBoundAdaptiveQuery(), this._getBoundOpenPagesQuery(tokens), query]; 1.546 + 1.547 + // Start executing our queries. 1.548 + this._telemetryStartTime = Date.now(); 1.549 + this._executeQueries(queries); 1.550 + 1.551 + // Set up our persistent state for the duration of the search. 1.552 + this._searchTokens = tokens; 1.553 + this._usedPlaces = {}; 1.554 + }, 1.555 + 1.556 + stopSearch: function PAC_stopSearch() 1.557 + { 1.558 + // We need to cancel our searches so we do not get any [more] results. 1.559 + // However, it's possible we haven't actually started any searches, so this 1.560 + // method may throw because this._pendingQuery may be undefined. 1.561 + if (this._pendingQuery) { 1.562 + this._stopActiveQuery(); 1.563 + } 1.564 + 1.565 + this._finishSearch(false); 1.566 + }, 1.567 + 1.568 + ////////////////////////////////////////////////////////////////////////////// 1.569 + //// nsIAutoCompleteSimpleResultListener 1.570 + 1.571 + onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB) 1.572 + { 1.573 + if (aRemoveFromDB) { 1.574 + PlacesUtils.history.removePage(NetUtil.newURI(aURISpec)); 1.575 + } 1.576 + }, 1.577 + 1.578 + ////////////////////////////////////////////////////////////////////////////// 1.579 + //// mozIPlacesAutoComplete 1.580 + 1.581 + // If the connection has not yet been started, use this local cache. This 1.582 + // prevents autocomplete from initing the database till the first search. 1.583 + _openPagesCache: [], 1.584 + registerOpenPage: function PAC_registerOpenPage(aURI) 1.585 + { 1.586 + if (!this._databaseInitialized) { 1.587 + this._openPagesCache.push(aURI.spec); 1.588 + return; 1.589 + } 1.590 + 1.591 + let stmt = this._registerOpenPageQuery; 1.592 + stmt.params.page_url = aURI.spec; 1.593 + stmt.executeAsync(); 1.594 + }, 1.595 + 1.596 + unregisterOpenPage: function PAC_unregisterOpenPage(aURI) 1.597 + { 1.598 + if (!this._databaseInitialized) { 1.599 + let index = this._openPagesCache.indexOf(aURI.spec); 1.600 + if (index != -1) { 1.601 + this._openPagesCache.splice(index, 1); 1.602 + } 1.603 + return; 1.604 + } 1.605 + 1.606 + let stmt = this._unregisterOpenPageQuery; 1.607 + stmt.params.page_url = aURI.spec; 1.608 + stmt.executeAsync(); 1.609 + }, 1.610 + 1.611 + ////////////////////////////////////////////////////////////////////////////// 1.612 + //// mozIStorageStatementCallback 1.613 + 1.614 + handleResult: function PAC_handleResult(aResultSet) 1.615 + { 1.616 + let row, haveMatches = false; 1.617 + while ((row = aResultSet.getNextRow())) { 1.618 + let match = this._processRow(row); 1.619 + haveMatches = haveMatches || match; 1.620 + 1.621 + if (this._result.matchCount == this._maxRichResults) { 1.622 + // We have enough results, so stop running our search. 1.623 + this._stopActiveQuery(); 1.624 + 1.625 + // And finish our search. 1.626 + this._finishSearch(true); 1.627 + return; 1.628 + } 1.629 + 1.630 + } 1.631 + 1.632 + // Notify about results if we've gotten them. 1.633 + if (haveMatches) { 1.634 + this._notifyResults(true); 1.635 + } 1.636 + }, 1.637 + 1.638 + handleError: function PAC_handleError(aError) 1.639 + { 1.640 + Components.utils.reportError("Places AutoComplete: An async statement encountered an " + 1.641 + "error: " + aError.result + ", '" + aError.message + "'"); 1.642 + }, 1.643 + 1.644 + handleCompletion: function PAC_handleCompletion(aReason) 1.645 + { 1.646 + // If we have already finished our search, we should bail out early. 1.647 + if (this.isSearchComplete()) { 1.648 + return; 1.649 + } 1.650 + 1.651 + // If we do not have enough results, and our match type is 1.652 + // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more 1.653 + // results. 1.654 + if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE && 1.655 + this._result.matchCount < this._maxRichResults && !this._secondPass) { 1.656 + this._secondPass = true; 1.657 + let queries = [ 1.658 + this._getBoundAdaptiveQuery(MATCH_ANYWHERE), 1.659 + this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens), 1.660 + ]; 1.661 + this._executeQueries(queries); 1.662 + return; 1.663 + } 1.664 + 1.665 + this._finishSearch(true); 1.666 + }, 1.667 + 1.668 + ////////////////////////////////////////////////////////////////////////////// 1.669 + //// nsIObserver 1.670 + 1.671 + observe: function PAC_observe(aSubject, aTopic, aData) 1.672 + { 1.673 + if (aTopic == kTopicShutdown) { 1.674 + this._os.removeObserver(this, kTopicShutdown); 1.675 + 1.676 + // Remove our preference observer. 1.677 + this._prefs.removeObserver("", this); 1.678 + delete this._prefs; 1.679 + 1.680 + // Finalize the statements that we have used. 1.681 + let stmts = [ 1.682 + "_defaultQuery", 1.683 + "_historyQuery", 1.684 + "_bookmarkQuery", 1.685 + "_tagsQuery", 1.686 + "_openPagesQuery", 1.687 + "_typedQuery", 1.688 + "_adaptiveQuery", 1.689 + "_keywordQuery", 1.690 + "_registerOpenPageQuery", 1.691 + "_unregisterOpenPageQuery", 1.692 + ]; 1.693 + for (let i = 0; i < stmts.length; i++) { 1.694 + // We do not want to create any query we haven't already created, so 1.695 + // see if it is a getter first. 1.696 + if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) { 1.697 + this[stmts[i]].finalize(); 1.698 + } 1.699 + } 1.700 + 1.701 + if (this._databaseInitialized) { 1.702 + this._db.asyncClose(); 1.703 + } 1.704 + } 1.705 + else if (aTopic == kPrefChanged) { 1.706 + this._loadPrefs(); 1.707 + } 1.708 + }, 1.709 + 1.710 + ////////////////////////////////////////////////////////////////////////////// 1.711 + //// nsPlacesAutoComplete 1.712 + 1.713 + get _databaseInitialized() 1.714 + Object.getOwnPropertyDescriptor(this, "_db").value !== undefined, 1.715 + 1.716 + /** 1.717 + * Generates the tokens used in searching from a given string. 1.718 + * 1.719 + * @param aSearchString 1.720 + * The string to generate tokens from. 1.721 + * @return an array of tokens. 1.722 + */ 1.723 + _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString) 1.724 + { 1.725 + // Calling split on an empty string will return an array containing one 1.726 + // empty string. We don't want that, as it'll break our logic, so return an 1.727 + // empty array then. 1.728 + return aSearchString.length ? aSearchString.split(" ") : []; 1.729 + }, 1.730 + 1.731 + /** 1.732 + * Properly cleans up when searching is completed. 1.733 + * 1.734 + * @param aNotify 1.735 + * Indicates if we should notify the AutoComplete listener about our 1.736 + * results or not. 1.737 + */ 1.738 + _finishSearch: function PAC_finishSearch(aNotify) 1.739 + { 1.740 + // Notify about results if we are supposed to. 1.741 + if (aNotify) { 1.742 + this._notifyResults(false); 1.743 + } 1.744 + 1.745 + // Clear our state 1.746 + delete this._originalSearchString; 1.747 + delete this._currentSearchString; 1.748 + delete this._strippedPrefix; 1.749 + delete this._searchTokens; 1.750 + delete this._listener; 1.751 + delete this._result; 1.752 + delete this._usedPlaces; 1.753 + delete this._pendingQuery; 1.754 + this._secondPass = false; 1.755 + this._enableActions = false; 1.756 + }, 1.757 + 1.758 + /** 1.759 + * Executes the given queries asynchronously. 1.760 + * 1.761 + * @param aQueries 1.762 + * The queries to execute. 1.763 + */ 1.764 + _executeQueries: function PAC_executeQueries(aQueries) 1.765 + { 1.766 + // Because we might get a handleCompletion for canceled queries, we want to 1.767 + // filter out queries we no longer care about (described in the 1.768 + // handleCompletion implementation of AutoCompleteStatementCallbackWrapper). 1.769 + 1.770 + // Create our wrapper object and execute the queries. 1.771 + let wrapper = new AutoCompleteStatementCallbackWrapper(this, this, this._db); 1.772 + this._pendingQuery = wrapper.executeAsync(aQueries); 1.773 + }, 1.774 + 1.775 + /** 1.776 + * Stops executing our active query. 1.777 + */ 1.778 + _stopActiveQuery: function PAC_stopActiveQuery() 1.779 + { 1.780 + this._pendingQuery.cancel(); 1.781 + delete this._pendingQuery; 1.782 + }, 1.783 + 1.784 + /** 1.785 + * Notifies the listener about results. 1.786 + * 1.787 + * @param aSearchOngoing 1.788 + * Indicates if the search is ongoing or not. 1.789 + */ 1.790 + _notifyResults: function PAC_notifyResults(aSearchOngoing) 1.791 + { 1.792 + let result = this._result; 1.793 + let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH"; 1.794 + if (aSearchOngoing) { 1.795 + resultCode += "_ONGOING"; 1.796 + } 1.797 + result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]); 1.798 + this._listener.onSearchResult(this, result); 1.799 + if (this._telemetryStartTime) { 1.800 + let elapsed = Date.now() - this._telemetryStartTime; 1.801 + if (elapsed > 50) { 1.802 + try { 1.803 + Services.telemetry 1.804 + .getHistogramById("PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS") 1.805 + .add(elapsed); 1.806 + } catch (ex) { 1.807 + Components.utils.reportError("Unable to report telemetry."); 1.808 + } 1.809 + } 1.810 + this._telemetryStartTime = null; 1.811 + } 1.812 + }, 1.813 + 1.814 + /** 1.815 + * Loads the preferences that we care about. 1.816 + * 1.817 + * @param [optional] aRegisterObserver 1.818 + * Indicates if the preference observer should be added or not. The 1.819 + * default value is false. 1.820 + */ 1.821 + _loadPrefs: function PAC_loadPrefs(aRegisterObserver) 1.822 + { 1.823 + this._enabled = safePrefGetter(this._prefs, 1.824 + kBrowserUrlbarAutocompleteEnabledPref, 1.825 + true); 1.826 + this._matchBehavior = safePrefGetter(this._prefs, 1.827 + "matchBehavior", 1.828 + MATCH_BOUNDARY_ANYWHERE); 1.829 + this._filterJavaScript = safePrefGetter(this._prefs, "filter.javascript", true); 1.830 + this._maxRichResults = safePrefGetter(this._prefs, "maxRichResults", 25); 1.831 + this._restrictHistoryToken = safePrefGetter(this._prefs, 1.832 + "restrict.history", "^"); 1.833 + this._restrictBookmarkToken = safePrefGetter(this._prefs, 1.834 + "restrict.bookmark", "*"); 1.835 + this._restrictTypedToken = safePrefGetter(this._prefs, "restrict.typed", "~"); 1.836 + this._restrictTagToken = safePrefGetter(this._prefs, "restrict.tag", "+"); 1.837 + this._restrictOpenPageToken = safePrefGetter(this._prefs, 1.838 + "restrict.openpage", "%"); 1.839 + this._matchTitleToken = safePrefGetter(this._prefs, "match.title", "#"); 1.840 + this._matchURLToken = safePrefGetter(this._prefs, "match.url", "@"); 1.841 + this._defaultBehavior = safePrefGetter(this._prefs, "default.behavior", 0); 1.842 + // Further restrictions to apply for "empty searches" (i.e. searches for ""). 1.843 + this._emptySearchDefaultBehavior = 1.844 + this._defaultBehavior | 1.845 + safePrefGetter(this._prefs, "default.behavior.emptyRestriction", 1.846 + Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY | 1.847 + Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED); 1.848 + 1.849 + // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE. 1.850 + if (this._matchBehavior != MATCH_ANYWHERE && 1.851 + this._matchBehavior != MATCH_BOUNDARY && 1.852 + this._matchBehavior != MATCH_BEGINNING) { 1.853 + this._matchBehavior = MATCH_BOUNDARY_ANYWHERE; 1.854 + } 1.855 + // register observer 1.856 + if (aRegisterObserver) { 1.857 + this._prefs.addObserver("", this, false); 1.858 + } 1.859 + }, 1.860 + 1.861 + /** 1.862 + * Given an array of tokens, this function determines which query should be 1.863 + * ran. It also removes any special search tokens. 1.864 + * 1.865 + * @param aTokens 1.866 + * An array of search tokens. 1.867 + * @return an object with two properties: 1.868 + * query: the correctly optimized, bound query to search the database 1.869 + * with. 1.870 + * tokens: the filtered list of tokens to search with. 1.871 + */ 1.872 + _getSearch: function PAC_getSearch(aTokens) 1.873 + { 1.874 + // Set the proper behavior so our call to _getBoundSearchQuery gives us the 1.875 + // correct query. 1.876 + for (let i = aTokens.length - 1; i >= 0; i--) { 1.877 + switch (aTokens[i]) { 1.878 + case this._restrictHistoryToken: 1.879 + this._setBehavior("history"); 1.880 + break; 1.881 + case this._restrictBookmarkToken: 1.882 + this._setBehavior("bookmark"); 1.883 + break; 1.884 + case this._restrictTagToken: 1.885 + this._setBehavior("tag"); 1.886 + break; 1.887 + case this._restrictOpenPageToken: 1.888 + if (!this._enableActions) { 1.889 + continue; 1.890 + } 1.891 + this._setBehavior("openpage"); 1.892 + break; 1.893 + case this._matchTitleToken: 1.894 + this._setBehavior("title"); 1.895 + break; 1.896 + case this._matchURLToken: 1.897 + this._setBehavior("url"); 1.898 + break; 1.899 + case this._restrictTypedToken: 1.900 + this._setBehavior("typed"); 1.901 + break; 1.902 + default: 1.903 + // We do not want to remove the token if we did not match. 1.904 + continue; 1.905 + }; 1.906 + 1.907 + aTokens.splice(i, 1); 1.908 + } 1.909 + 1.910 + // Set the right JavaScript behavior based on our preference. Note that the 1.911 + // preference is whether or not we should filter JavaScript, and the 1.912 + // behavior is if we should search it or not. 1.913 + if (!this._filterJavaScript) { 1.914 + this._setBehavior("javascript"); 1.915 + } 1.916 + 1.917 + return { 1.918 + query: this._getBoundSearchQuery(this._matchBehavior, aTokens), 1.919 + tokens: aTokens 1.920 + }; 1.921 + }, 1.922 + 1.923 + /** 1.924 + * Obtains the search query to be used based on the previously set search 1.925 + * behaviors (accessed by this._hasBehavior). The query is bound and ready to 1.926 + * execute. 1.927 + * 1.928 + * @param aMatchBehavior 1.929 + * How this query should match its tokens to the search string. 1.930 + * @param aTokens 1.931 + * An array of search tokens. 1.932 + * @return the correctly optimized query to search the database with and the 1.933 + * new list of tokens to search with. The query has all the needed 1.934 + * parameters bound, so consumers can execute it without doing any 1.935 + * additional work. 1.936 + */ 1.937 + _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior, 1.938 + aTokens) 1.939 + { 1.940 + // We use more optimized queries for restricted searches, so we will always 1.941 + // return the most restrictive one to the least restrictive one if more than 1.942 + // one token is found. 1.943 + // Note: "openpages" behavior is supported by the default query. 1.944 + // _openPagesQuery instead returns only pages not supported by 1.945 + // history and it is always executed. 1.946 + let query = this._hasBehavior("tag") ? this._tagsQuery : 1.947 + this._hasBehavior("bookmark") ? this._bookmarkQuery : 1.948 + this._hasBehavior("typed") ? this._typedQuery : 1.949 + this._hasBehavior("history") ? this._historyQuery : 1.950 + this._defaultQuery; 1.951 + 1.952 + // Bind the needed parameters to the query so consumers can use it. 1.953 + let (params = query.params) { 1.954 + params.parent = PlacesUtils.tagsFolderId; 1.955 + params.query_type = kQueryTypeFiltered; 1.956 + params.matchBehavior = aMatchBehavior; 1.957 + params.searchBehavior = this._behavior; 1.958 + 1.959 + // We only want to search the tokens that we are left with - not the 1.960 + // original search string. 1.961 + params.searchString = aTokens.join(" "); 1.962 + 1.963 + // Limit the query to the the maximum number of desired results. 1.964 + // This way we can avoid doing more work than needed. 1.965 + params.maxResults = this._maxRichResults; 1.966 + } 1.967 + 1.968 + return query; 1.969 + }, 1.970 + 1.971 + _getBoundOpenPagesQuery: function PAC_getBoundOpenPagesQuery(aTokens) 1.972 + { 1.973 + let query = this._openPagesQuery; 1.974 + 1.975 + // Bind the needed parameters to the query so consumers can use it. 1.976 + let (params = query.params) { 1.977 + params.query_type = kQueryTypeFiltered; 1.978 + params.matchBehavior = this._matchBehavior; 1.979 + params.searchBehavior = this._behavior; 1.980 + // We only want to search the tokens that we are left with - not the 1.981 + // original search string. 1.982 + params.searchString = aTokens.join(" "); 1.983 + params.maxResults = this._maxRichResults; 1.984 + } 1.985 + 1.986 + return query; 1.987 + }, 1.988 + 1.989 + /** 1.990 + * Obtains the keyword query with the properly bound parameters. 1.991 + * 1.992 + * @param aTokens 1.993 + * The array of search tokens to check against. 1.994 + * @return the bound keyword query. 1.995 + */ 1.996 + _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens) 1.997 + { 1.998 + // The keyword is the first word in the search string, with the parameters 1.999 + // following it. 1.1000 + let searchString = this._originalSearchString; 1.1001 + let queryString = ""; 1.1002 + let queryIndex = searchString.indexOf(" "); 1.1003 + if (queryIndex != -1) { 1.1004 + queryString = searchString.substring(queryIndex + 1); 1.1005 + } 1.1006 + // We need to escape the parameters as if they were the query in a URL 1.1007 + queryString = encodeURIComponent(queryString).replace("%20", "+", "g"); 1.1008 + 1.1009 + // The first word could be a keyword, so that's what we'll search. 1.1010 + let keyword = aTokens[0]; 1.1011 + 1.1012 + let query = this._keywordQuery; 1.1013 + let (params = query.params) { 1.1014 + params.keyword = keyword; 1.1015 + params.query_string = queryString; 1.1016 + params.query_type = kQueryTypeKeyword; 1.1017 + } 1.1018 + 1.1019 + return query; 1.1020 + }, 1.1021 + 1.1022 + /** 1.1023 + * Obtains the adaptive query with the properly bound parameters. 1.1024 + * 1.1025 + * @return the bound adaptive query. 1.1026 + */ 1.1027 + _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior) 1.1028 + { 1.1029 + // If we were not given a match behavior, use the stored match behavior. 1.1030 + if (arguments.length == 0) { 1.1031 + aMatchBehavior = this._matchBehavior; 1.1032 + } 1.1033 + 1.1034 + let query = this._adaptiveQuery; 1.1035 + let (params = query.params) { 1.1036 + params.parent = PlacesUtils.tagsFolderId; 1.1037 + params.search_string = this._currentSearchString; 1.1038 + params.query_type = kQueryTypeFiltered; 1.1039 + params.matchBehavior = aMatchBehavior; 1.1040 + params.searchBehavior = this._behavior; 1.1041 + } 1.1042 + 1.1043 + return query; 1.1044 + }, 1.1045 + 1.1046 + /** 1.1047 + * Processes a mozIStorageRow to generate the proper data for the AutoComplete 1.1048 + * result. This will add an entry to the current result if it matches the 1.1049 + * criteria. 1.1050 + * 1.1051 + * @param aRow 1.1052 + * The row to process. 1.1053 + * @return true if the row is accepted, and false if not. 1.1054 + */ 1.1055 + _processRow: function PAC_processRow(aRow) 1.1056 + { 1.1057 + // Before we do any work, make sure this entry isn't already in our results. 1.1058 + let entryId = aRow.getResultByIndex(kQueryIndexPlaceId); 1.1059 + let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL); 1.1060 + let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0; 1.1061 + 1.1062 + // If actions are enabled and the page is open, add only the switch-to-tab 1.1063 + // result. Otherwise, add the normal result. 1.1064 + let [url, action] = this._enableActions && openPageCount > 0 ? 1.1065 + ["moz-action:switchtab," + escapedEntryURL, "action "] : 1.1066 + [escapedEntryURL, ""]; 1.1067 + 1.1068 + if (this._inResults(entryId, url)) { 1.1069 + return false; 1.1070 + } 1.1071 + 1.1072 + let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || ""; 1.1073 + let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || ""; 1.1074 + let entryBookmarked = aRow.getResultByIndex(kQueryIndexBookmarked); 1.1075 + let entryBookmarkTitle = entryBookmarked ? 1.1076 + aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null; 1.1077 + let entryTags = aRow.getResultByIndex(kQueryIndexTags) || ""; 1.1078 + 1.1079 + // Always prefer the bookmark title unless it is empty 1.1080 + let title = entryBookmarkTitle || entryTitle; 1.1081 + 1.1082 + let style; 1.1083 + if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) { 1.1084 + // If we do not have a title, then we must have a keyword, so let the UI 1.1085 + // know it is a keyword. Otherwise, we found an exact page match, so just 1.1086 + // show the page like a regular result. Because the page title is likely 1.1087 + // going to be more specific than the bookmark title (keyword title). 1.1088 + if (!entryTitle) { 1.1089 + style = "keyword"; 1.1090 + } 1.1091 + else { 1.1092 + title = entryTitle; 1.1093 + } 1.1094 + } 1.1095 + 1.1096 + // We will always prefer to show tags if we have them. 1.1097 + let showTags = !!entryTags; 1.1098 + 1.1099 + // However, we'll act as if a page is not bookmarked or tagged if the user 1.1100 + // only wants only history and not bookmarks or tags. 1.1101 + if (this._hasBehavior("history") && 1.1102 + !(this._hasBehavior("bookmark") || this._hasBehavior("tag"))) { 1.1103 + showTags = false; 1.1104 + style = "favicon"; 1.1105 + } 1.1106 + 1.1107 + // If we have tags and should show them, we need to add them to the title. 1.1108 + if (showTags) { 1.1109 + title += kTitleTagsSeparator + entryTags; 1.1110 + } 1.1111 + // We have to determine the right style to display. Tags show the tag icon, 1.1112 + // bookmarks get the bookmark icon, and keywords get the keyword icon. If 1.1113 + // the result does not fall into any of those, it just gets the favicon. 1.1114 + if (!style) { 1.1115 + // It is possible that we already have a style set (from a keyword 1.1116 + // search or because of the user's preferences), so only set it if we 1.1117 + // haven't already done so. 1.1118 + if (showTags) { 1.1119 + style = "tag"; 1.1120 + } 1.1121 + else if (entryBookmarked) { 1.1122 + style = "bookmark"; 1.1123 + } 1.1124 + else { 1.1125 + style = "favicon"; 1.1126 + } 1.1127 + } 1.1128 + 1.1129 + this._addToResults(entryId, url, title, entryFavicon, action + style); 1.1130 + return true; 1.1131 + }, 1.1132 + 1.1133 + /** 1.1134 + * Checks to see if the given place has already been added to the results. 1.1135 + * 1.1136 + * @param aPlaceId 1.1137 + * The place id to check for, may be null. 1.1138 + * @param aUrl 1.1139 + * The url to check for. 1.1140 + * @return true if the place has been added, false otherwise. 1.1141 + * 1.1142 + * @note Must check both the id and the url for a negative match, since 1.1143 + * autocomplete may run in the middle of a new page addition. In such 1.1144 + * a case the switch-to-tab query would hash the page by url, then a 1.1145 + * next query, running after the page addition, would hash it by id. 1.1146 + * It's not possible to just rely on url though, since keywords 1.1147 + * dynamically modify the url to include their search string. 1.1148 + */ 1.1149 + _inResults: function PAC_inResults(aPlaceId, aUrl) 1.1150 + { 1.1151 + if (aPlaceId && aPlaceId in this._usedPlaces) { 1.1152 + return true; 1.1153 + } 1.1154 + return aUrl in this._usedPlaces; 1.1155 + }, 1.1156 + 1.1157 + /** 1.1158 + * Adds a result to the AutoComplete results. Also tracks that we've added 1.1159 + * this place_id into the result set. 1.1160 + * 1.1161 + * @param aPlaceId 1.1162 + * The place_id of the item to be added to the result set. This is 1.1163 + * used by _inResults. 1.1164 + * @param aURISpec 1.1165 + * The URI spec for the entry. 1.1166 + * @param aTitle 1.1167 + * The title to give the entry. 1.1168 + * @param aFaviconSpec 1.1169 + * The favicon to give to the entry. 1.1170 + * @param aStyle 1.1171 + * Indicates how the entry should be styled when displayed. 1.1172 + */ 1.1173 + _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle, 1.1174 + aFaviconSpec, aStyle) 1.1175 + { 1.1176 + // Add this to our internal tracker to ensure duplicates do not end up in 1.1177 + // the result. _usedPlaces is an Object that is being used as a set. 1.1178 + // Not all entries have a place id, thus we fallback to the url for them. 1.1179 + // We cannot use only the url since keywords entries are modified to 1.1180 + // include the search string, and would be returned multiple times. Ids 1.1181 + // are faster too. 1.1182 + this._usedPlaces[aPlaceId || aURISpec] = true; 1.1183 + 1.1184 + // Obtain the favicon for this URI. 1.1185 + let favicon; 1.1186 + if (aFaviconSpec) { 1.1187 + let uri = NetUtil.newURI(aFaviconSpec); 1.1188 + favicon = PlacesUtils.favicons.getFaviconLinkForIcon(uri).spec; 1.1189 + } 1.1190 + favicon = favicon || PlacesUtils.favicons.defaultFavicon.spec; 1.1191 + 1.1192 + this._result.appendMatch(aURISpec, aTitle, favicon, aStyle); 1.1193 + }, 1.1194 + 1.1195 + /** 1.1196 + * Determines if the specified AutoComplete behavior is set. 1.1197 + * 1.1198 + * @param aType 1.1199 + * The behavior type to test for. 1.1200 + * @return true if the behavior is set, false otherwise. 1.1201 + */ 1.1202 + _hasBehavior: function PAC_hasBehavior(aType) 1.1203 + { 1.1204 + return (this._behavior & 1.1205 + Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]); 1.1206 + }, 1.1207 + 1.1208 + /** 1.1209 + * Enables the desired AutoComplete behavior. 1.1210 + * 1.1211 + * @param aType 1.1212 + * The behavior type to set. 1.1213 + */ 1.1214 + _setBehavior: function PAC_setBehavior(aType) 1.1215 + { 1.1216 + this._behavior |= 1.1217 + Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]; 1.1218 + }, 1.1219 + 1.1220 + /** 1.1221 + * Determines if we are done searching or not. 1.1222 + * 1.1223 + * @return true if we have completed searching, false otherwise. 1.1224 + */ 1.1225 + isSearchComplete: function PAC_isSearchComplete() 1.1226 + { 1.1227 + // If _pendingQuery is null, we should no longer do any work since we have 1.1228 + // already called _finishSearch. This means we completed our search. 1.1229 + return this._pendingQuery == null; 1.1230 + }, 1.1231 + 1.1232 + /** 1.1233 + * Determines if the given handle of a pending statement is a pending search 1.1234 + * or not. 1.1235 + * 1.1236 + * @param aHandle 1.1237 + * A mozIStoragePendingStatement to check and see if we are waiting for 1.1238 + * results from it still. 1.1239 + * @return true if it is a pending query, false otherwise. 1.1240 + */ 1.1241 + isPendingSearch: function PAC_isPendingSearch(aHandle) 1.1242 + { 1.1243 + return this._pendingQuery == aHandle; 1.1244 + }, 1.1245 + 1.1246 + ////////////////////////////////////////////////////////////////////////////// 1.1247 + //// nsISupports 1.1248 + 1.1249 + classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"), 1.1250 + 1.1251 + _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesAutoComplete), 1.1252 + 1.1253 + QueryInterface: XPCOMUtils.generateQI([ 1.1254 + Ci.nsIAutoCompleteSearch, 1.1255 + Ci.nsIAutoCompleteSimpleResultListener, 1.1256 + Ci.mozIPlacesAutoComplete, 1.1257 + Ci.mozIStorageStatementCallback, 1.1258 + Ci.nsIObserver, 1.1259 + Ci.nsISupportsWeakReference, 1.1260 + ]) 1.1261 +}; 1.1262 + 1.1263 +//////////////////////////////////////////////////////////////////////////////// 1.1264 +//// urlInlineComplete class 1.1265 +//// component @mozilla.org/autocomplete/search;1?name=urlinline 1.1266 + 1.1267 +function urlInlineComplete() 1.1268 +{ 1.1269 + this._loadPrefs(true); 1.1270 + Services.obs.addObserver(this, kTopicShutdown, true); 1.1271 +} 1.1272 + 1.1273 +urlInlineComplete.prototype = { 1.1274 + 1.1275 +///////////////////////////////////////////////////////////////////////////////// 1.1276 +//// Database and query getters 1.1277 + 1.1278 + __db: null, 1.1279 + 1.1280 + get _db() 1.1281 + { 1.1282 + if (!this.__db && this._autofillEnabled) { 1.1283 + this.__db = PlacesUtils.history.DBConnection.clone(true); 1.1284 + } 1.1285 + return this.__db; 1.1286 + }, 1.1287 + 1.1288 + __hostQuery: null, 1.1289 + 1.1290 + get _hostQuery() 1.1291 + { 1.1292 + if (!this.__hostQuery) { 1.1293 + // Add a trailing slash at the end of the hostname, since we always 1.1294 + // want to complete up to and including a URL separator. 1.1295 + this.__hostQuery = this._db.createAsyncStatement( 1.1296 + "/* do not warn (bug no): could index on (typed,frecency) but not worth it */ " 1.1297 + + "SELECT host || '/', prefix || host || '/' " 1.1298 + + "FROM moz_hosts " 1.1299 + + "WHERE host BETWEEN :search_string AND :search_string || X'FFFF' " 1.1300 + + "AND frecency <> 0 " 1.1301 + + (this._autofillTyped ? "AND typed = 1 " : "") 1.1302 + + "ORDER BY frecency DESC " 1.1303 + + "LIMIT 1" 1.1304 + ); 1.1305 + } 1.1306 + return this.__hostQuery; 1.1307 + }, 1.1308 + 1.1309 + __urlQuery: null, 1.1310 + 1.1311 + get _urlQuery() 1.1312 + { 1.1313 + if (!this.__urlQuery) { 1.1314 + this.__urlQuery = this._db.createAsyncStatement( 1.1315 + "/* do not warn (bug no): can't use an index */ " 1.1316 + + "SELECT h.url " 1.1317 + + "FROM moz_places h " 1.1318 + + "WHERE h.frecency <> 0 " 1.1319 + + (this._autofillTyped ? "AND h.typed = 1 " : "") 1.1320 + + "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " 1.1321 + + "h.title, '', " 1.1322 + + "h.visit_count, h.typed, 0, 0, " 1.1323 + + ":matchBehavior, :searchBehavior) " 1.1324 + + "ORDER BY h.frecency DESC, h.id DESC " 1.1325 + + "LIMIT 1" 1.1326 + ); 1.1327 + } 1.1328 + return this.__urlQuery; 1.1329 + }, 1.1330 + 1.1331 + ////////////////////////////////////////////////////////////////////////////// 1.1332 + //// nsIAutoCompleteSearch 1.1333 + 1.1334 + startSearch: function UIC_startSearch(aSearchString, aSearchParam, 1.1335 + aPreviousResult, aListener) 1.1336 + { 1.1337 + // Stop the search in case the controller has not taken care of it. 1.1338 + if (this._pendingQuery) { 1.1339 + this.stopSearch(); 1.1340 + } 1.1341 + 1.1342 + // We want to store the original string with no leading or trailing 1.1343 + // whitespace for case sensitive searches. 1.1344 + this._originalSearchString = aSearchString; 1.1345 + this._currentSearchString = 1.1346 + fixupSearchText(this._originalSearchString.toLowerCase()); 1.1347 + // The protocol and the host are lowercased by nsIURI, so it's fine to 1.1348 + // lowercase the typed prefix to add it back to the results later. 1.1349 + this._strippedPrefix = this._originalSearchString.slice( 1.1350 + 0, this._originalSearchString.length - this._currentSearchString.length 1.1351 + ).toLowerCase(); 1.1352 + 1.1353 + this._result = Cc["@mozilla.org/autocomplete/simple-result;1"]. 1.1354 + createInstance(Ci.nsIAutoCompleteSimpleResult); 1.1355 + this._result.setSearchString(aSearchString); 1.1356 + this._result.setTypeAheadResult(true); 1.1357 + 1.1358 + this._listener = aListener; 1.1359 + 1.1360 + // Don't autoFill if the search term is recognized as a keyword, otherwise 1.1361 + // it will override default keywords behavior. Note that keywords are 1.1362 + // hashed on first use, so while the first query may delay a little bit, 1.1363 + // next ones will just hit the memory hash. 1.1364 + if (this._currentSearchString.length == 0 || !this._db || 1.1365 + PlacesUtils.bookmarks.getURIForKeyword(this._currentSearchString)) { 1.1366 + this._finishSearch(); 1.1367 + return; 1.1368 + } 1.1369 + 1.1370 + // Don't try to autofill if the search term includes any whitespace. 1.1371 + // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH 1.1372 + // tokenizer ends up trimming the search string and returning a value 1.1373 + // that doesn't match it, or is even shorter. 1.1374 + if (/\s/.test(this._currentSearchString)) { 1.1375 + this._finishSearch(); 1.1376 + return; 1.1377 + } 1.1378 + 1.1379 + // Hosts have no "/" in them. 1.1380 + let lastSlashIndex = this._currentSearchString.lastIndexOf("/"); 1.1381 + 1.1382 + // Search only URLs if there's a slash in the search string... 1.1383 + if (lastSlashIndex != -1) { 1.1384 + // ...but not if it's exactly at the end of the search string. 1.1385 + if (lastSlashIndex < this._currentSearchString.length - 1) 1.1386 + this._queryURL(); 1.1387 + else 1.1388 + this._finishSearch(); 1.1389 + return; 1.1390 + } 1.1391 + 1.1392 + // Do a synchronous search on the table of hosts. 1.1393 + let query = this._hostQuery; 1.1394 + query.params.search_string = this._currentSearchString.toLowerCase(); 1.1395 + // This is just to measure the delay to reach the UI, not the query time. 1.1396 + TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY); 1.1397 + let ac = this; 1.1398 + let wrapper = new AutoCompleteStatementCallbackWrapper(this, { 1.1399 + handleResult: function (aResultSet) { 1.1400 + let row = aResultSet.getNextRow(); 1.1401 + let trimmedHost = row.getResultByIndex(0); 1.1402 + let untrimmedHost = row.getResultByIndex(1); 1.1403 + // If the untrimmed value doesn't preserve the user's input just 1.1404 + // ignore it and complete to the found host. 1.1405 + if (untrimmedHost && 1.1406 + !untrimmedHost.toLowerCase().contains(ac._originalSearchString.toLowerCase())) { 1.1407 + untrimmedHost = null; 1.1408 + } 1.1409 + 1.1410 + ac._result.appendMatch(ac._strippedPrefix + trimmedHost, "", "", "", untrimmedHost); 1.1411 + 1.1412 + // handleCompletion() will cause the result listener to be called, and 1.1413 + // will display the result in the UI. 1.1414 + }, 1.1415 + 1.1416 + handleError: function (aError) { 1.1417 + Components.utils.reportError( 1.1418 + "URL Inline Complete: An async statement encountered an " + 1.1419 + "error: " + aError.result + ", '" + aError.message + "'"); 1.1420 + }, 1.1421 + 1.1422 + handleCompletion: function (aReason) { 1.1423 + TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY); 1.1424 + ac._finishSearch(); 1.1425 + } 1.1426 + }, this._db); 1.1427 + this._pendingQuery = wrapper.executeAsync([query]); 1.1428 + }, 1.1429 + 1.1430 + /** 1.1431 + * Execute an asynchronous search through places, and complete 1.1432 + * up to the next URL separator. 1.1433 + */ 1.1434 + _queryURL: function UIC__queryURL() 1.1435 + { 1.1436 + // The URIs in the database are fixed up, so we can match on a lowercased 1.1437 + // host, but the path must be matched in a case sensitive way. 1.1438 + let pathIndex = 1.1439 + this._originalSearchString.indexOf("/", this._strippedPrefix.length); 1.1440 + this._currentSearchString = fixupSearchText( 1.1441 + this._originalSearchString.slice(0, pathIndex).toLowerCase() + 1.1442 + this._originalSearchString.slice(pathIndex) 1.1443 + ); 1.1444 + 1.1445 + // Within the standard autocomplete query, we only search the beginning 1.1446 + // of URLs for 1 result. 1.1447 + let query = this._urlQuery; 1.1448 + let (params = query.params) { 1.1449 + params.matchBehavior = MATCH_BEGINNING_CASE_SENSITIVE; 1.1450 + params.searchBehavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_URL"]; 1.1451 + params.searchString = this._currentSearchString; 1.1452 + } 1.1453 + 1.1454 + // Execute the query. 1.1455 + let ac = this; 1.1456 + let wrapper = new AutoCompleteStatementCallbackWrapper(this, { 1.1457 + handleResult: function(aResultSet) { 1.1458 + let row = aResultSet.getNextRow(); 1.1459 + let value = row.getResultByIndex(0); 1.1460 + let url = fixupSearchText(value); 1.1461 + 1.1462 + let prefix = value.slice(0, value.length - stripPrefix(value).length); 1.1463 + 1.1464 + // We must complete the URL up to the next separator (which is /, ? or #). 1.1465 + let separatorIndex = url.slice(ac._currentSearchString.length) 1.1466 + .search(/[\/\?\#]/); 1.1467 + if (separatorIndex != -1) { 1.1468 + separatorIndex += ac._currentSearchString.length; 1.1469 + if (url[separatorIndex] == "/") { 1.1470 + separatorIndex++; // Include the "/" separator 1.1471 + } 1.1472 + url = url.slice(0, separatorIndex); 1.1473 + } 1.1474 + 1.1475 + // Add the result. 1.1476 + // If the untrimmed value doesn't preserve the user's input just 1.1477 + // ignore it and complete to the found url. 1.1478 + let untrimmedURL = prefix + url; 1.1479 + if (untrimmedURL && 1.1480 + !untrimmedURL.toLowerCase().contains(ac._originalSearchString.toLowerCase())) { 1.1481 + untrimmedURL = null; 1.1482 + } 1.1483 + 1.1484 + ac._result.appendMatch(ac._strippedPrefix + url, "", "", "", untrimmedURL); 1.1485 + 1.1486 + // handleCompletion() will cause the result listener to be called, and 1.1487 + // will display the result in the UI. 1.1488 + }, 1.1489 + 1.1490 + handleError: function(aError) { 1.1491 + Components.utils.reportError( 1.1492 + "URL Inline Complete: An async statement encountered an " + 1.1493 + "error: " + aError.result + ", '" + aError.message + "'"); 1.1494 + }, 1.1495 + 1.1496 + handleCompletion: function(aReason) { 1.1497 + ac._finishSearch(); 1.1498 + } 1.1499 + }, this._db); 1.1500 + this._pendingQuery = wrapper.executeAsync([query]); 1.1501 + }, 1.1502 + 1.1503 + stopSearch: function UIC_stopSearch() 1.1504 + { 1.1505 + delete this._originalSearchString; 1.1506 + delete this._currentSearchString; 1.1507 + delete this._result; 1.1508 + delete this._listener; 1.1509 + 1.1510 + if (this._pendingQuery) { 1.1511 + this._pendingQuery.cancel(); 1.1512 + delete this._pendingQuery; 1.1513 + } 1.1514 + }, 1.1515 + 1.1516 + /** 1.1517 + * Loads the preferences that we care about. 1.1518 + * 1.1519 + * @param [optional] aRegisterObserver 1.1520 + * Indicates if the preference observer should be added or not. The 1.1521 + * default value is false. 1.1522 + */ 1.1523 + _loadPrefs: function UIC_loadPrefs(aRegisterObserver) 1.1524 + { 1.1525 + let prefBranch = Services.prefs.getBranch(kBrowserUrlbarBranch); 1.1526 + let autocomplete = safePrefGetter(prefBranch, 1.1527 + kBrowserUrlbarAutocompleteEnabledPref, 1.1528 + true); 1.1529 + let autofill = safePrefGetter(prefBranch, 1.1530 + kBrowserUrlbarAutofillPref, 1.1531 + true); 1.1532 + this._autofillEnabled = autocomplete && autofill; 1.1533 + this._autofillTyped = safePrefGetter(prefBranch, 1.1534 + kBrowserUrlbarAutofillTypedPref, 1.1535 + true); 1.1536 + if (aRegisterObserver) { 1.1537 + Services.prefs.addObserver(kBrowserUrlbarBranch, this, true); 1.1538 + } 1.1539 + }, 1.1540 + 1.1541 + ////////////////////////////////////////////////////////////////////////////// 1.1542 + //// nsIAutoCompleteSearchDescriptor 1.1543 + get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE, 1.1544 + 1.1545 + ////////////////////////////////////////////////////////////////////////////// 1.1546 + //// nsIObserver 1.1547 + 1.1548 + observe: function UIC_observe(aSubject, aTopic, aData) 1.1549 + { 1.1550 + if (aTopic == kTopicShutdown) { 1.1551 + this._closeDatabase(); 1.1552 + } 1.1553 + else if (aTopic == kPrefChanged && 1.1554 + (aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillPref || 1.1555 + aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutocompleteEnabledPref || 1.1556 + aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillTypedPref)) { 1.1557 + let previousAutofillTyped = this._autofillTyped; 1.1558 + this._loadPrefs(); 1.1559 + if (!this._autofillEnabled) { 1.1560 + this.stopSearch(); 1.1561 + this._closeDatabase(); 1.1562 + } 1.1563 + else if (this._autofillTyped != previousAutofillTyped) { 1.1564 + // Invalidate the statements to update them for the new typed status. 1.1565 + this._invalidateStatements(); 1.1566 + } 1.1567 + } 1.1568 + }, 1.1569 + 1.1570 + /** 1.1571 + * Finalizes and invalidates cached statements. 1.1572 + */ 1.1573 + _invalidateStatements: function UIC_invalidateStatements() 1.1574 + { 1.1575 + // Finalize the statements that we have used. 1.1576 + let stmts = [ 1.1577 + "__hostQuery", 1.1578 + "__urlQuery", 1.1579 + ]; 1.1580 + for (let i = 0; i < stmts.length; i++) { 1.1581 + // We do not want to create any query we haven't already created, so 1.1582 + // see if it is a getter first. 1.1583 + if (this[stmts[i]]) { 1.1584 + this[stmts[i]].finalize(); 1.1585 + this[stmts[i]] = null; 1.1586 + } 1.1587 + } 1.1588 + }, 1.1589 + 1.1590 + /** 1.1591 + * Closes the database. 1.1592 + */ 1.1593 + _closeDatabase: function UIC_closeDatabase() 1.1594 + { 1.1595 + this._invalidateStatements(); 1.1596 + if (this.__db) { 1.1597 + this._db.asyncClose(); 1.1598 + this.__db = null; 1.1599 + } 1.1600 + }, 1.1601 + 1.1602 + ////////////////////////////////////////////////////////////////////////////// 1.1603 + //// urlInlineComplete 1.1604 + 1.1605 + _finishSearch: function UIC_finishSearch() 1.1606 + { 1.1607 + // Notify the result object 1.1608 + let result = this._result; 1.1609 + 1.1610 + if (result.matchCount) { 1.1611 + result.setDefaultIndex(0); 1.1612 + result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_SUCCESS"]); 1.1613 + } else { 1.1614 + result.setDefaultIndex(-1); 1.1615 + result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_NOMATCH"]); 1.1616 + } 1.1617 + 1.1618 + this._listener.onSearchResult(this, result); 1.1619 + this.stopSearch(); 1.1620 + }, 1.1621 + 1.1622 + isSearchComplete: function UIC_isSearchComplete() 1.1623 + { 1.1624 + return this._pendingQuery == null; 1.1625 + }, 1.1626 + 1.1627 + isPendingSearch: function UIC_isPendingSearch(aHandle) 1.1628 + { 1.1629 + return this._pendingQuery == aHandle; 1.1630 + }, 1.1631 + 1.1632 + ////////////////////////////////////////////////////////////////////////////// 1.1633 + //// nsISupports 1.1634 + 1.1635 + classID: Components.ID("c88fae2d-25cf-4338-a1f4-64a320ea7440"), 1.1636 + 1.1637 + _xpcom_factory: XPCOMUtils.generateSingletonFactory(urlInlineComplete), 1.1638 + 1.1639 + QueryInterface: XPCOMUtils.generateQI([ 1.1640 + Ci.nsIAutoCompleteSearch, 1.1641 + Ci.nsIAutoCompleteSearchDescriptor, 1.1642 + Ci.nsIObserver, 1.1643 + Ci.nsISupportsWeakReference, 1.1644 + ]) 1.1645 +}; 1.1646 + 1.1647 +let components = [nsPlacesAutoComplete, urlInlineComplete]; 1.1648 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);