michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: * vim: sw=2 ts=2 sts=2 expandtab michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Constants michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cu = Components.utils; michael@0: michael@0: const TOPIC_SHUTDOWN = "places-shutdown"; michael@0: const TOPIC_PREFCHANGED = "nsPref:changed"; michael@0: michael@0: const DEFAULT_BEHAVIOR = 0; michael@0: michael@0: const PREF_BRANCH = "browser.urlbar"; michael@0: michael@0: // Prefs are defined as [pref name, default value]. michael@0: const PREF_ENABLED = [ "autocomplete.enabled", true ]; michael@0: const PREF_AUTOFILL = [ "autoFill", true ]; michael@0: const PREF_AUTOFILL_TYPED = [ "autoFill.typed", true ]; michael@0: const PREF_AUTOFILL_PRIORITY = [ "autoFill.priority", true ]; michael@0: const PREF_DELAY = [ "delay", 50 ]; michael@0: const PREF_BEHAVIOR = [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ]; michael@0: const PREF_DEFAULT_BEHAVIOR = [ "default.behavior", DEFAULT_BEHAVIOR ]; michael@0: const PREF_EMPTY_BEHAVIOR = [ "default.behavior.emptyRestriction", michael@0: Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY | michael@0: Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED ]; michael@0: const PREF_FILTER_JS = [ "filter.javascript", true ]; michael@0: const PREF_MAXRESULTS = [ "maxRichResults", 25 ]; michael@0: const PREF_RESTRICT_HISTORY = [ "restrict.history", "^" ]; michael@0: const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark", "*" ]; michael@0: const PREF_RESTRICT_TYPED = [ "restrict.typed", "~" ]; michael@0: const PREF_RESTRICT_TAG = [ "restrict.tag", "+" ]; michael@0: const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage", "%" ]; michael@0: const PREF_MATCH_TITLE = [ "match.title", "#" ]; michael@0: const PREF_MATCH_URL = [ "match.url", "@" ]; michael@0: michael@0: // Match type constants. michael@0: // These indicate what type of search function we should be using. michael@0: const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE; michael@0: const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE; michael@0: const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY; michael@0: const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING; michael@0: const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE; michael@0: michael@0: // AutoComplete query type constants. michael@0: // Describes the various types of queries that we can process rows for. michael@0: const QUERYTYPE_KEYWORD = 0; michael@0: const QUERYTYPE_FILTERED = 1; michael@0: const QUERYTYPE_AUTOFILL_HOST = 2; michael@0: const QUERYTYPE_AUTOFILL_URL = 3; michael@0: michael@0: // This separator is used as an RTL-friendly way to split the title and tags. michael@0: // It can also be used by an nsIAutoCompleteResult consumer to re-split the michael@0: // "comment" back into the title and the tag. michael@0: const TITLE_TAGS_SEPARATOR = " \u2013 "; michael@0: michael@0: // Telemetry probes. michael@0: const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS"; michael@0: michael@0: // The default frecency value used when inserting priority results. michael@0: const FRECENCY_PRIORITY_DEFAULT = 1000; michael@0: michael@0: // Sqlite result row index constants. michael@0: const QUERYINDEX_QUERYTYPE = 0; michael@0: const QUERYINDEX_URL = 1; michael@0: const QUERYINDEX_TITLE = 2; michael@0: const QUERYINDEX_ICONURL = 3; michael@0: const QUERYINDEX_BOOKMARKED = 4; michael@0: const QUERYINDEX_BOOKMARKTITLE = 5; michael@0: const QUERYINDEX_TAGS = 6; michael@0: const QUERYINDEX_VISITCOUNT = 7; michael@0: const QUERYINDEX_TYPED = 8; michael@0: const QUERYINDEX_PLACEID = 9; michael@0: const QUERYINDEX_SWITCHTAB = 10; michael@0: const QUERYINDEX_FRECENCY = 11; michael@0: michael@0: // This SQL query fragment provides the following: michael@0: // - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED) michael@0: // - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE) michael@0: // - the tags associated with a bookmarked entry (QUERYINDEX_TAGS) michael@0: const SQL_BOOKMARK_TAGS_FRAGMENT = sql( michael@0: "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,", michael@0: "( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL", michael@0: "ORDER BY lastModified DESC LIMIT 1", michael@0: ") AS btitle,", michael@0: "( SELECT GROUP_CONCAT(t.title, ',')", michael@0: "FROM moz_bookmarks b", michael@0: "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent", michael@0: "WHERE b.fk = h.id", michael@0: ") AS tags"); michael@0: michael@0: // TODO bug 412736: in case of a frecency tie, we might break it with h.typed michael@0: // and h.visit_count. That is slower though, so not doing it yet... michael@0: const SQL_DEFAULT_QUERY = sql( michael@0: "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",", michael@0: "h.visit_count, h.typed, h.id, t.open_count, h.frecency", michael@0: "FROM moz_places h", michael@0: "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", michael@0: "LEFT JOIN moz_openpages_temp t ON t.url = h.url", michael@0: "WHERE h.frecency <> 0", michael@0: "AND AUTOCOMPLETE_MATCH(:searchString, h.url,", michael@0: "IFNULL(btitle, h.title), tags,", michael@0: "h.visit_count, h.typed,", michael@0: "bookmarked, t.open_count,", michael@0: ":matchBehavior, :searchBehavior)", michael@0: "/*CONDITIONS*/", michael@0: "ORDER BY h.frecency DESC, h.id DESC", michael@0: "LIMIT :maxResults"); michael@0: michael@0: // Enforce ignoring the visit_count index, since the frecency one is much michael@0: // faster in this case. ANALYZE helps the query planner to figure out the michael@0: // faster path, but it may not have up-to-date information yet. michael@0: const SQL_HISTORY_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", michael@0: "AND +h.visit_count > 0", "g"); michael@0: michael@0: const SQL_BOOKMARK_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", michael@0: "AND bookmarked", "g"); michael@0: michael@0: const SQL_TAGS_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", michael@0: "AND tags NOTNULL", "g"); michael@0: michael@0: const SQL_TYPED_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/", michael@0: "AND h.typed = 1", "g"); michael@0: michael@0: const SQL_SWITCHTAB_QUERY = sql( michael@0: "SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,", michael@0: "t.open_count, NULL", michael@0: "FROM moz_openpages_temp t", michael@0: "LEFT JOIN moz_places h ON h.url = t.url", michael@0: "WHERE h.id IS NULL", michael@0: "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,", michael@0: "NULL, NULL, NULL, t.open_count,", michael@0: ":matchBehavior, :searchBehavior)", michael@0: "ORDER BY t.ROWID DESC", michael@0: "LIMIT :maxResults"); michael@0: michael@0: const SQL_ADAPTIVE_QUERY = sql( michael@0: "/* do not warn (bug 487789) */", michael@0: "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",", michael@0: "h.visit_count, h.typed, h.id, t.open_count, h.frecency", michael@0: "FROM (", michael@0: "SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,", michael@0: "place_id", michael@0: "FROM moz_inputhistory", michael@0: "WHERE input BETWEEN :search_string AND :search_string || X'FFFF'", michael@0: "GROUP BY place_id", michael@0: ") AS i", michael@0: "JOIN moz_places h ON h.id = i.place_id", michael@0: "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", michael@0: "LEFT JOIN moz_openpages_temp t ON t.url = h.url", michael@0: "WHERE AUTOCOMPLETE_MATCH(NULL, h.url,", michael@0: "IFNULL(btitle, h.title), tags,", michael@0: "h.visit_count, h.typed, bookmarked,", michael@0: "t.open_count,", michael@0: ":matchBehavior, :searchBehavior)", michael@0: "ORDER BY rank DESC, h.frecency DESC"); michael@0: michael@0: const SQL_KEYWORD_QUERY = sql( michael@0: "/* do not warn (bug 487787) */", michael@0: "SELECT :query_type,", michael@0: "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)", michael@0: "AS search_url, h.title,", michael@0: "IFNULL(f.url, (SELECT f.url", michael@0: "FROM moz_places", michael@0: "JOIN moz_favicons f ON f.id = favicon_id", michael@0: "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)", michael@0: "ORDER BY frecency DESC", michael@0: "LIMIT 1)", michael@0: "),", michael@0: "1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),", michael@0: "t.open_count, h.frecency", michael@0: "FROM moz_keywords k", michael@0: "JOIN moz_bookmarks b ON b.keyword_id = k.id", michael@0: "LEFT JOIN moz_places h ON h.url = search_url", michael@0: "LEFT JOIN moz_favicons f ON f.id = h.favicon_id", michael@0: "LEFT JOIN moz_openpages_temp t ON t.url = search_url", michael@0: "WHERE LOWER(k.keyword) = LOWER(:keyword)", michael@0: "ORDER BY h.frecency DESC"); michael@0: michael@0: const SQL_HOST_QUERY = sql( michael@0: "/* do not warn (bug NA): not worth to index on (typed, frecency) */", michael@0: "SELECT :query_type, host || '/', prefix || host || '/',", michael@0: "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency", michael@0: "FROM moz_hosts", michael@0: "WHERE host BETWEEN :searchString AND :searchString || X'FFFF'", michael@0: "AND frecency <> 0", michael@0: "/*CONDITIONS*/", michael@0: "ORDER BY frecency DESC", michael@0: "LIMIT 1"); michael@0: michael@0: const SQL_TYPED_HOST_QUERY = SQL_HOST_QUERY.replace("/*CONDITIONS*/", michael@0: "AND typed = 1"); michael@0: const SQL_URL_QUERY = sql( michael@0: "/* do not warn (bug no): cannot use an index */", michael@0: "SELECT :query_type, h.url,", michael@0: "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, h.frecency", michael@0: "FROM moz_places h", michael@0: "WHERE h.frecency <> 0", michael@0: "/*CONDITIONS*/", michael@0: "AND AUTOCOMPLETE_MATCH(:searchString, h.url,", michael@0: "h.title, '',", michael@0: "h.visit_count, h.typed, 0, 0,", michael@0: ":matchBehavior, :searchBehavior)", michael@0: "ORDER BY h.frecency DESC, h.id DESC", michael@0: "LIMIT 1"); michael@0: michael@0: const SQL_TYPED_URL_QUERY = SQL_URL_QUERY.replace("/*CONDITIONS*/", michael@0: "AND typed = 1"); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Getters michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", michael@0: "resource://gre/modules/TelemetryStopwatch.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Preferences", michael@0: "resource://gre/modules/Preferences.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Sqlite", michael@0: "resource://gre/modules/Sqlite.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PriorityUrlProvider", michael@0: "resource://gre/modules/PriorityUrlProvider.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "textURIService", michael@0: "@mozilla.org/intl/texttosuburi;1", michael@0: "nsITextToSubURI"); michael@0: michael@0: /** michael@0: * Storage object for switch-to-tab entries. michael@0: * This takes care of caching and registering open pages, that will be reused michael@0: * by switch-to-tab queries. It has an internal cache, so that the Sqlite michael@0: * store is lazy initialized only on first use. michael@0: * It has a simple API: michael@0: * initDatabase(conn): initializes the temporary Sqlite entities to store data michael@0: * add(uri): adds a given nsIURI to the store michael@0: * delete(uri): removes a given nsIURI from the store michael@0: * shutdown(): stops storing data to Sqlite michael@0: */ michael@0: XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({ michael@0: _conn: null, michael@0: // Temporary queue used while the database connection is not available. michael@0: _queue: new Set(), michael@0: initDatabase: Task.async(function* (conn) { michael@0: // To reduce IO use an in-memory table for switch-to-tab tracking. michael@0: // Note: this should be kept up-to-date with the definition in michael@0: // nsPlacesTables.h. michael@0: yield conn.execute(sql( michael@0: "CREATE TEMP TABLE moz_openpages_temp (", michael@0: "url TEXT PRIMARY KEY,", michael@0: "open_count INTEGER", michael@0: ")")); michael@0: michael@0: // Note: this should be kept up-to-date with the definition in michael@0: // nsPlacesTriggers.h. michael@0: yield conn.execute(sql( michael@0: "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger", michael@0: "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW", michael@0: "WHEN NEW.open_count = 0", michael@0: "BEGIN", michael@0: "DELETE FROM moz_openpages_temp", michael@0: "WHERE url = NEW.url;", michael@0: "END")); michael@0: michael@0: this._conn = conn; michael@0: michael@0: // Populate the table with the current cache contents... michael@0: this._queue.forEach(this.add, this); michael@0: // ...then clear it to avoid double additions. michael@0: this._queue.clear(); michael@0: }), michael@0: michael@0: add: function (uri) { michael@0: if (!this._conn) { michael@0: this._queue.add(uri); michael@0: return; michael@0: } michael@0: this._conn.executeCached(sql( michael@0: "INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)", michael@0: "VALUES ( :url, IFNULL( (SELECT open_count + 1", michael@0: "FROM moz_openpages_temp", michael@0: "WHERE url = :url),", michael@0: "1", michael@0: ")", michael@0: ")" michael@0: ), { url: uri.spec }); michael@0: }, michael@0: michael@0: delete: function (uri) { michael@0: if (!this._conn) { michael@0: this._queue.delete(uri); michael@0: return; michael@0: } michael@0: this._conn.executeCached(sql( michael@0: "UPDATE moz_openpages_temp", michael@0: "SET open_count = open_count - 1", michael@0: "WHERE url = :url" michael@0: ), { url: uri.spec }); michael@0: }, michael@0: michael@0: shutdown: function () { michael@0: this._conn = null; michael@0: this._queue.clear(); michael@0: } michael@0: })); michael@0: michael@0: /** michael@0: * This helper keeps track of preferences and keeps their values up-to-date. michael@0: */ michael@0: XPCOMUtils.defineLazyGetter(this, "Prefs", () => { michael@0: let prefs = new Preferences(PREF_BRANCH); michael@0: michael@0: function loadPrefs() { michael@0: store.enabled = prefs.get(...PREF_ENABLED); michael@0: store.autofill = prefs.get(...PREF_AUTOFILL); michael@0: store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED); michael@0: store.autofillPriority = prefs.get(...PREF_AUTOFILL_PRIORITY); michael@0: store.delay = prefs.get(...PREF_DELAY); michael@0: store.matchBehavior = prefs.get(...PREF_BEHAVIOR); michael@0: store.filterJavaScript = prefs.get(...PREF_FILTER_JS); michael@0: store.maxRichResults = prefs.get(...PREF_MAXRESULTS); michael@0: store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY); michael@0: store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS); michael@0: store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED); michael@0: store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG); michael@0: store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB); michael@0: store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE); michael@0: store.matchURLToken = prefs.get(...PREF_MATCH_URL); michael@0: store.defaultBehavior = prefs.get(...PREF_DEFAULT_BEHAVIOR); michael@0: // Further restrictions to apply for "empty searches" (i.e. searches for ""). michael@0: store.emptySearchDefaultBehavior = store.defaultBehavior | michael@0: prefs.get(...PREF_EMPTY_BEHAVIOR); michael@0: michael@0: // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE. michael@0: if (store.matchBehavior != MATCH_ANYWHERE && michael@0: store.matchBehavior != MATCH_BOUNDARY && michael@0: store.matchBehavior != MATCH_BEGINNING) { michael@0: store.matchBehavior = MATCH_BOUNDARY_ANYWHERE; michael@0: } michael@0: michael@0: store.tokenToBehaviorMap = new Map([ michael@0: [ store.restrictHistoryToken, "history" ], michael@0: [ store.restrictBookmarkToken, "bookmark" ], michael@0: [ store.restrictTagToken, "tag" ], michael@0: [ store.restrictOpenPageToken, "openpage" ], michael@0: [ store.matchTitleToken, "title" ], michael@0: [ store.matchURLToken, "url" ], michael@0: [ store.restrictTypedToken, "typed" ] michael@0: ]); michael@0: } michael@0: michael@0: let store = { michael@0: observe: function (subject, topic, data) { michael@0: loadPrefs(); michael@0: }, michael@0: QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ]) michael@0: }; michael@0: loadPrefs(); michael@0: prefs.observe("", store); michael@0: michael@0: return Object.seal(store); michael@0: }); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Helper functions michael@0: michael@0: /** michael@0: * Joins multiple sql tokens into a single sql query. michael@0: */ michael@0: function sql(...parts) parts.join(" "); michael@0: michael@0: /** michael@0: * Used to unescape encoded URI strings and drop information that we do not michael@0: * care about. michael@0: * michael@0: * @param spec michael@0: * The text to unescape and modify. michael@0: * @return the modified spec. michael@0: */ michael@0: function fixupSearchText(spec) michael@0: textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec)); michael@0: michael@0: /** michael@0: * Generates the tokens used in searching from a given string. michael@0: * michael@0: * @param searchString michael@0: * The string to generate tokens from. michael@0: * @return an array of tokens. michael@0: * @note Calling split on an empty string will return an array containing one michael@0: * empty string. We don't want that, as it'll break our logic, so return michael@0: * an empty array then. michael@0: */ michael@0: function getUnfilteredSearchTokens(searchString) michael@0: searchString.length ? searchString.split(" ") : []; michael@0: michael@0: /** michael@0: * Strip prefixes from the URI that we don't care about for searching. michael@0: * michael@0: * @param spec michael@0: * The text to modify. michael@0: * @return the modified spec. michael@0: */ michael@0: function stripPrefix(spec) michael@0: { michael@0: ["http://", "https://", "ftp://"].some(scheme => { michael@0: if (spec.startsWith(scheme)) { michael@0: spec = spec.slice(scheme.length); michael@0: return true; michael@0: } michael@0: return false; michael@0: }); michael@0: michael@0: if (spec.startsWith("www.")) { michael@0: spec = spec.slice(4); michael@0: } michael@0: return spec; michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Search Class michael@0: //// Manages a single instance of an autocomplete search. michael@0: michael@0: function Search(searchString, searchParam, autocompleteListener, michael@0: resultListener, autocompleteSearch) { michael@0: // We want to store the original string with no leading or trailing michael@0: // whitespace for case sensitive searches. michael@0: this._originalSearchString = searchString.trim(); michael@0: this._searchString = fixupSearchText(this._originalSearchString.toLowerCase()); michael@0: this._searchTokens = michael@0: this.filterTokens(getUnfilteredSearchTokens(this._searchString)); michael@0: // The protocol and the host are lowercased by nsIURI, so it's fine to michael@0: // lowercase the typed prefix, to add it back to the results later. michael@0: this._strippedPrefix = this._originalSearchString.slice( michael@0: 0, this._originalSearchString.length - this._searchString.length michael@0: ).toLowerCase(); michael@0: // The URIs in the database are fixed-up, so we can match on a lowercased michael@0: // host, but the path must be matched in a case sensitive way. michael@0: let pathIndex = michael@0: this._originalSearchString.indexOf("/", this._strippedPrefix.length); michael@0: this._autofillUrlSearchString = fixupSearchText( michael@0: this._originalSearchString.slice(0, pathIndex).toLowerCase() + michael@0: this._originalSearchString.slice(pathIndex) michael@0: ); michael@0: michael@0: this._enableActions = searchParam.split(" ").indexOf("enable-actions") != -1; michael@0: michael@0: this._listener = autocompleteListener; michael@0: this._autocompleteSearch = autocompleteSearch; michael@0: michael@0: this._matchBehavior = Prefs.matchBehavior; michael@0: // Set the default behavior for this search. michael@0: this._behavior = this._searchString ? Prefs.defaultBehavior michael@0: : Prefs.emptySearchDefaultBehavior; michael@0: // Create a new result to add eventual matches. Note we need a result michael@0: // regardless having matches. michael@0: let result = Cc["@mozilla.org/autocomplete/simple-result;1"] michael@0: .createInstance(Ci.nsIAutoCompleteSimpleResult); michael@0: result.setSearchString(searchString); michael@0: result.setListener(resultListener); michael@0: // Will be set later, if needed. michael@0: result.setDefaultIndex(-1); michael@0: this._result = result; michael@0: michael@0: // These are used to avoid adding duplicate entries to the results. michael@0: this._usedURLs = new Set(); michael@0: this._usedPlaceIds = new Set(); michael@0: } michael@0: michael@0: Search.prototype = { michael@0: /** michael@0: * Enables the desired AutoComplete behavior. michael@0: * michael@0: * @param type michael@0: * The behavior type to set. michael@0: */ michael@0: setBehavior: function (type) { michael@0: this._behavior |= michael@0: Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]; michael@0: }, michael@0: michael@0: /** michael@0: * Determines if the specified AutoComplete behavior is set. michael@0: * michael@0: * @param aType michael@0: * The behavior type to test for. michael@0: * @return true if the behavior is set, false otherwise. michael@0: */ michael@0: hasBehavior: function (type) { michael@0: return this._behavior & michael@0: Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]; michael@0: }, michael@0: michael@0: /** michael@0: * Used to delay the most complex queries, to save IO while the user is michael@0: * typing. michael@0: */ michael@0: _sleepDeferred: null, michael@0: _sleep: function (aTimeMs) { michael@0: // Reuse a single instance to try shaving off some usless work before michael@0: // the first query. michael@0: if (!this._sleepTimer) michael@0: this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this._sleepDeferred = Promise.defer(); michael@0: this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(), michael@0: aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT); michael@0: return this._sleepDeferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Given an array of tokens, this function determines which query should be michael@0: * ran. It also removes any special search tokens. michael@0: * michael@0: * @param tokens michael@0: * An array of search tokens. michael@0: * @return the filtered list of tokens to search with. michael@0: */ michael@0: filterTokens: function (tokens) { michael@0: // Set the proper behavior while filtering tokens. michael@0: for (let i = tokens.length - 1; i >= 0; i--) { michael@0: let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]); michael@0: // Don't remove the token if it didn't match, or if it's an action but michael@0: // actions are not enabled. michael@0: if (behavior && (behavior != "openpage" || this._enableActions)) { michael@0: this.setBehavior(behavior); michael@0: tokens.splice(i, 1); michael@0: } michael@0: } michael@0: michael@0: // Set the right JavaScript behavior based on our preference. Note that the michael@0: // preference is whether or not we should filter JavaScript, and the michael@0: // behavior is if we should search it or not. michael@0: if (!Prefs.filterJavaScript) { michael@0: this.setBehavior("javascript"); michael@0: } michael@0: michael@0: return tokens; michael@0: }, michael@0: michael@0: /** michael@0: * Used to cancel this search, will stop providing results. michael@0: */ michael@0: cancel: function () { michael@0: if (this._sleepTimer) michael@0: this._sleepTimer.cancel(); michael@0: if (this._sleepDeferred) { michael@0: this._sleepDeferred.resolve(); michael@0: this._sleepDeferred = null; michael@0: } michael@0: delete this._pendingQuery; michael@0: }, michael@0: michael@0: /** michael@0: * Whether this search is running. michael@0: */ michael@0: get pending() !!this._pendingQuery, michael@0: michael@0: /** michael@0: * Execute the search and populate results. michael@0: * @param conn michael@0: * The Sqlite connection. michael@0: */ michael@0: execute: Task.async(function* (conn) { michael@0: this._pendingQuery = true; michael@0: TelemetryStopwatch.start(TELEMETRY_1ST_RESULT); michael@0: michael@0: // For any given search, we run many queries: michael@0: // 1) priority domains michael@0: // 2) inline completion michael@0: // 3) keywords (this._keywordQuery) michael@0: // 4) adaptive learning (this._adaptiveQuery) michael@0: // 5) open pages not supported by history (this._switchToTabQuery) michael@0: // 6) query based on match behavior michael@0: // michael@0: // (3) only gets ran if we get any filtered tokens, since if there are no michael@0: // tokens, there is nothing to match. michael@0: michael@0: // Get the final query, based on the tokens found in the search string. michael@0: let queries = [ this._adaptiveQuery, michael@0: this._switchToTabQuery, michael@0: this._searchQuery ]; michael@0: michael@0: if (this._searchTokens.length == 1) { michael@0: yield this._matchPriorityUrl(); michael@0: } else if (this._searchTokens.length > 1) { michael@0: queries.unshift(this._keywordQuery); michael@0: } michael@0: michael@0: if (this._shouldAutofill) { michael@0: // Hosts have no "/" in them. michael@0: let lastSlashIndex = this._searchString.lastIndexOf("/"); michael@0: // Search only URLs if there's a slash in the search string... michael@0: if (lastSlashIndex != -1) { michael@0: // ...but not if it's exactly at the end of the search string. michael@0: if (lastSlashIndex < this._searchString.length - 1) { michael@0: queries.unshift(this._urlQuery); michael@0: } michael@0: } else if (this.pending) { michael@0: // The host query is executed immediately, while any other is delayed michael@0: // to avoid overloading the connection. michael@0: let [ query, params ] = this._hostQuery; michael@0: yield conn.executeCached(query, params, this._onResultRow.bind(this)); michael@0: } michael@0: } michael@0: michael@0: yield this._sleep(Prefs.delay); michael@0: if (!this.pending) michael@0: return; michael@0: michael@0: for (let [query, params] of queries) { michael@0: yield conn.executeCached(query, params, this._onResultRow.bind(this)); michael@0: if (!this.pending) michael@0: return; michael@0: } michael@0: michael@0: // If we do not have enough results, and our match type is michael@0: // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more michael@0: // results. michael@0: if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE && michael@0: this._result.matchCount < Prefs.maxRichResults) { michael@0: this._matchBehavior = MATCH_ANYWHERE; michael@0: for (let [query, params] of [ this._adaptiveQuery, michael@0: this._searchQuery ]) { michael@0: yield conn.executeCached(query, params, this._onResultRow); michael@0: if (!this.pending) michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // If we didn't find enough matches and we have some frecency-driven michael@0: // matches, add them. michael@0: if (this._frecencyMatches) { michael@0: this._frecencyMatches.forEach(this._addMatch, this); michael@0: } michael@0: }), michael@0: michael@0: _matchPriorityUrl: function* () { michael@0: if (!Prefs.autofillPriority) michael@0: return; michael@0: let priorityMatch = yield PriorityUrlProvider.getMatch(this._searchString); michael@0: if (priorityMatch) { michael@0: this._result.setDefaultIndex(0); michael@0: this._addFrecencyMatch({ michael@0: value: priorityMatch.token, michael@0: comment: priorityMatch.title, michael@0: icon: priorityMatch.iconUrl, michael@0: style: "priority-" + priorityMatch.reason, michael@0: finalCompleteValue: priorityMatch.url, michael@0: frecency: FRECENCY_PRIORITY_DEFAULT michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: _onResultRow: function (row) { michael@0: TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT); michael@0: let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE); michael@0: let match; michael@0: switch (queryType) { michael@0: case QUERYTYPE_AUTOFILL_HOST: michael@0: this._result.setDefaultIndex(0); michael@0: match = this._processHostRow(row); michael@0: break; michael@0: case QUERYTYPE_AUTOFILL_URL: michael@0: this._result.setDefaultIndex(0); michael@0: match = this._processUrlRow(row); michael@0: break; michael@0: case QUERYTYPE_FILTERED: michael@0: case QUERYTYPE_KEYWORD: michael@0: match = this._processRow(row); michael@0: break; michael@0: } michael@0: this._addMatch(match); michael@0: }, michael@0: michael@0: /** michael@0: * These matches should be mixed up with other matches, based on frecency. michael@0: */ michael@0: _addFrecencyMatch: function (match) { michael@0: if (!this._frecencyMatches) michael@0: this._frecencyMatches = []; michael@0: this._frecencyMatches.push(match); michael@0: // We keep this array in reverse order, so we can walk it and remove stuff michael@0: // from it in one pass. Notice that for frecency reverse order means from michael@0: // lower to higher. michael@0: this._frecencyMatches.sort((a, b) => a.frecency - b.frecency); michael@0: }, michael@0: michael@0: _addMatch: function (match) { michael@0: let notifyResults = false; michael@0: michael@0: if (this._frecencyMatches) { michael@0: for (let i = this._frecencyMatches.length - 1; i >= 0 ; i--) { michael@0: if (this._frecencyMatches[i].frecency > match.frecency) { michael@0: this._addMatch(this._frecencyMatches.splice(i, 1)[0]); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Must check both id and url, cause keywords dinamically modify the url. michael@0: if ((!match.placeId || !this._usedPlaceIds.has(match.placeId)) && michael@0: !this._usedURLs.has(stripPrefix(match.value))) { michael@0: // Add this to our internal tracker to ensure duplicates do not end up in michael@0: // the result. michael@0: // Not all entries have a place id, thus we fallback to the url for them. michael@0: // We cannot use only the url since keywords entries are modified to michael@0: // include the search string, and would be returned multiple times. Ids michael@0: // are faster too. michael@0: if (match.placeId) michael@0: this._usedPlaceIds.add(match.placeId); michael@0: this._usedURLs.add(stripPrefix(match.value)); michael@0: michael@0: this._result.appendMatch(match.value, michael@0: match.comment, michael@0: match.icon || PlacesUtils.favicons.defaultFavicon.spec, michael@0: match.style || "favicon", michael@0: match.finalCompleteValue); michael@0: notifyResults = true; michael@0: } michael@0: michael@0: if (this._result.matchCount == Prefs.maxRichResults || !this.pending) { michael@0: // We have enough results, so stop running our search. michael@0: this.cancel(); michael@0: // This tells Sqlite.jsm to stop providing us results and cancel the michael@0: // underlying query. michael@0: throw StopIteration; michael@0: } michael@0: michael@0: if (notifyResults) { michael@0: // Notify about results if we've gotten them. michael@0: this.notifyResults(true); michael@0: } michael@0: }, michael@0: michael@0: _processHostRow: function (row) { michael@0: let match = {}; michael@0: let trimmedHost = row.getResultByIndex(QUERYINDEX_URL); michael@0: let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE); michael@0: let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); michael@0: // If the untrimmed value doesn't preserve the user's input just michael@0: // ignore it and complete to the found host. michael@0: if (untrimmedHost && michael@0: !untrimmedHost.toLowerCase().contains(this._originalSearchString.toLowerCase())) { michael@0: // THIS CAUSES null TO BE SHOWN AS TITLE. michael@0: untrimmedHost = null; michael@0: } michael@0: michael@0: match.value = this._strippedPrefix + trimmedHost; michael@0: match.comment = trimmedHost; michael@0: match.finalCompleteValue = untrimmedHost; michael@0: match.frecency = frecency; michael@0: return match; michael@0: }, michael@0: michael@0: _processUrlRow: function (row) { michael@0: let match = {}; michael@0: let value = row.getResultByIndex(QUERYINDEX_URL); michael@0: let url = fixupSearchText(value); michael@0: let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); michael@0: michael@0: let prefix = value.slice(0, value.length - stripPrefix(value).length); michael@0: michael@0: // We must complete the URL up to the next separator (which is /, ? or #). michael@0: let separatorIndex = url.slice(this._searchString.length) michael@0: .search(/[\/\?\#]/); michael@0: if (separatorIndex != -1) { michael@0: separatorIndex += this._searchString.length; michael@0: if (url[separatorIndex] == "/") { michael@0: separatorIndex++; // Include the "/" separator michael@0: } michael@0: url = url.slice(0, separatorIndex); michael@0: } michael@0: michael@0: // If the untrimmed value doesn't preserve the user's input just michael@0: // ignore it and complete to the found url. michael@0: let untrimmedURL = prefix + url; michael@0: if (untrimmedURL && michael@0: !untrimmedURL.toLowerCase().contains(this._originalSearchString.toLowerCase())) { michael@0: // THIS CAUSES null TO BE SHOWN AS TITLE. michael@0: untrimmedURL = null; michael@0: } michael@0: michael@0: match.value = this._strippedPrefix + url; michael@0: match.comment = url; michael@0: match.finalCompleteValue = untrimmedURL; michael@0: match.frecency = frecency; michael@0: return match; michael@0: }, michael@0: michael@0: _processRow: function (row) { michael@0: let match = {}; michael@0: match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID); michael@0: let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE); michael@0: let escapedURL = row.getResultByIndex(QUERYINDEX_URL); michael@0: let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0; michael@0: let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || ""; michael@0: let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || ""; michael@0: let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED); michael@0: let bookmarkTitle = bookmarked ? michael@0: row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null; michael@0: let tags = row.getResultByIndex(QUERYINDEX_TAGS) || ""; michael@0: let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY); michael@0: michael@0: // If actions are enabled and the page is open, add only the switch-to-tab michael@0: // result. Otherwise, add the normal result. michael@0: let [url, action] = this._enableActions && openPageCount > 0 ? michael@0: ["moz-action:switchtab," + escapedURL, "action "] : michael@0: [escapedURL, ""]; michael@0: michael@0: // Always prefer the bookmark title unless it is empty michael@0: let title = bookmarkTitle || historyTitle; michael@0: michael@0: if (queryType == QUERYTYPE_KEYWORD) { michael@0: // If we do not have a title, then we must have a keyword, so let the UI michael@0: // know it is a keyword. Otherwise, we found an exact page match, so just michael@0: // show the page like a regular result. Because the page title is likely michael@0: // going to be more specific than the bookmark title (keyword title). michael@0: if (!historyTitle) { michael@0: match.style = "keyword"; michael@0: } michael@0: else { michael@0: title = historyTitle; michael@0: } michael@0: } michael@0: michael@0: // We will always prefer to show tags if we have them. michael@0: let showTags = !!tags; michael@0: michael@0: // However, we'll act as if a page is not bookmarked or tagged if the user michael@0: // only wants only history and not bookmarks or tags. michael@0: if (this.hasBehavior("history") && michael@0: !(this.hasBehavior("bookmark") || this.hasBehavior("tag"))) { michael@0: showTags = false; michael@0: match.style = "favicon"; michael@0: } michael@0: michael@0: // If we have tags and should show them, we need to add them to the title. michael@0: if (showTags) { michael@0: title += TITLE_TAGS_SEPARATOR + tags; michael@0: } michael@0: michael@0: // We have to determine the right style to display. Tags show the tag icon, michael@0: // bookmarks get the bookmark icon, and keywords get the keyword icon. If michael@0: // the result does not fall into any of those, it just gets the favicon. michael@0: if (!match.style) { michael@0: // It is possible that we already have a style set (from a keyword michael@0: // search or because of the user's preferences), so only set it if we michael@0: // haven't already done so. michael@0: if (showTags) { michael@0: match.style = "tag"; michael@0: } michael@0: else if (bookmarked) { michael@0: match.style = "bookmark"; michael@0: } michael@0: } michael@0: michael@0: match.value = url; michael@0: match.comment = title; michael@0: if (iconurl) { michael@0: match.icon = PlacesUtils.favicons michael@0: .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec; michael@0: } michael@0: match.frecency = frecency; michael@0: michael@0: return match; michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the search query to be used based on the previously set search michael@0: * behaviors (accessed by this.hasBehavior). michael@0: * michael@0: * @return an array consisting of the correctly optimized query to search the michael@0: * database with and an object containing the params to bound. michael@0: */ michael@0: get _searchQuery() { michael@0: // We use more optimized queries for restricted searches, so we will always michael@0: // return the most restrictive one to the least restrictive one if more than michael@0: // one token is found. michael@0: // Note: "openpages" behavior is supported by the default query. michael@0: // _switchToTabQuery instead returns only pages not supported by michael@0: // history and it is always executed. michael@0: let query = this.hasBehavior("tag") ? SQL_TAGS_QUERY : michael@0: this.hasBehavior("bookmark") ? SQL_BOOKMARK_QUERY : michael@0: this.hasBehavior("typed") ? SQL_TYPED_QUERY : michael@0: this.hasBehavior("history") ? SQL_HISTORY_QUERY : michael@0: SQL_DEFAULT_QUERY; michael@0: michael@0: return [ michael@0: query, michael@0: { michael@0: parent: PlacesUtils.tagsFolderId, michael@0: query_type: QUERYTYPE_FILTERED, michael@0: matchBehavior: this._matchBehavior, michael@0: searchBehavior: this._behavior, michael@0: // We only want to search the tokens that we are left with - not the michael@0: // original search string. michael@0: searchString: this._searchTokens.join(" "), michael@0: // Limit the query to the the maximum number of desired results. michael@0: // This way we can avoid doing more work than needed. michael@0: maxResults: Prefs.maxRichResults michael@0: } michael@0: ]; michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the query to search for keywords. michael@0: * michael@0: * @return an array consisting of the correctly optimized query to search the michael@0: * database with and an object containing the params to bound. michael@0: */ michael@0: get _keywordQuery() { michael@0: // The keyword is the first word in the search string, with the parameters michael@0: // following it. michael@0: let searchString = this._originalSearchString; michael@0: let queryString = ""; michael@0: let queryIndex = searchString.indexOf(" "); michael@0: if (queryIndex != -1) { michael@0: queryString = searchString.substring(queryIndex + 1); michael@0: } michael@0: // We need to escape the parameters as if they were the query in a URL michael@0: queryString = encodeURIComponent(queryString).replace("%20", "+", "g"); michael@0: michael@0: // The first word could be a keyword, so that's what we'll search. michael@0: let keyword = this._searchTokens[0]; michael@0: michael@0: return [ michael@0: SQL_KEYWORD_QUERY, michael@0: { michael@0: keyword: keyword, michael@0: query_string: queryString, michael@0: query_type: QUERYTYPE_KEYWORD michael@0: } michael@0: ]; michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the query to search for switch-to-tab entries. michael@0: * michael@0: * @return an array consisting of the correctly optimized query to search the michael@0: * database with and an object containing the params to bound. michael@0: */ michael@0: get _switchToTabQuery() [ michael@0: SQL_SWITCHTAB_QUERY, michael@0: { michael@0: query_type: QUERYTYPE_FILTERED, michael@0: matchBehavior: this._matchBehavior, michael@0: searchBehavior: this._behavior, michael@0: // We only want to search the tokens that we are left with - not the michael@0: // original search string. michael@0: searchString: this._searchTokens.join(" "), michael@0: maxResults: Prefs.maxRichResults michael@0: } michael@0: ], michael@0: michael@0: /** michael@0: * Obtains the query to search for adaptive results. michael@0: * michael@0: * @return an array consisting of the correctly optimized query to search the michael@0: * database with and an object containing the params to bound. michael@0: */ michael@0: get _adaptiveQuery() [ michael@0: SQL_ADAPTIVE_QUERY, michael@0: { michael@0: parent: PlacesUtils.tagsFolderId, michael@0: search_string: this._searchString, michael@0: query_type: QUERYTYPE_FILTERED, michael@0: matchBehavior: this._matchBehavior, michael@0: searchBehavior: this._behavior michael@0: } michael@0: ], michael@0: michael@0: /** michael@0: * Whether we should try to autoFill. michael@0: */ michael@0: get _shouldAutofill() { michael@0: // First of all, check for the autoFill pref. michael@0: if (!Prefs.autofill) michael@0: return false; michael@0: michael@0: // Then, we should not try to autofill if the behavior is not the default. michael@0: // TODO (bug 751709): Ideally we should have a more fine-grained behavior michael@0: // here, but for now it's enough to just check for default behavior. michael@0: if (Prefs.defaultBehavior != DEFAULT_BEHAVIOR) michael@0: return false; michael@0: michael@0: // Don't autoFill if the search term is recognized as a keyword, otherwise michael@0: // it will override default keywords behavior. Note that keywords are michael@0: // hashed on first use, so while the first query may delay a little bit, michael@0: // next ones will just hit the memory hash. michael@0: if (this._searchString.length == 0 || michael@0: PlacesUtils.bookmarks.getURIForKeyword(this._searchString)) { michael@0: return false; michael@0: } michael@0: michael@0: // Don't try to autofill if the search term includes any whitespace. michael@0: // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH michael@0: // tokenizer ends up trimming the search string and returning a value michael@0: // that doesn't match it, or is even shorter. michael@0: if (/\s/.test(this._searchString)) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Obtains the query to search for autoFill host results. michael@0: * michael@0: * @return an array consisting of the correctly optimized query to search the michael@0: * database with and an object containing the params to bound. michael@0: */ michael@0: get _hostQuery() [ michael@0: Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY, michael@0: { michael@0: query_type: QUERYTYPE_AUTOFILL_HOST, michael@0: searchString: this._searchString.toLowerCase() michael@0: } michael@0: ], michael@0: michael@0: /** michael@0: * Obtains the query to search for autoFill url results. michael@0: * michael@0: * @return an array consisting of the correctly optimized query to search the michael@0: * database with and an object containing the params to bound. michael@0: */ michael@0: get _urlQuery() [ michael@0: Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY, michael@0: { michael@0: query_type: QUERYTYPE_AUTOFILL_URL, michael@0: searchString: this._autofillUrlSearchString, michael@0: matchBehavior: MATCH_BEGINNING_CASE_SENSITIVE, michael@0: searchBehavior: Ci.mozIPlacesAutoComplete.BEHAVIOR_URL michael@0: } michael@0: ], michael@0: michael@0: /** michael@0: * Notifies the listener about results. michael@0: * michael@0: * @param searchOngoing michael@0: * Indicates whether the search is ongoing. michael@0: */ michael@0: notifyResults: function (searchOngoing) { michael@0: let result = this._result; michael@0: let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH"; michael@0: if (searchOngoing) { michael@0: resultCode += "_ONGOING"; michael@0: } michael@0: result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]); michael@0: this._listener.onSearchResult(this._autocompleteSearch, result); michael@0: }, michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// UnifiedComplete class michael@0: //// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete michael@0: michael@0: function UnifiedComplete() { michael@0: Services.obs.addObserver(this, TOPIC_SHUTDOWN, true); michael@0: } michael@0: michael@0: UnifiedComplete.prototype = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIObserver michael@0: michael@0: observe: function (subject, topic, data) { michael@0: if (topic === TOPIC_SHUTDOWN) { michael@0: this.ensureShutdown(); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Database handling michael@0: michael@0: /** michael@0: * Promise resolved when the database initialization has completed, or null michael@0: * if it has never been requested. michael@0: */ michael@0: _promiseDatabase: null, michael@0: michael@0: /** michael@0: * Gets a Sqlite database handle. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves to the Sqlite database handle (according to Sqlite.jsm). michael@0: * @rejects javascript exception. michael@0: */ michael@0: getDatabaseHandle: function () { michael@0: if (Prefs.enabled && !this._promiseDatabase) { michael@0: this._promiseDatabase = Task.spawn(function* () { michael@0: let conn = yield Sqlite.cloneStorageConnection({ michael@0: connection: PlacesUtils.history.DBConnection, michael@0: readOnly: true michael@0: }); michael@0: michael@0: // Autocomplete often fallbacks to a table scan due to lack of text michael@0: // indices. A larger cache helps reducing IO and improving performance. michael@0: // The value used here is larger than the default Storage value defined michael@0: // as MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp. michael@0: yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB michael@0: michael@0: yield SwitchToTabStorage.initDatabase(conn); michael@0: michael@0: return conn; michael@0: }.bind(this)).then(null, Cu.reportError); michael@0: } michael@0: return this._promiseDatabase; michael@0: }, michael@0: michael@0: /** michael@0: * Used to stop running queries and close the database handle. michael@0: */ michael@0: ensureShutdown: function () { michael@0: if (this._promiseDatabase) { michael@0: Task.spawn(function* () { michael@0: let conn = yield this.getDatabaseHandle(); michael@0: SwitchToTabStorage.shutdown(); michael@0: yield conn.close() michael@0: }.bind(this)).then(null, Cu.reportError); michael@0: this._promiseDatabase = null; michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// mozIPlacesAutoComplete michael@0: michael@0: registerOpenPage: function PAC_registerOpenPage(uri) { michael@0: SwitchToTabStorage.add(uri); michael@0: }, michael@0: michael@0: unregisterOpenPage: function PAC_unregisterOpenPage(uri) { michael@0: SwitchToTabStorage.delete(uri); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteSearch michael@0: michael@0: startSearch: function (searchString, searchParam, previousResult, listener) { michael@0: // Stop the search in case the controller has not taken care of it. michael@0: if (this._currentSearch) { michael@0: this.stopSearch(); michael@0: } michael@0: michael@0: // Note: We don't use previousResult to make sure ordering of results are michael@0: // consistent. See bug 412730 for more details. michael@0: michael@0: this._currentSearch = new Search(searchString, searchParam, listener, michael@0: this, this); michael@0: michael@0: // If we are not enabled, we need to return now. Notice we need an empty michael@0: // result regardless, so we still create the Search object. michael@0: if (!Prefs.enabled) { michael@0: this.finishSearch(true); michael@0: return; michael@0: } michael@0: michael@0: let search = this._currentSearch; michael@0: this.getDatabaseHandle().then(conn => search.execute(conn)) michael@0: .then(() => { michael@0: if (search == this._currentSearch) { michael@0: this.finishSearch(true); michael@0: } michael@0: }, Cu.reportError); michael@0: }, michael@0: michael@0: stopSearch: function () { michael@0: if (this._currentSearch) { michael@0: this._currentSearch.cancel(); michael@0: } michael@0: this.finishSearch(); michael@0: }, michael@0: michael@0: /** michael@0: * Properly cleans up when searching is completed. michael@0: * michael@0: * @param notify [optional] michael@0: * Indicates if we should notify the AutoComplete listener about our michael@0: * results or not. michael@0: */ michael@0: finishSearch: function (notify=false) { michael@0: // Notify about results if we are supposed to. michael@0: if (notify) { michael@0: this._currentSearch.notifyResults(false); michael@0: } michael@0: michael@0: // Clear our state michael@0: TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT); michael@0: delete this._currentSearch; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteSimpleResultListener michael@0: michael@0: onValueRemoved: function (result, spec, removeFromDB) { michael@0: if (removeFromDB) { michael@0: PlacesUtils.history.removePage(NetUtil.newURI(spec)); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIAutoCompleteSearchDescriptor michael@0: michael@0: get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsISupports michael@0: michael@0: classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"), michael@0: michael@0: _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete), michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIAutoCompleteSearch, michael@0: Ci.nsIAutoCompleteSimpleResultListener, michael@0: Ci.mozIPlacesAutoComplete, michael@0: Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference michael@0: ]) michael@0: }; michael@0: michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]);