Bug 959582 - Refactor the search URL provider for the location bar. r=mak

--HG--
rename : toolkit/components/places/PriorityUrlProvider.jsm => toolkit/components/places/PlacesSearchAutocompleteProvider.jsm
rename : toolkit/components/places/tests/unit/test_priorityUrlProvider.js => toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js
This commit is contained in:
Paolo Amadini 2014-08-01 16:08:48 +01:00
parent c0421143cf
commit 8e8b017fdb
6 changed files with 193 additions and 160 deletions

View File

@ -0,0 +1,160 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Provides functions to handle search engine URLs in the browser history.
*/
"use strict";
this.EXPORTED_SYMBOLS = [ "PlacesSearchAutocompleteProvider" ];
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
const SearchAutocompleteProviderInternal = {
/**
* Array of objects in the format returned by findMatchByToken.
*/
priorityMatches: null,
initialize: function () {
return new Promise((resolve, reject) => {
Services.search.init(status => {
if (!Components.isSuccessCode(status)) {
reject(new Error("Unable to initialize search service."));
}
try {
// The initial loading of the search engines must succeed.
this._refresh();
Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true);
this.initialized = true;
resolve();
} catch (ex) {
reject(ex);
}
});
});
},
initialized: false,
observe: function (subject, topic, data) {
switch (data) {
case "engine-added":
case "engine-changed":
case "engine-removed":
this._refresh();
}
},
_refresh: function () {
this.priorityMatches = [];
// The search engines will always be processed in the order returned by the
// search service, which can be defined by the user.
Services.search.getVisibleEngines().forEach(e => this._addEngine(e));
},
_addEngine: function (engine) {
let token = engine.getResultDomain();
if (!token) {
return;
}
this.priorityMatches.push({
token: token,
// The searchForm property returns a simple URL for the search engine, but
// we may need an URL which includes an affiliate code (bug 990799).
url: engine.searchForm,
engineName: engine.name,
iconUrl: engine.iconURI ? engine.iconURI.spec : null,
});
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
}
let gInitializationPromise = null;
this.PlacesSearchAutocompleteProvider = Object.freeze({
/**
* Starts initializing the component and returns a promise that is resolved or
* rejected when initialization finished. The same promise is returned if
* this function is called multiple times.
*/
ensureInitialized: function () {
if (!gInitializationPromise) {
gInitializationPromise = SearchAutocompleteProviderInternal.initialize();
}
return gInitializationPromise;
},
/**
* Matches a given string to an item that should be included by URL search
* components, like autocomplete in the address bar.
*
* @param searchToken
* String containing the first part of the matching domain name.
*
* @return An object with the following properties, or undefined if the token
* does not match any relevant URL:
* {
* token: The full string used to match the search term to the URL.
* url: The URL to navigate to if the match is selected.
* engineName: The display name of the search engine.
* iconUrl: Icon associated to the match, or null if not available.
* }
*/
findMatchByToken: Task.async(function* (searchToken) {
yield this.ensureInitialized();
// Match at the beginning for now. In the future, an "options" argument may
// allow the matching behavior to be tuned.
return SearchAutocompleteProviderInternal.priorityMatches
.find(m => m.token.startsWith(searchToken));
}),
/**
* Synchronously determines if the provided URL represents results from a
* search engine, and provides details about the match.
*
* @param url
* String containing the URL to parse.
*
* @return An object with the following properties, or null if the URL does
* not represent a search result:
* {
* engineName: The display name of the search engine.
* terms: The originally sought terms extracted from the URI.
* }
*
* @remarks The asynchronous ensureInitialized function must be called before
* this synchronous method can be used.
*
* @note This API function needs to be synchronous because it is called inside
* a row processing callback of Sqlite.jsm, in UnifiedComplete.js.
*/
parseSubmissionURL: function (url) {
if (!SearchAutocompleteProviderInternal.initialized) {
throw new Error("The component has not been initialized.");
}
let parseUrlResult = Services.search.parseSubmissionURL(url);
return parseUrlResult.engine && {
engineName: parseUrlResult.engine.name,
terms: parseUrlResult.terms,
};
},
});

