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