toolkit/components/places/UnifiedComplete.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:e43611520b84
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 "use strict";
8
9 ////////////////////////////////////////////////////////////////////////////////
10 //// Constants
11
12 const Cc = Components.classes;
13 const Ci = Components.interfaces;
14 const Cr = Components.results;
15 const Cu = Components.utils;
16
17 const TOPIC_SHUTDOWN = "places-shutdown";
18 const TOPIC_PREFCHANGED = "nsPref:changed";
19
20 const DEFAULT_BEHAVIOR = 0;
21
22 const PREF_BRANCH = "browser.urlbar";
23
24 // Prefs are defined as [pref name, default value].
25 const PREF_ENABLED = [ "autocomplete.enabled", true ];
26 const PREF_AUTOFILL = [ "autoFill", true ];
27 const PREF_AUTOFILL_TYPED = [ "autoFill.typed", true ];
28 const PREF_AUTOFILL_PRIORITY = [ "autoFill.priority", true ];
29 const PREF_DELAY = [ "delay", 50 ];
30 const PREF_BEHAVIOR = [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
31 const PREF_DEFAULT_BEHAVIOR = [ "default.behavior", DEFAULT_BEHAVIOR ];
32 const PREF_EMPTY_BEHAVIOR = [ "default.behavior.emptyRestriction",
33 Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
34 Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED ];
35 const PREF_FILTER_JS = [ "filter.javascript", true ];
36 const PREF_MAXRESULTS = [ "maxRichResults", 25 ];
37 const PREF_RESTRICT_HISTORY = [ "restrict.history", "^" ];
38 const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark", "*" ];
39 const PREF_RESTRICT_TYPED = [ "restrict.typed", "~" ];
40 const PREF_RESTRICT_TAG = [ "restrict.tag", "+" ];
41 const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage", "%" ];
42 const PREF_MATCH_TITLE = [ "match.title", "#" ];
43 const PREF_MATCH_URL = [ "match.url", "@" ];
44
45 // Match type constants.
46 // These indicate what type of search function we should be using.
47 const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
48 const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
49 const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
50 const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
51 const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
52
53 // AutoComplete query type constants.
54 // Describes the various types of queries that we can process rows for.
55 const QUERYTYPE_KEYWORD = 0;
56 const QUERYTYPE_FILTERED = 1;
57 const QUERYTYPE_AUTOFILL_HOST = 2;
58 const QUERYTYPE_AUTOFILL_URL = 3;
59
60 // This separator is used as an RTL-friendly way to split the title and tags.
61 // It can also be used by an nsIAutoCompleteResult consumer to re-split the
62 // "comment" back into the title and the tag.
63 const TITLE_TAGS_SEPARATOR = " \u2013 ";
64
65 // Telemetry probes.
66 const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
67
68 // The default frecency value used when inserting priority results.
69 const FRECENCY_PRIORITY_DEFAULT = 1000;
70
71 // Sqlite result row index constants.
72 const QUERYINDEX_QUERYTYPE = 0;
73 const QUERYINDEX_URL = 1;
74 const QUERYINDEX_TITLE = 2;
75 const QUERYINDEX_ICONURL = 3;
76 const QUERYINDEX_BOOKMARKED = 4;
77 const QUERYINDEX_BOOKMARKTITLE = 5;
78 const QUERYINDEX_TAGS = 6;
79 const QUERYINDEX_VISITCOUNT = 7;
80 const QUERYINDEX_TYPED = 8;
81 const QUERYINDEX_PLACEID = 9;
82 const QUERYINDEX_SWITCHTAB = 10;
83 const QUERYINDEX_FRECENCY = 11;
84
85 // This SQL query fragment provides the following:
86 // - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED)
87 // - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE)
88 // - the tags associated with a bookmarked entry (QUERYINDEX_TAGS)
89 const SQL_BOOKMARK_TAGS_FRAGMENT = sql(
90 "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,",
91 "( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL",
92 "ORDER BY lastModified DESC LIMIT 1",
93 ") AS btitle,",
94 "( SELECT GROUP_CONCAT(t.title, ',')",
95 "FROM moz_bookmarks b",
96 "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent",
97 "WHERE b.fk = h.id",
98 ") AS tags");
99
100 // TODO bug 412736: in case of a frecency tie, we might break it with h.typed
101 // and h.visit_count. That is slower though, so not doing it yet...
102 const SQL_DEFAULT_QUERY = sql(
103 "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",",
104 "h.visit_count, h.typed, h.id, t.open_count, h.frecency",
105 "FROM moz_places h",
106 "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
107 "LEFT JOIN moz_openpages_temp t ON t.url = h.url",
108 "WHERE h.frecency <> 0",
109 "AND AUTOCOMPLETE_MATCH(:searchString, h.url,",
110 "IFNULL(btitle, h.title), tags,",
111 "h.visit_count, h.typed,",
112 "bookmarked, t.open_count,",
113 ":matchBehavior, :searchBehavior)",
114 "/*CONDITIONS*/",
115 "ORDER BY h.frecency DESC, h.id DESC",
116 "LIMIT :maxResults");
117
118 // Enforce ignoring the visit_count index, since the frecency one is much
119 // faster in this case. ANALYZE helps the query planner to figure out the
120 // faster path, but it may not have up-to-date information yet.
121 const SQL_HISTORY_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
122 "AND +h.visit_count > 0", "g");
123
124 const SQL_BOOKMARK_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
125 "AND bookmarked", "g");
126
127 const SQL_TAGS_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
128 "AND tags NOTNULL", "g");
129
130 const SQL_TYPED_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
131 "AND h.typed = 1", "g");
132
133 const SQL_SWITCHTAB_QUERY = sql(
134 "SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,",
135 "t.open_count, NULL",
136 "FROM moz_openpages_temp t",
137 "LEFT JOIN moz_places h ON h.url = t.url",
138 "WHERE h.id IS NULL",
139 "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,",
140 "NULL, NULL, NULL, t.open_count,",
141 ":matchBehavior, :searchBehavior)",
142 "ORDER BY t.ROWID DESC",
143 "LIMIT :maxResults");
144
145 const SQL_ADAPTIVE_QUERY = sql(
146 "/* do not warn (bug 487789) */",
147 "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",",
148 "h.visit_count, h.typed, h.id, t.open_count, h.frecency",
149 "FROM (",
150 "SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,",
151 "place_id",
152 "FROM moz_inputhistory",
153 "WHERE input BETWEEN :search_string AND :search_string || X'FFFF'",
154 "GROUP BY place_id",
155 ") AS i",
156 "JOIN moz_places h ON h.id = i.place_id",
157 "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
158 "LEFT JOIN moz_openpages_temp t ON t.url = h.url",
159 "WHERE AUTOCOMPLETE_MATCH(NULL, h.url,",
160 "IFNULL(btitle, h.title), tags,",
161 "h.visit_count, h.typed, bookmarked,",
162 "t.open_count,",
163 ":matchBehavior, :searchBehavior)",
164 "ORDER BY rank DESC, h.frecency DESC");
165
166 const SQL_KEYWORD_QUERY = sql(
167 "/* do not warn (bug 487787) */",
168 "SELECT :query_type,",
169 "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)",
170 "AS search_url, h.title,",
171 "IFNULL(f.url, (SELECT f.url",
172 "FROM moz_places",
173 "JOIN moz_favicons f ON f.id = favicon_id",
174 "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)",
175 "ORDER BY frecency DESC",
176 "LIMIT 1)",
177 "),",
178 "1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),",
179 "t.open_count, h.frecency",
180 "FROM moz_keywords k",
181 "JOIN moz_bookmarks b ON b.keyword_id = k.id",
182 "LEFT JOIN moz_places h ON h.url = search_url",
183 "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
184 "LEFT JOIN moz_openpages_temp t ON t.url = search_url",
185 "WHERE LOWER(k.keyword) = LOWER(:keyword)",
186 "ORDER BY h.frecency DESC");
187
188 const SQL_HOST_QUERY = sql(
189 "/* do not warn (bug NA): not worth to index on (typed, frecency) */",
190 "SELECT :query_type, host || '/', prefix || host || '/',",
191 "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency",
192 "FROM moz_hosts",
193 "WHERE host BETWEEN :searchString AND :searchString || X'FFFF'",
194 "AND frecency <> 0",
195 "/*CONDITIONS*/",
196 "ORDER BY frecency DESC",
197 "LIMIT 1");
198
199 const SQL_TYPED_HOST_QUERY = SQL_HOST_QUERY.replace("/*CONDITIONS*/",
200 "AND typed = 1");
201 const SQL_URL_QUERY = sql(
202 "/* do not warn (bug no): cannot use an index */",
203 "SELECT :query_type, h.url,",
204 "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, h.frecency",
205 "FROM moz_places h",
206 "WHERE h.frecency <> 0",
207 "/*CONDITIONS*/",
208 "AND AUTOCOMPLETE_MATCH(:searchString, h.url,",
209 "h.title, '',",
210 "h.visit_count, h.typed, 0, 0,",
211 ":matchBehavior, :searchBehavior)",
212 "ORDER BY h.frecency DESC, h.id DESC",
213 "LIMIT 1");
214
215 const SQL_TYPED_URL_QUERY = SQL_URL_QUERY.replace("/*CONDITIONS*/",
216 "AND typed = 1");
217
218 ////////////////////////////////////////////////////////////////////////////////
219 //// Getters
220
221 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
222 Cu.import("resource://gre/modules/Services.jsm");
223
224 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
225 "resource://gre/modules/PlacesUtils.jsm");
226 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
227 "resource://gre/modules/TelemetryStopwatch.jsm");
228 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
229 "resource://gre/modules/NetUtil.jsm");
230 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
231 "resource://gre/modules/Preferences.jsm");
232 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
233 "resource://gre/modules/Sqlite.jsm");
234 XPCOMUtils.defineLazyModuleGetter(this, "OS",
235 "resource://gre/modules/osfile.jsm");
236 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
237 "resource://gre/modules/Promise.jsm");
238 XPCOMUtils.defineLazyModuleGetter(this, "Task",
239 "resource://gre/modules/Task.jsm");
240 XPCOMUtils.defineLazyModuleGetter(this, "PriorityUrlProvider",
241 "resource://gre/modules/PriorityUrlProvider.jsm");
242
243 XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
244 "@mozilla.org/intl/texttosuburi;1",
245 "nsITextToSubURI");
246
247 /**
248 * Storage object for switch-to-tab entries.
249 * This takes care of caching and registering open pages, that will be reused
250 * by switch-to-tab queries. It has an internal cache, so that the Sqlite
251 * store is lazy initialized only on first use.
252 * It has a simple API:
253 * initDatabase(conn): initializes the temporary Sqlite entities to store data
254 * add(uri): adds a given nsIURI to the store
255 * delete(uri): removes a given nsIURI from the store
256 * shutdown(): stops storing data to Sqlite
257 */
258 XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({
259 _conn: null,
260 // Temporary queue used while the database connection is not available.
261 _queue: new Set(),
262 initDatabase: Task.async(function* (conn) {
263 // To reduce IO use an in-memory table for switch-to-tab tracking.
264 // Note: this should be kept up-to-date with the definition in
265 // nsPlacesTables.h.
266 yield conn.execute(sql(
267 "CREATE TEMP TABLE moz_openpages_temp (",
268 "url TEXT PRIMARY KEY,",
269 "open_count INTEGER",
270 ")"));
271
272 // Note: this should be kept up-to-date with the definition in
273 // nsPlacesTriggers.h.
274 yield conn.execute(sql(
275 "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger",
276 "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW",
277 "WHEN NEW.open_count = 0",
278 "BEGIN",
279 "DELETE FROM moz_openpages_temp",
280 "WHERE url = NEW.url;",
281 "END"));
282
283 this._conn = conn;
284
285 // Populate the table with the current cache contents...
286 this._queue.forEach(this.add, this);
287 // ...then clear it to avoid double additions.
288 this._queue.clear();
289 }),
290
291 add: function (uri) {
292 if (!this._conn) {
293 this._queue.add(uri);
294 return;
295 }
296 this._conn.executeCached(sql(
297 "INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)",
298 "VALUES ( :url, IFNULL( (SELECT open_count + 1",
299 "FROM moz_openpages_temp",
300 "WHERE url = :url),",
301 "1",
302 ")",
303 ")"
304 ), { url: uri.spec });
305 },
306
307 delete: function (uri) {
308 if (!this._conn) {
309 this._queue.delete(uri);
310 return;
311 }
312 this._conn.executeCached(sql(
313 "UPDATE moz_openpages_temp",
314 "SET open_count = open_count - 1",
315 "WHERE url = :url"
316 ), { url: uri.spec });
317 },
318
319 shutdown: function () {
320 this._conn = null;
321 this._queue.clear();
322 }
323 }));
324
325 /**
326 * This helper keeps track of preferences and keeps their values up-to-date.
327 */
328 XPCOMUtils.defineLazyGetter(this, "Prefs", () => {
329 let prefs = new Preferences(PREF_BRANCH);
330
331 function loadPrefs() {
332 store.enabled = prefs.get(...PREF_ENABLED);
333 store.autofill = prefs.get(...PREF_AUTOFILL);
334 store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
335 store.autofillPriority = prefs.get(...PREF_AUTOFILL_PRIORITY);
336 store.delay = prefs.get(...PREF_DELAY);
337 store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
338 store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
339 store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
340 store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY);
341 store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS);
342 store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED);
343 store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG);
344 store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB);
345 store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE);
346 store.matchURLToken = prefs.get(...PREF_MATCH_URL);
347 store.defaultBehavior = prefs.get(...PREF_DEFAULT_BEHAVIOR);
348 // Further restrictions to apply for "empty searches" (i.e. searches for "").
349 store.emptySearchDefaultBehavior = store.defaultBehavior |
350 prefs.get(...PREF_EMPTY_BEHAVIOR);
351
352 // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
353 if (store.matchBehavior != MATCH_ANYWHERE &&
354 store.matchBehavior != MATCH_BOUNDARY &&
355 store.matchBehavior != MATCH_BEGINNING) {
356 store.matchBehavior = MATCH_BOUNDARY_ANYWHERE;
357 }
358
359 store.tokenToBehaviorMap = new Map([
360 [ store.restrictHistoryToken, "history" ],
361 [ store.restrictBookmarkToken, "bookmark" ],
362 [ store.restrictTagToken, "tag" ],
363 [ store.restrictOpenPageToken, "openpage" ],
364 [ store.matchTitleToken, "title" ],
365 [ store.matchURLToken, "url" ],
366 [ store.restrictTypedToken, "typed" ]
367 ]);
368 }
369
370 let store = {
371 observe: function (subject, topic, data) {
372 loadPrefs();
373 },
374 QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ])
375 };
376 loadPrefs();
377 prefs.observe("", store);
378
379 return Object.seal(store);
380 });
381
382 ////////////////////////////////////////////////////////////////////////////////
383 //// Helper functions
384
385 /**
386 * Joins multiple sql tokens into a single sql query.
387 */
388 function sql(...parts) parts.join(" ");
389
390 /**
391 * Used to unescape encoded URI strings and drop information that we do not
392 * care about.
393 *
394 * @param spec
395 * The text to unescape and modify.
396 * @return the modified spec.
397 */
398 function fixupSearchText(spec)
399 textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec));
400
401 /**
402 * Generates the tokens used in searching from a given string.
403 *
404 * @param searchString
405 * The string to generate tokens from.
406 * @return an array of tokens.
407 * @note Calling split on an empty string will return an array containing one
408 * empty string. We don't want that, as it'll break our logic, so return
409 * an empty array then.
410 */
411 function getUnfilteredSearchTokens(searchString)
412 searchString.length ? searchString.split(" ") : [];
413
414 /**
415 * Strip prefixes from the URI that we don't care about for searching.
416 *
417 * @param spec
418 * The text to modify.
419 * @return the modified spec.
420 */
421 function stripPrefix(spec)
422 {
423 ["http://", "https://", "ftp://"].some(scheme => {
424 if (spec.startsWith(scheme)) {
425 spec = spec.slice(scheme.length);
426 return true;
427 }
428 return false;
429 });
430
431 if (spec.startsWith("www.")) {
432 spec = spec.slice(4);
433 }
434 return spec;
435 }
436
437 ////////////////////////////////////////////////////////////////////////////////
438 //// Search Class
439 //// Manages a single instance of an autocomplete search.
440
441 function Search(searchString, searchParam, autocompleteListener,
442 resultListener, autocompleteSearch) {
443 // We want to store the original string with no leading or trailing
444 // whitespace for case sensitive searches.
445 this._originalSearchString = searchString.trim();
446 this._searchString = fixupSearchText(this._originalSearchString.toLowerCase());
447 this._searchTokens =
448 this.filterTokens(getUnfilteredSearchTokens(this._searchString));
449 // The protocol and the host are lowercased by nsIURI, so it's fine to
450 // lowercase the typed prefix, to add it back to the results later.
451 this._strippedPrefix = this._originalSearchString.slice(
452 0, this._originalSearchString.length - this._searchString.length
453 ).toLowerCase();
454 // The URIs in the database are fixed-up, so we can match on a lowercased
455 // host, but the path must be matched in a case sensitive way.
456 let pathIndex =
457 this._originalSearchString.indexOf("/", this._strippedPrefix.length);
458 this._autofillUrlSearchString = fixupSearchText(
459 this._originalSearchString.slice(0, pathIndex).toLowerCase() +
460 this._originalSearchString.slice(pathIndex)
461 );
462
463 this._enableActions = searchParam.split(" ").indexOf("enable-actions") != -1;
464
465 this._listener = autocompleteListener;
466 this._autocompleteSearch = autocompleteSearch;
467
468 this._matchBehavior = Prefs.matchBehavior;
469 // Set the default behavior for this search.
470 this._behavior = this._searchString ? Prefs.defaultBehavior
471 : Prefs.emptySearchDefaultBehavior;
472 // Create a new result to add eventual matches. Note we need a result
473 // regardless having matches.
474 let result = Cc["@mozilla.org/autocomplete/simple-result;1"]
475 .createInstance(Ci.nsIAutoCompleteSimpleResult);
476 result.setSearchString(searchString);
477 result.setListener(resultListener);
478 // Will be set later, if needed.
479 result.setDefaultIndex(-1);
480 this._result = result;
481
482 // These are used to avoid adding duplicate entries to the results.
483 this._usedURLs = new Set();
484 this._usedPlaceIds = new Set();
485 }
486
487 Search.prototype = {
488 /**
489 * Enables the desired AutoComplete behavior.
490 *
491 * @param type
492 * The behavior type to set.
493 */
494 setBehavior: function (type) {
495 this._behavior |=
496 Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
497 },
498
499 /**
500 * Determines if the specified AutoComplete behavior is set.
501 *
502 * @param aType
503 * The behavior type to test for.
504 * @return true if the behavior is set, false otherwise.
505 */
506 hasBehavior: function (type) {
507 return this._behavior &
508 Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
509 },
510
511 /**
512 * Used to delay the most complex queries, to save IO while the user is
513 * typing.
514 */
515 _sleepDeferred: null,
516 _sleep: function (aTimeMs) {
517 // Reuse a single instance to try shaving off some usless work before
518 // the first query.
519 if (!this._sleepTimer)
520 this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
521 this._sleepDeferred = Promise.defer();
522 this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(),
523 aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT);
524 return this._sleepDeferred.promise;
525 },
526
527 /**
528 * Given an array of tokens, this function determines which query should be
529 * ran. It also removes any special search tokens.
530 *
531 * @param tokens
532 * An array of search tokens.
533 * @return the filtered list of tokens to search with.
534 */
535 filterTokens: function (tokens) {
536 // Set the proper behavior while filtering tokens.
537 for (let i = tokens.length - 1; i >= 0; i--) {
538 let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]);
539 // Don't remove the token if it didn't match, or if it's an action but
540 // actions are not enabled.
541 if (behavior && (behavior != "openpage" || this._enableActions)) {
542 this.setBehavior(behavior);
543 tokens.splice(i, 1);
544 }
545 }
546
547 // Set the right JavaScript behavior based on our preference. Note that the
548 // preference is whether or not we should filter JavaScript, and the
549 // behavior is if we should search it or not.
550 if (!Prefs.filterJavaScript) {
551 this.setBehavior("javascript");
552 }
553
554 return tokens;
555 },
556
557 /**
558 * Used to cancel this search, will stop providing results.
559 */
560 cancel: function () {
561 if (this._sleepTimer)
562 this._sleepTimer.cancel();
563 if (this._sleepDeferred) {
564 this._sleepDeferred.resolve();
565 this._sleepDeferred = null;
566 }
567 delete this._pendingQuery;
568 },
569
570 /**
571 * Whether this search is running.
572 */
573 get pending() !!this._pendingQuery,
574
575 /**
576 * Execute the search and populate results.
577 * @param conn
578 * The Sqlite connection.
579 */
580 execute: Task.async(function* (conn) {
581 this._pendingQuery = true;
582 TelemetryStopwatch.start(TELEMETRY_1ST_RESULT);
583
584 // For any given search, we run many queries:
585 // 1) priority domains
586 // 2) inline completion
587 // 3) keywords (this._keywordQuery)
588 // 4) adaptive learning (this._adaptiveQuery)
589 // 5) open pages not supported by history (this._switchToTabQuery)
590 // 6) query based on match behavior
591 //
592 // (3) only gets ran if we get any filtered tokens, since if there are no
593 // tokens, there is nothing to match.
594
595 // Get the final query, based on the tokens found in the search string.
596 let queries = [ this._adaptiveQuery,
597 this._switchToTabQuery,
598 this._searchQuery ];
599
600 if (this._searchTokens.length == 1) {
601 yield this._matchPriorityUrl();
602 } else if (this._searchTokens.length > 1) {
603 queries.unshift(this._keywordQuery);
604 }
605
606 if (this._shouldAutofill) {
607 // Hosts have no "/" in them.
608 let lastSlashIndex = this._searchString.lastIndexOf("/");
609 // Search only URLs if there's a slash in the search string...
610 if (lastSlashIndex != -1) {
611 // ...but not if it's exactly at the end of the search string.
612 if (lastSlashIndex < this._searchString.length - 1) {
613 queries.unshift(this._urlQuery);
614 }
615 } else if (this.pending) {
616 // The host query is executed immediately, while any other is delayed
617 // to avoid overloading the connection.
618 let [ query, params ] = this._hostQuery;
619 yield conn.executeCached(query, params, this._onResultRow.bind(this));
620 }
621 }
622
623 yield this._sleep(Prefs.delay);
624 if (!this.pending)
625 return;
626
627 for (let [query, params] of queries) {
628 yield conn.executeCached(query, params, this._onResultRow.bind(this));
629 if (!this.pending)
630 return;
631 }
632
633 // If we do not have enough results, and our match type is
634 // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
635 // results.
636 if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
637 this._result.matchCount < Prefs.maxRichResults) {
638 this._matchBehavior = MATCH_ANYWHERE;
639 for (let [query, params] of [ this._adaptiveQuery,
640 this._searchQuery ]) {
641 yield conn.executeCached(query, params, this._onResultRow);
642 if (!this.pending)
643 return;
644 }
645 }
646
647 // If we didn't find enough matches and we have some frecency-driven
648 // matches, add them.
649 if (this._frecencyMatches) {
650 this._frecencyMatches.forEach(this._addMatch, this);
651 }
652 }),
653
654 _matchPriorityUrl: function* () {
655 if (!Prefs.autofillPriority)
656 return;
657 let priorityMatch = yield PriorityUrlProvider.getMatch(this._searchString);
658 if (priorityMatch) {
659 this._result.setDefaultIndex(0);
660 this._addFrecencyMatch({
661 value: priorityMatch.token,
662 comment: priorityMatch.title,
663 icon: priorityMatch.iconUrl,
664 style: "priority-" + priorityMatch.reason,
665 finalCompleteValue: priorityMatch.url,
666 frecency: FRECENCY_PRIORITY_DEFAULT
667 });
668 }
669 },
670
671 _onResultRow: function (row) {
672 TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT);
673 let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
674 let match;
675 switch (queryType) {
676 case QUERYTYPE_AUTOFILL_HOST:
677 this._result.setDefaultIndex(0);
678 match = this._processHostRow(row);
679 break;
680 case QUERYTYPE_AUTOFILL_URL:
681 this._result.setDefaultIndex(0);
682 match = this._processUrlRow(row);
683 break;
684 case QUERYTYPE_FILTERED:
685 case QUERYTYPE_KEYWORD:
686 match = this._processRow(row);
687 break;
688 }
689 this._addMatch(match);
690 },
691
692 /**
693 * These matches should be mixed up with other matches, based on frecency.
694 */
695 _addFrecencyMatch: function (match) {
696 if (!this._frecencyMatches)
697 this._frecencyMatches = [];
698 this._frecencyMatches.push(match);
699 // We keep this array in reverse order, so we can walk it and remove stuff
700 // from it in one pass. Notice that for frecency reverse order means from
701 // lower to higher.
702 this._frecencyMatches.sort((a, b) => a.frecency - b.frecency);
703 },
704
705 _addMatch: function (match) {
706 let notifyResults = false;
707
708 if (this._frecencyMatches) {
709 for (let i = this._frecencyMatches.length - 1; i >= 0 ; i--) {
710 if (this._frecencyMatches[i].frecency > match.frecency) {
711 this._addMatch(this._frecencyMatches.splice(i, 1)[0]);
712 }
713 }
714 }
715
716 // Must check both id and url, cause keywords dinamically modify the url.
717 if ((!match.placeId || !this._usedPlaceIds.has(match.placeId)) &&
718 !this._usedURLs.has(stripPrefix(match.value))) {
719 // Add this to our internal tracker to ensure duplicates do not end up in
720 // the result.
721 // Not all entries have a place id, thus we fallback to the url for them.
722 // We cannot use only the url since keywords entries are modified to
723 // include the search string, and would be returned multiple times. Ids
724 // are faster too.
725 if (match.placeId)
726 this._usedPlaceIds.add(match.placeId);
727 this._usedURLs.add(stripPrefix(match.value));
728
729 this._result.appendMatch(match.value,
730 match.comment,
731 match.icon || PlacesUtils.favicons.defaultFavicon.spec,
732 match.style || "favicon",
733 match.finalCompleteValue);
734 notifyResults = true;
735 }
736
737 if (this._result.matchCount == Prefs.maxRichResults || !this.pending) {
738 // We have enough results, so stop running our search.
739 this.cancel();
740 // This tells Sqlite.jsm to stop providing us results and cancel the
741 // underlying query.
742 throw StopIteration;
743 }
744
745 if (notifyResults) {
746 // Notify about results if we've gotten them.
747 this.notifyResults(true);
748 }
749 },
750
751 _processHostRow: function (row) {
752 let match = {};
753 let trimmedHost = row.getResultByIndex(QUERYINDEX_URL);
754 let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE);
755 let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
756 // If the untrimmed value doesn't preserve the user's input just
757 // ignore it and complete to the found host.
758 if (untrimmedHost &&
759 !untrimmedHost.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
760 // THIS CAUSES null TO BE SHOWN AS TITLE.
761 untrimmedHost = null;
762 }
763
764 match.value = this._strippedPrefix + trimmedHost;
765 match.comment = trimmedHost;
766 match.finalCompleteValue = untrimmedHost;
767 match.frecency = frecency;
768 return match;
769 },
770
771 _processUrlRow: function (row) {
772 let match = {};
773 let value = row.getResultByIndex(QUERYINDEX_URL);
774 let url = fixupSearchText(value);
775 let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
776
777 let prefix = value.slice(0, value.length - stripPrefix(value).length);
778
779 // We must complete the URL up to the next separator (which is /, ? or #).
780 let separatorIndex = url.slice(this._searchString.length)
781 .search(/[\/\?\#]/);
782 if (separatorIndex != -1) {
783 separatorIndex += this._searchString.length;
784 if (url[separatorIndex] == "/") {
785 separatorIndex++; // Include the "/" separator
786 }
787 url = url.slice(0, separatorIndex);
788 }
789
790 // If the untrimmed value doesn't preserve the user's input just
791 // ignore it and complete to the found url.
792 let untrimmedURL = prefix + url;
793 if (untrimmedURL &&
794 !untrimmedURL.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
795 // THIS CAUSES null TO BE SHOWN AS TITLE.
796 untrimmedURL = null;
797 }
798
799 match.value = this._strippedPrefix + url;
800 match.comment = url;
801 match.finalCompleteValue = untrimmedURL;
802 match.frecency = frecency;
803 return match;
804 },
805
806 _processRow: function (row) {
807 let match = {};
808 match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
809 let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
810 let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
811 let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0;
812 let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || "";
813 let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || "";
814 let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED);
815 let bookmarkTitle = bookmarked ?
816 row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null;
817 let tags = row.getResultByIndex(QUERYINDEX_TAGS) || "";
818 let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
819
820 // If actions are enabled and the page is open, add only the switch-to-tab
821 // result. Otherwise, add the normal result.
822 let [url, action] = this._enableActions && openPageCount > 0 ?
823 ["moz-action:switchtab," + escapedURL, "action "] :
824 [escapedURL, ""];
825
826 // Always prefer the bookmark title unless it is empty
827 let title = bookmarkTitle || historyTitle;
828
829 if (queryType == QUERYTYPE_KEYWORD) {
830 // If we do not have a title, then we must have a keyword, so let the UI
831 // know it is a keyword. Otherwise, we found an exact page match, so just
832 // show the page like a regular result. Because the page title is likely
833 // going to be more specific than the bookmark title (keyword title).
834 if (!historyTitle) {
835 match.style = "keyword";
836 }
837 else {
838 title = historyTitle;
839 }
840 }
841
842 // We will always prefer to show tags if we have them.
843 let showTags = !!tags;
844
845 // However, we'll act as if a page is not bookmarked or tagged if the user
846 // only wants only history and not bookmarks or tags.
847 if (this.hasBehavior("history") &&
848 !(this.hasBehavior("bookmark") || this.hasBehavior("tag"))) {
849 showTags = false;
850 match.style = "favicon";
851 }
852
853 // If we have tags and should show them, we need to add them to the title.
854 if (showTags) {
855 title += TITLE_TAGS_SEPARATOR + tags;
856 }
857
858 // We have to determine the right style to display. Tags show the tag icon,
859 // bookmarks get the bookmark icon, and keywords get the keyword icon. If
860 // the result does not fall into any of those, it just gets the favicon.
861 if (!match.style) {
862 // It is possible that we already have a style set (from a keyword
863 // search or because of the user's preferences), so only set it if we
864 // haven't already done so.
865 if (showTags) {
866 match.style = "tag";
867 }
868 else if (bookmarked) {
869 match.style = "bookmark";
870 }
871 }
872
873 match.value = url;
874 match.comment = title;
875 if (iconurl) {
876 match.icon = PlacesUtils.favicons
877 .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec;
878 }
879 match.frecency = frecency;
880
881 return match;
882 },
883
884 /**
885 * Obtains the search query to be used based on the previously set search
886 * behaviors (accessed by this.hasBehavior).
887 *
888 * @return an array consisting of the correctly optimized query to search the
889 * database with and an object containing the params to bound.
890 */
891 get _searchQuery() {
892 // We use more optimized queries for restricted searches, so we will always
893 // return the most restrictive one to the least restrictive one if more than
894 // one token is found.
895 // Note: "openpages" behavior is supported by the default query.
896 // _switchToTabQuery instead returns only pages not supported by
897 // history and it is always executed.
898 let query = this.hasBehavior("tag") ? SQL_TAGS_QUERY :
899 this.hasBehavior("bookmark") ? SQL_BOOKMARK_QUERY :
900 this.hasBehavior("typed") ? SQL_TYPED_QUERY :
901 this.hasBehavior("history") ? SQL_HISTORY_QUERY :
902 SQL_DEFAULT_QUERY;
903
904 return [
905 query,
906 {
907 parent: PlacesUtils.tagsFolderId,
908 query_type: QUERYTYPE_FILTERED,
909 matchBehavior: this._matchBehavior,
910 searchBehavior: this._behavior,
911 // We only want to search the tokens that we are left with - not the
912 // original search string.
913 searchString: this._searchTokens.join(" "),
914 // Limit the query to the the maximum number of desired results.
915 // This way we can avoid doing more work than needed.
916 maxResults: Prefs.maxRichResults
917 }
918 ];
919 },
920
921 /**
922 * Obtains the query to search for keywords.
923 *
924 * @return an array consisting of the correctly optimized query to search the
925 * database with and an object containing the params to bound.
926 */
927 get _keywordQuery() {
928 // The keyword is the first word in the search string, with the parameters
929 // following it.
930 let searchString = this._originalSearchString;
931 let queryString = "";
932 let queryIndex = searchString.indexOf(" ");
933 if (queryIndex != -1) {
934 queryString = searchString.substring(queryIndex + 1);
935 }
936 // We need to escape the parameters as if they were the query in a URL
937 queryString = encodeURIComponent(queryString).replace("%20", "+", "g");
938
939 // The first word could be a keyword, so that's what we'll search.
940 let keyword = this._searchTokens[0];
941
942 return [
943 SQL_KEYWORD_QUERY,
944 {
945 keyword: keyword,
946 query_string: queryString,
947 query_type: QUERYTYPE_KEYWORD
948 }
949 ];
950 },
951
952 /**
953 * Obtains the query to search for switch-to-tab entries.
954 *
955 * @return an array consisting of the correctly optimized query to search the
956 * database with and an object containing the params to bound.
957 */
958 get _switchToTabQuery() [
959 SQL_SWITCHTAB_QUERY,
960 {
961 query_type: QUERYTYPE_FILTERED,
962 matchBehavior: this._matchBehavior,
963 searchBehavior: this._behavior,
964 // We only want to search the tokens that we are left with - not the
965 // original search string.
966 searchString: this._searchTokens.join(" "),
967 maxResults: Prefs.maxRichResults
968 }
969 ],
970
971 /**
972 * Obtains the query to search for adaptive results.
973 *
974 * @return an array consisting of the correctly optimized query to search the
975 * database with and an object containing the params to bound.
976 */
977 get _adaptiveQuery() [
978 SQL_ADAPTIVE_QUERY,
979 {
980 parent: PlacesUtils.tagsFolderId,
981 search_string: this._searchString,
982 query_type: QUERYTYPE_FILTERED,
983 matchBehavior: this._matchBehavior,
984 searchBehavior: this._behavior
985 }
986 ],
987
988 /**
989 * Whether we should try to autoFill.
990 */
991 get _shouldAutofill() {
992 // First of all, check for the autoFill pref.
993 if (!Prefs.autofill)
994 return false;
995
996 // Then, we should not try to autofill if the behavior is not the default.
997 // TODO (bug 751709): Ideally we should have a more fine-grained behavior
998 // here, but for now it's enough to just check for default behavior.
999 if (Prefs.defaultBehavior != DEFAULT_BEHAVIOR)
1000 return false;
1001
1002 // Don't autoFill if the search term is recognized as a keyword, otherwise
1003 // it will override default keywords behavior. Note that keywords are
1004 // hashed on first use, so while the first query may delay a little bit,
1005 // next ones will just hit the memory hash.
1006 if (this._searchString.length == 0 ||
1007 PlacesUtils.bookmarks.getURIForKeyword(this._searchString)) {
1008 return false;
1009 }
1010
1011 // Don't try to autofill if the search term includes any whitespace.
1012 // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
1013 // tokenizer ends up trimming the search string and returning a value
1014 // that doesn't match it, or is even shorter.
1015 if (/\s/.test(this._searchString)) {
1016 return false;
1017 }
1018
1019 return true;
1020 },
1021
1022 /**
1023 * Obtains the query to search for autoFill host results.
1024 *
1025 * @return an array consisting of the correctly optimized query to search the
1026 * database with and an object containing the params to bound.
1027 */
1028 get _hostQuery() [
1029 Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY,
1030 {
1031 query_type: QUERYTYPE_AUTOFILL_HOST,
1032 searchString: this._searchString.toLowerCase()
1033 }
1034 ],
1035
1036 /**
1037 * Obtains the query to search for autoFill url results.
1038 *
1039 * @return an array consisting of the correctly optimized query to search the
1040 * database with and an object containing the params to bound.
1041 */
1042 get _urlQuery() [
1043 Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY,
1044 {
1045 query_type: QUERYTYPE_AUTOFILL_URL,
1046 searchString: this._autofillUrlSearchString,
1047 matchBehavior: MATCH_BEGINNING_CASE_SENSITIVE,
1048 searchBehavior: Ci.mozIPlacesAutoComplete.BEHAVIOR_URL
1049 }
1050 ],
1051
1052 /**
1053 * Notifies the listener about results.
1054 *
1055 * @param searchOngoing
1056 * Indicates whether the search is ongoing.
1057 */
1058 notifyResults: function (searchOngoing) {
1059 let result = this._result;
1060 let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
1061 if (searchOngoing) {
1062 resultCode += "_ONGOING";
1063 }
1064 result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
1065 this._listener.onSearchResult(this._autocompleteSearch, result);
1066 },
1067 }
1068
1069 ////////////////////////////////////////////////////////////////////////////////
1070 //// UnifiedComplete class
1071 //// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete
1072
1073 function UnifiedComplete() {
1074 Services.obs.addObserver(this, TOPIC_SHUTDOWN, true);
1075 }
1076
1077 UnifiedComplete.prototype = {
1078 //////////////////////////////////////////////////////////////////////////////
1079 //// nsIObserver
1080
1081 observe: function (subject, topic, data) {
1082 if (topic === TOPIC_SHUTDOWN) {
1083 this.ensureShutdown();
1084 }
1085 },
1086
1087 //////////////////////////////////////////////////////////////////////////////
1088 //// Database handling
1089
1090 /**
1091 * Promise resolved when the database initialization has completed, or null
1092 * if it has never been requested.
1093 */
1094 _promiseDatabase: null,
1095
1096 /**
1097 * Gets a Sqlite database handle.
1098 *
1099 * @return {Promise}
1100 * @resolves to the Sqlite database handle (according to Sqlite.jsm).
1101 * @rejects javascript exception.
1102 */
1103 getDatabaseHandle: function () {
1104 if (Prefs.enabled && !this._promiseDatabase) {
1105 this._promiseDatabase = Task.spawn(function* () {
1106 let conn = yield Sqlite.cloneStorageConnection({
1107 connection: PlacesUtils.history.DBConnection,
1108 readOnly: true
1109 });
1110
1111 // Autocomplete often fallbacks to a table scan due to lack of text
1112 // indices. A larger cache helps reducing IO and improving performance.
1113 // The value used here is larger than the default Storage value defined
1114 // as MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp.
1115 yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB
1116
1117 yield SwitchToTabStorage.initDatabase(conn);
1118
1119 return conn;
1120 }.bind(this)).then(null, Cu.reportError);
1121 }
1122 return this._promiseDatabase;
1123 },
1124
1125 /**
1126 * Used to stop running queries and close the database handle.
1127 */
1128 ensureShutdown: function () {
1129 if (this._promiseDatabase) {
1130 Task.spawn(function* () {
1131 let conn = yield this.getDatabaseHandle();
1132 SwitchToTabStorage.shutdown();
1133 yield conn.close()
1134 }.bind(this)).then(null, Cu.reportError);
1135 this._promiseDatabase = null;
1136 }
1137 },
1138
1139 //////////////////////////////////////////////////////////////////////////////
1140 //// mozIPlacesAutoComplete
1141
1142 registerOpenPage: function PAC_registerOpenPage(uri) {
1143 SwitchToTabStorage.add(uri);
1144 },
1145
1146 unregisterOpenPage: function PAC_unregisterOpenPage(uri) {
1147 SwitchToTabStorage.delete(uri);
1148 },
1149
1150 //////////////////////////////////////////////////////////////////////////////
1151 //// nsIAutoCompleteSearch
1152
1153 startSearch: function (searchString, searchParam, previousResult, listener) {
1154 // Stop the search in case the controller has not taken care of it.
1155 if (this._currentSearch) {
1156 this.stopSearch();
1157 }
1158
1159 // Note: We don't use previousResult to make sure ordering of results are
1160 // consistent. See bug 412730 for more details.
1161
1162 this._currentSearch = new Search(searchString, searchParam, listener,
1163 this, this);
1164
1165 // If we are not enabled, we need to return now. Notice we need an empty
1166 // result regardless, so we still create the Search object.
1167 if (!Prefs.enabled) {
1168 this.finishSearch(true);
1169 return;
1170 }
1171
1172 let search = this._currentSearch;
1173 this.getDatabaseHandle().then(conn => search.execute(conn))
1174 .then(() => {
1175 if (search == this._currentSearch) {
1176 this.finishSearch(true);
1177 }
1178 }, Cu.reportError);
1179 },
1180
1181 stopSearch: function () {
1182 if (this._currentSearch) {
1183 this._currentSearch.cancel();
1184 }
1185 this.finishSearch();
1186 },
1187
1188 /**
1189 * Properly cleans up when searching is completed.
1190 *
1191 * @param notify [optional]
1192 * Indicates if we should notify the AutoComplete listener about our
1193 * results or not.
1194 */
1195 finishSearch: function (notify=false) {
1196 // Notify about results if we are supposed to.
1197 if (notify) {
1198 this._currentSearch.notifyResults(false);
1199 }
1200
1201 // Clear our state
1202 TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT);
1203 delete this._currentSearch;
1204 },
1205
1206 //////////////////////////////////////////////////////////////////////////////
1207 //// nsIAutoCompleteSimpleResultListener
1208
1209 onValueRemoved: function (result, spec, removeFromDB) {
1210 if (removeFromDB) {
1211 PlacesUtils.history.removePage(NetUtil.newURI(spec));
1212 }
1213 },
1214
1215 //////////////////////////////////////////////////////////////////////////////
1216 //// nsIAutoCompleteSearchDescriptor
1217
1218 get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE,
1219
1220 //////////////////////////////////////////////////////////////////////////////
1221 //// nsISupports
1222
1223 classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"),
1224
1225 _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete),
1226
1227 QueryInterface: XPCOMUtils.generateQI([
1228 Ci.nsIAutoCompleteSearch,
1229 Ci.nsIAutoCompleteSimpleResultListener,
1230 Ci.mozIPlacesAutoComplete,
1231 Ci.nsIObserver,
1232 Ci.nsISupportsWeakReference
1233 ])
1234 };
1235
1236 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]);

mercurial