View File

@ -1,142 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = [ "PriorityUrlProvider" ];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
/**
* Provides search engines matches to the PriorityUrlProvider through the
* search engines definitions handled by the Search Service.
*/
const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
let SearchEnginesProvider = {
init: function () {
this._engines = new Map();
let deferred = Promise.defer();
Services.search.init(rv => {
if (Components.isSuccessCode(rv)) {
Services.search.getVisibleEngines().forEach(this._addEngine, this);
deferred.resolve();
} else {
deferred.reject(new Error("Unable to initialize search service."));
}
});
Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, true);
return deferred.promise;
},
observe: function (engine, topic, verb) {
let engine = engine.QueryInterface(Ci.nsISearchEngine);
switch (verb) {
case "engine-added":
this._addEngine(engine);
break;
case "engine-changed":
if (engine.hidden) {
this._removeEngine(engine);
} else {
this._addEngine(engine);
}
break;
case "engine-removed":
this._removeEngine(engine);
break;
}
},
_addEngine: function (engine) {
if (this._engines.has(engine.name)) {
return;
}
let token = engine.getResultDomain();
if (!token) {
return;
}
let match = { token: token,
// TODO (bug 990799): searchForm should provide an usable
// url with affiliate code, if available.
url: engine.searchForm,
title: engine.name,
iconUrl: engine.iconURI ? engine.iconURI.spec : null,
reason: "search" }
this._engines.set(engine.name, match);
PriorityUrlProvider.addMatch(match);
},
_removeEngine: function (engine) {
if (!this._engines.has(engine.name)) {
return;
}
this._engines.delete(engine.name);
PriorityUrlProvider.removeMatchByToken(engine.getResultDomain());
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference])
}
/**
* The PriorityUrlProvider allows to match a given string to a list of
* urls that should have priority in url search components, like autocomplete.
* Each returned match is an object with the following properties:
* - token: string used to match the search term to the url
* - url: url string represented by the match
* - title: title describing the match, or an empty string if not available
* - iconUrl: url of the icon associated to the match, or null if not available
* - reason: a string describing the origin of the match, for example if it
* represents a search engine, it will be "search".
*/
let matches = new Map();
let initialized = false;
function promiseInitialized() {
if (initialized) {
return Promise.resolve();
}
return Task.spawn(function* () {
try {
yield SearchEnginesProvider.init();
} catch (ex) {
Cu.reportError(ex);
}
initialized = true;
});
}
this.PriorityUrlProvider = Object.freeze({
addMatch: function (match) {
matches.set(match.token, match);
},
removeMatchByToken: function (token) {
matches.delete(token);
},
getMatch: function (searchToken) {
return Task.spawn(function* () {
yield promiseInitialized();
for (let [token, match] of matches.entries()) {
// Match at the beginning for now. In future an aOptions argument may
// allow to control the matching behavior.
if (token.startsWith(searchToken)) {
return match;
}
}
return null;
}.bind(this));
}
});

View File

@ -264,8 +264,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PriorityUrlProvider",
"resource://gre/modules/PriorityUrlProvider.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
"resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
"@mozilla.org/intl/texttosuburi;1",
@ -703,14 +703,17 @@ Search.prototype = {
_matchPriorityUrl: function* () {
if (!Prefs.autofillPriority)
return;
let priorityMatch = yield PriorityUrlProvider.getMatch(this._searchString);
// Handle priority matches for search engine domains.
let priorityMatch =
yield PlacesSearchAutocompleteProvider.findMatchByToken(this._searchString);
if (priorityMatch) {
this._result.setDefaultIndex(0);
this._addFrecencyMatch({
value: priorityMatch.token,
comment: priorityMatch.title,
comment: priorityMatch.engineName,
icon: priorityMatch.iconUrl,
style: "priority-" + priorityMatch.reason,
style: "priority-search",
finalCompleteValue: priorityMatch.url,
frecency: FRECENCY_PRIORITY_DEFAULT
});

View File

@ -67,8 +67,8 @@ if CONFIG['MOZ_PLACES']:
'ColorConversion.js',
'PlacesBackups.jsm',
'PlacesDBUtils.jsm',
'PlacesSearchAutocompleteProvider.jsm',
'PlacesTransactions.jsm',
'PriorityUrlProvider.jsm'
]
EXTRA_PP_JS_MODULES += [

View File

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Cu.import("resource://gre/modules/PriorityUrlProvider.jsm");
Cu.import("resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
function run_test() {
run_next_test();
@ -11,15 +11,14 @@ function run_test() {
add_task(function* search_engine_match() {
let engine = yield promiseDefaultSearchEngine();
let token = engine.getResultDomain();
let match = yield PriorityUrlProvider.getMatch(token.substr(0, 1));
let match = yield PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1));
do_check_eq(match.url, engine.searchForm);
do_check_eq(match.title, engine.name);
do_check_eq(match.engineName, engine.name);
do_check_eq(match.iconUrl, engine.iconURI ? engine.iconURI.spec : null);
do_check_eq(match.reason, "search");
});
add_task(function* no_match() {
do_check_eq(null, yield PriorityUrlProvider.getMatch("test"));
do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("test"));
});
add_task(function* hide_search_engine_nomatch() {
@ -29,20 +28,19 @@ add_task(function* hide_search_engine_nomatch() {
Services.search.removeEngine(engine);
yield promiseTopic;
do_check_true(engine.hidden);
do_check_eq(null, yield PriorityUrlProvider.getMatch(token.substr(0, 1)));
do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1)));
});
add_task(function* add_search_engine_match() {
let promiseTopic = promiseSearchTopic("engine-added");
do_check_eq(null, yield PriorityUrlProvider.getMatch("bacon"));
do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon"));
Services.search.addEngineWithDetails("bacon", "", "bacon", "Search Bacon",
"GET", "http://www.bacon.moz/?search={searchTerms}");
yield promiseSearchTopic;
let match = yield PriorityUrlProvider.getMatch("bacon");
let match = yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon");
do_check_eq(match.url, "http://www.bacon.moz");
do_check_eq(match.title, "bacon");
do_check_eq(match.engineName, "bacon");
do_check_eq(match.iconUrl, null);
do_check_eq(match.reason, "search");
});
add_task(function* remove_search_engine_nomatch() {
@ -50,7 +48,21 @@ add_task(function* remove_search_engine_nomatch() {
let promiseTopic = promiseSearchTopic("engine-removed");
Services.search.removeEngine(engine);
yield promiseTopic;
do_check_eq(null, yield PriorityUrlProvider.getMatch("bacon"));
do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon"));
});
add_task(function* test_parseSubmissionURL_basic() {
// Most of the logic of parseSubmissionURL is tested in the search service
// itself, thus we only do a sanity check of the wrapper here.
let engine = yield promiseDefaultSearchEngine();
let submissionURL = engine.getSubmission("terms").uri.spec;
let result = PlacesSearchAutocompleteProvider.parseSubmissionURL(submissionURL);
do_check_eq(result.engineName, engine.name);
do_check_eq(result.terms, "terms");
let result = PlacesSearchAutocompleteProvider.parseSubmissionURL("http://example.org/");
do_check_eq(result, null);
});
function promiseDefaultSearchEngine() {

View File

@ -115,6 +115,7 @@ skip-if = true
[test_pageGuid_bookmarkGuid.js]
[test_frecency_observers.js]
[test_placeURIs.js]
[test_PlacesSearchAutocompleteProvider.js]
[test_PlacesUtils_asyncGetBookmarkIds.js]
[test_PlacesUtils_lazyobservers.js]
[test_placesTxn.js]
@ -125,7 +126,6 @@ skip-if = os == "android"
# Bug 676989: test hangs consistently on Android
skip-if = os == "android"
[test_preventive_maintenance_runTasks.js]
[test_priorityUrlProvider.js]
[test_promiseBookmarksTree.js]
[test_removeVisitsByTimeframe.js]
# Bug 676989: test hangs consistently on Android