Bug 720258 - Inline autocomplete should only autocomplete URLs you've typed.

r=dietrich
This commit is contained in:
Marco Bonardo
2012-02-17 14:24:58 +01:00
parent 7d67d3bd1e
commit 4345da6dbd
13 changed files with 285 additions and 90 deletions

View File

@@ -282,6 +282,7 @@ pref("browser.urlbar.doubleClickSelectsAll", true);
pref("browser.urlbar.doubleClickSelectsAll", false);
#endif
pref("browser.urlbar.autoFill", false);
pref("browser.urlbar.autoFill.typed", true);
// 0: Match anywhere (e.g., middle of words)
// 1: Match on word boundaries and then try matching anywhere
// 2: Match only on word boundaries (e.g., after / or .)

View File

@@ -744,6 +744,13 @@ Database::InitSchema(bool* aDatabaseMigrated)
// Firefox 12 uses schema version 17.
if (currentSchemaVersion < 18) {
rv = MigrateV18Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 13 uses schema version 18.
// Schema Upgrades must add migration code here.
rv = UpdateBookmarkRootTitles();
@@ -790,8 +797,6 @@ Database::InitSchema(bool* aDatabaseMigrated)
// moz_hosts.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HOSTS);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HOSTS_FRECENCYHOST);
NS_ENSURE_SUCCESS(rv, rv);
// moz_bookmarks.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_BOOKMARKS);
@@ -961,7 +966,9 @@ Database::InitTempTriggers()
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERDELETE_TRIGGER);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_TRIGGER);
rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_FRECENCY_TRIGGER);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_TYPED_TRIGGER);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
@@ -1675,8 +1682,6 @@ Database::MigrateV17Up()
// Add the moz_hosts table so we can get hostnames for URL autocomplete.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HOSTS);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HOSTS_FRECENCYHOST);
NS_ENSURE_SUCCESS(rv, rv);
}
// Fill the moz_hosts table with all the domains in moz_places.
@@ -1700,6 +1705,50 @@ Database::MigrateV17Up()
return NS_OK;
}
nsresult
Database::MigrateV18Up()
{
MOZ_ASSERT(NS_IsMainThread());
// moz_hosts should distinguish on typed entries.
// Check if the profile already has a typed column.
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
"SELECT typed FROM moz_hosts"
), getter_AddRefs(stmt));
if (NS_FAILED(rv)) {
rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"ALTER TABLE moz_hosts ADD COLUMN typed NOT NULL DEFAULT 0"
));
NS_ENSURE_SUCCESS(rv, rv);
}
// With the addition of the typed column the covering index loses its
// advantages. On the other side querying on host and (optionally) typed
// largely restricts the number of results, making scans decently fast.
rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"DROP INDEX IF EXISTS moz_hosts_frecencyhostindex"
));
NS_ENSURE_SUCCESS(rv, rv);
// Update typed data.
nsCOMPtr<mozIStorageAsyncStatement> updateTypedStmt;
rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
"UPDATE moz_hosts SET typed = 1 WHERE host IN ( "
"SELECT fixup_url(get_unreversed_host(rev_host)) "
"FROM moz_places WHERE typed = 1 "
") "
), getter_AddRefs(updateTypedStmt));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStoragePendingStatement> ps;
rv = updateTypedStmt->ExecuteAsync(nsnull, getter_AddRefs(ps));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
void
Database::Shutdown()
{

View File

@@ -47,7 +47,7 @@
// This is the schema version. Update it at any schema change and add a
// corresponding migrateVxx method below.
#define DATABASE_SCHEMA_VERSION 17
#define DATABASE_SCHEMA_VERSION 18
// Fired after Places inited.
#define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
@@ -300,6 +300,7 @@ protected:
nsresult MigrateV15Up();
nsresult MigrateV16Up();
nsresult MigrateV17Up();
nsresult MigrateV18Up();
nsresult UpdateBookmarkRootTitles();
nsresult CheckAndUpdateGUIDs();

View File

@@ -102,7 +102,11 @@ const kQueryTypeFiltered = 1;
const kTitleTagsSeparator = " \u2013 ";
const kBrowserUrlbarBranch = "browser.urlbar.";
const kBrowserUrlbarAutofillPref = "browser.urlbar.autoFill";
// Toggle autoFill.
const kBrowserUrlbarAutofillPref = "autoFill";
// Whether to search only typed entries.
const kBrowserUrlbarAutofillTypedPref = "autoFill.typed";
////////////////////////////////////////////////////////////////////////////////
//// Globals
@@ -840,8 +844,6 @@ nsPlacesAutoComplete.prototype = {
*/
_loadPrefs: function PAC_loadPrefs(aRegisterObserver)
{
let self = this;
this._enabled = safePrefGetter(this._prefs, "autocomplete.enabled", true);
this._matchBehavior = safePrefGetter(this._prefs,
"matchBehavior",
@@ -1287,8 +1289,7 @@ nsPlacesAutoComplete.prototype = {
function urlInlineComplete()
{
this._loadPrefs(true);
// register observers
Services.obs.addObserver(this, kTopicShutdown, false);
Services.obs.addObserver(this, kTopicShutdown, true);
}
urlInlineComplete.prototype = {
@@ -1301,10 +1302,8 @@ urlInlineComplete.prototype = {
get _db()
{
if (!this.__db && this._autofill) {
this.__db = Cc["@mozilla.org/browser/nav-history-service;1"].
getService(Ci.nsPIPlacesDatabase).
DBConnection.
clone(true);
this.__db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).
DBConnection.clone(true);
}
return this.__db;
},
@@ -1317,9 +1316,11 @@ urlInlineComplete.prototype = {
// Add a trailing slash at the end of the hostname, since we always
// want to complete up to and including a URL separator.
this.__syncQuery = this._db.createStatement(
"SELECT host || '/' "
"/* do not warn (bug no): could index on (typed,frecency) but not worth it */ "
+ "SELECT host || '/' "
+ "FROM moz_hosts "
+ "WHERE host BETWEEN :search_string AND :search_string || X'FFFF' "
+ (this._autofillTyped ? "AND typed = 1 " : "")
+ "ORDER BY frecency DESC "
+ "LIMIT 1"
);
@@ -1333,9 +1334,11 @@ urlInlineComplete.prototype = {
{
if (!this.__asyncQuery) {
this.__asyncQuery = this._db.createAsyncStatement(
"SELECT h.url "
"/* do not warn (bug no): can't use an index */ "
+ "SELECT h.url "
+ "FROM moz_places h "
+ "WHERE h.frecency <> 0 "
+ (this._autofillTyped ? "AND h.typed = 1 " : "")
+ "AND AUTOCOMPLETE_MATCH(:searchString, h.url, "
+ "h.title, '', "
+ "h.visit_count, h.typed, 0, 0, "
@@ -1463,11 +1466,15 @@ urlInlineComplete.prototype = {
*/
_loadPrefs: function UIC_loadPrefs(aRegisterObserver)
{
this._autofill = safePrefGetter(Services.prefs,
let prefBranch = Services.prefs.getBranch(kBrowserUrlbarBranch);
this._autofill = safePrefGetter(prefBranch,
kBrowserUrlbarAutofillPref,
true);
this._autofillTyped = safePrefGetter(prefBranch,
kBrowserUrlbarAutofillTypedPref,
true);
if (aRegisterObserver) {
Services.prefs.addObserver(kBrowserUrlbarAutofillPref, this, true);
Services.prefs.addObserver(kBrowserUrlbarBranch, this, true);
}
},
@@ -1511,27 +1518,31 @@ urlInlineComplete.prototype = {
//////////////////////////////////////////////////////////////////////////////
//// nsIObserver
observe: function PAC_observe(aSubject, aTopic, aData)
observe: function UIC_observe(aSubject, aTopic, aData)
{
if (aTopic == kTopicShutdown) {
Services.obs.removeObserver(this, kTopicShutdown);
this._closeDatabase();
}
else if (aTopic == kPrefChanged) {
else if (aTopic == kPrefChanged &&
(aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillPref ||
aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillTypedPref)) {
let previousAutofillTyped = this._autofillTyped;
this._loadPrefs();
if (!this._autofill) {
this.stopSearch();
this._closeDatabase();
}
else if (this._autofillTyped != previousAutofillTyped) {
// Invalidate the statements to update them for the new typed status.
this._invalidateStatements();
}
}
},
/**
*
* Finalize and close the database safely
*
**/
_closeDatabase: function UIC_closeDatabase()
* Finalizes and invalidates cached statements.
*/
_invalidateStatements: function UIC_invalidateStatements()
{
// Finalize the statements that we have used.
let stmts = [
@@ -1546,6 +1557,14 @@ urlInlineComplete.prototype = {
this[stmts[i]] = null;
}
}
},
/**
* Closes the database.
*/
_closeDatabase: function UIC_closeDatabase()
{
this._invalidateStatements();
if (this.__db) {
this._db.asyncClose();
this.__db = null;

View File

@@ -136,15 +136,6 @@
"placeattributeindex", "moz_annos", "place_id, anno_attribute_id", "UNIQUE" \
)
/**
* moz_hosts
*/
#define CREATE_IDX_MOZ_HOSTS_FRECENCYHOST \
CREATE_PLACES_IDX( \
"frecencyhostindex", "moz_hosts", "frecency, host", "" \
)
/**
* moz_items_annos
*/

View File

@@ -167,6 +167,7 @@
" id INTEGER PRIMARY KEY" \
", host TEXT NOT NULL UNIQUE" \
", frecency INTEGER" \
", typed INTEGER NOT NULL DEFAULT 0" \
")" \
)

View File

@@ -87,11 +87,12 @@
"AFTER INSERT ON moz_places FOR EACH ROW " \
"WHEN LENGTH(NEW.rev_host) > 1 " \
"BEGIN " \
"INSERT OR REPLACE INTO moz_hosts (id, host, frecency) " \
"INSERT OR REPLACE INTO moz_hosts (id, host, frecency, typed) " \
"VALUES (" \
"(SELECT id FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), " \
"fixup_url(get_unreversed_host(NEW.rev_host)), " \
"MAX((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), NEW.frecency) " \
"MAX((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), NEW.frecency), " \
"MAX(IFNULL((SELECT typed FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), 0), NEW.typed) " \
"); " \
"END" \
)
@@ -110,7 +111,7 @@
// frecency changes by a meaningful percentage. This is because the frecency
// decay algorithm requires to update all the frecencies at once, causing a
// too high overhead, while leaving the ordering unchanged.
#define CREATE_PLACES_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \
#define CREATE_PLACES_AFTERUPDATE_FRECENCY_TRIGGER NS_LITERAL_CSTRING( \
"CREATE TEMP TRIGGER moz_places_afterupdate_frecency_trigger " \
"AFTER UPDATE OF frecency ON moz_places FOR EACH ROW " \
"WHEN NEW.frecency >= 0 " \
@@ -127,6 +128,17 @@
"END" \
)
#define CREATE_PLACES_AFTERUPDATE_TYPED_TRIGGER NS_LITERAL_CSTRING( \
"CREATE TEMP TRIGGER moz_places_afterupdate_typed_trigger " \
"AFTER UPDATE OF typed ON moz_places FOR EACH ROW " \
"WHEN NEW.typed = 1 " \
"BEGIN " \
"UPDATE moz_hosts " \
"SET typed = 1 " \
"WHERE host = fixup_url(get_unreversed_host(NEW.rev_host)); " \
"END" \
)
/**
* This trigger removes a row from moz_openpages_temp when open_count reaches 0.
*

View File

@@ -35,7 +35,7 @@
*
* ***** END LICENSE BLOCK ***** */
const CURRENT_SCHEMA_VERSION = 17;
const CURRENT_SCHEMA_VERSION = 18;
const NS_APP_USER_PROFILE_50_DIR = "ProfD";
const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";

View File

@@ -21,29 +21,6 @@ XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
"@mozilla.org/browser/history;1",
"mozIAsyncHistory");
function VisitInfo(aTransitionType, aVisitTime)
{
this.transitionType =
aTransitionType === undefined ? TRANSITION_LINK : aTransitionType;
this.visitDate = aVisitTime || Date.now() * 1000;
}
function addVisits(aUrls)
{
let places = [];
aUrls.forEach(function(url) {
places.push({
uri: url.url,
title: "test for " + url.url,
visits: [
new VisitInfo(url.transition),
],
});
});
gHistory.updatePlaces(places);
}
/**
* @param aSearches
* Array of AutoCompleteSearch names.
@@ -168,8 +145,10 @@ function ensure_results(aSearchString, aExpectedValue) {
function run_test() {
Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
do_register_cleanup(function () {
Services.prefs.clearUserPref("browser.urlbar.autoFill");
Services.prefs.clearUserPref("browser.urlbar.autoFill.typed");
});
gAutoCompleteTests.forEach(function (testData) {
@@ -214,3 +193,26 @@ function addBookmark(aBookmarkObj) {
PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aBookmarkObj.keyword);
}
}
function VisitInfo(aTransitionType, aVisitTime)
{
this.transitionType =
aTransitionType === undefined ? TRANSITION_LINK : aTransitionType;
this.visitDate = aVisitTime || Date.now() * 1000;
}
function addVisits(aUrls)
{
let places = [];
aUrls.forEach(function(url) {
places.push({
uri: url.url,
title: "test for " + url.url,
visits: [
new VisitInfo(url.transition),
],
});
});
gHistory.updatePlaces(places);
}

View File

@@ -0,0 +1,80 @@
/* 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/. */
// First do searches with typed behavior forced to false, so later tests will
// ensure autocomplete is able to dinamically switch behavior.
add_autocomplete_test([
"Searching for domain should autoFill it",
"moz",
"mozilla.org/",
function () {
Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
addVisits([ { url: NetUtil.newURI("http://mozilla.org/link/")
, transition: TRANSITION_LINK }
]);
}
]);
add_autocomplete_test([
"Searching for url should autoFill it",
"mozilla.org/li",
"mozilla.org/link/",
function () {
Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
addVisits([ { url: NetUtil.newURI("http://mozilla.org/link/")
, transition: TRANSITION_LINK }
]);
}
]);
// Now do searches with typed behavior forced to true.
add_autocomplete_test([
"Searching for non-typed domain should not autoFill it",
"moz",
"moz",
function () {
Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
addVisits([ { url: NetUtil.newURI("http://mozilla.org/link/")
, transition: TRANSITION_LINK }
]);
}
]);
add_autocomplete_test([
"Searching for typed domain should autoFill it",
"moz",
"mozilla.org/",
function () {
Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
addVisits([ { url: NetUtil.newURI("http://mozilla.org/typed/")
, transition: TRANSITION_TYPED }
]);
}
]);
add_autocomplete_test([
"Searching for non-typed url should not autoFill it",
"mozilla.org/li",
"mozilla.org/li",
function () {
Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
addVisits([ { url: NetUtil.newURI("http://mozilla.org/link/")
, transition: TRANSITION_LINK }
]);
}
]);
add_autocomplete_test([
"Searching for typed url should autoFill it",
"mozilla.org/li",
"mozilla.org/link/",
function () {
Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
addVisits([ { url: NetUtil.newURI("http://mozilla.org/link/")
, transition: TRANSITION_TYPED }
]);
}
]);

View File

@@ -5,3 +5,4 @@ tail =
[test_autocomplete_functional.js]
[test_casing.js]
[test_keywords.js]
[test_typed.js]

View File

@@ -282,30 +282,40 @@ function test_moz_hosts()
{
// This will throw if the column does not exist
let stmt = DBConn().createStatement(
"SELECT host, frecency "
"SELECT host, frecency, typed "
+ "FROM moz_hosts "
);
stmt.finalize();
// moz_hosts is populated asynchronously, so query asynchronously to serialize
// to that.
// check the number of entries in moz_hosts equals the number of
// unique rev_host in moz_places
var query = "SELECT ("
+ "SELECT COUNT(host) "
+ "FROM moz_hosts), ("
+ "SELECT COUNT(DISTINCT rev_host) "
+ "FROM moz_places "
+ "WHERE LENGTH(rev_host) > 1)";
stmt = DBConn().createStatement(query);
stmt = DBConn().createAsyncStatement(
"SELECT ("
+ "SELECT COUNT(host) "
+ "FROM moz_hosts), ("
+ "SELECT COUNT(DISTINCT rev_host) "
+ "FROM moz_places "
+ "WHERE LENGTH(rev_host) > 1)"
);
try {
stmt.executeStep();
let mozPlacesCount = stmt.getInt32(0);
let mozHostsCount = stmt.getInt32(1);
do_check_eq(mozPlacesCount, mozHostsCount);
stmt.executeAsync({
handleResult: function (aResultSet) {
let row = aResult.getNextRow();
let mozPlacesCount = row.getResultByIndex(0);
let mozHostsCount = row.getResultByIndex(1);
do_check_eq(mozPlacesCount, mozHostsCount);
},
handleError: function () {},
handleCompletion: function (aReason) {
do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED);
run_next_test();
}
});
}
finally {
stmt.finalize();
run_next_test();
}
}
@@ -328,8 +338,6 @@ function test_final_state()
do_check_true(db.indexExists("moz_places_guid_uniqueindex"));
do_check_true(db.indexExists("moz_favicons_guid_uniqueindex"));
do_check_true(db.indexExists("moz_hosts_frecencyhostindex"));
do_check_eq(db.schemaVersion, CURRENT_SCHEMA_VERSION);
db.close();

View File

@@ -5,6 +5,10 @@
* This file tests the validity of various triggers that add remove hosts from moz_hosts
*/
XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
"@mozilla.org/browser/history;1",
"mozIAsyncHistory");
// add some visits and remove them, add a bookmark,
// change its uri, then remove it, and
// for each change check that moz_hosts has correctly been updated.
@@ -28,10 +32,10 @@ function isHostInMozPlaces(aURI)
return result;
}
function isHostInMozHosts(aURI)
function isHostInMozHosts(aURI, aTyped)
{
let stmt = DBConn().createStatement(
"SELECT host "
"SELECT host, typed "
+ "FROM moz_hosts "
+ "WHERE host = :host"
);
@@ -39,7 +43,10 @@ function isHostInMozHosts(aURI)
stmt.params.host = aURI.host;
while(stmt.executeStep()) {
if (stmt.row.host == aURI.host) {
result = true;
if (aTyped != null)
result = aTyped == stmt.row.typed;
else
result = true;
break;
}
}
@@ -49,12 +56,15 @@ function isHostInMozHosts(aURI)
let urls = [{uri: NetUtil.newURI("http://visit1.mozilla.org"),
expected: "visit1.mozilla.org",
typed: 0
},
{uri: NetUtil.newURI("http://visit2.mozilla.org"),
expected: "visit2.mozilla.org",
typed: 0
},
{uri: NetUtil.newURI("http://www.foo.mozilla.org"),
expected: "foo.mozilla.org",
typed: 1
},
];
@@ -75,16 +85,12 @@ function test_moz_hosts_update()
uri: url.uri,
title: "test for " + url.url,
visits: [
new VisitInfo(),
new VisitInfo(url.typed ? TRANSITION_TYPED : undefined),
],
};
places.push(place);
});
XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
"@mozilla.org/browser/history;1",
"mozIAsyncHistory");
gHistory.updatePlaces(places, {
handleResult: function () {
},
@@ -92,10 +98,11 @@ function test_moz_hosts_update()
do_throw("gHistory.updatePlaces() failed");
},
handleCompletion: function () {
do_check_true(isHostInMozHosts(urls[0].uri));
do_check_true(isHostInMozHosts(urls[1].uri));
do_check_true(isHostInMozHosts(urls[0].uri, urls[0].typed));
do_check_true(isHostInMozHosts(urls[1].uri, urls[1].typed));
// strip the WWW from the url before testing...
do_check_true(isHostInMozHosts(NetUtil.newURI("http://foo.mozilla.org")));
do_check_true(isHostInMozHosts(NetUtil.newURI("http://foo.mozilla.org"),
urls[2].typed));
run_next_test();
}
});
@@ -148,7 +155,29 @@ function test_bookmark_removal()
do_check_false(isHostInMozHosts(newUri));
run_next_test();
});
}
function test_moz_hosts_typed_update()
{
const TEST_URI = NetUtil.newURI("http://typed.mozilla.com");
let places = [{ uri: TEST_URI
, title: "test for " + TEST_URI.spec
, visits: [ new VisitInfo(TRANSITION_LINK)
, new VisitInfo(TRANSITION_TYPED)
]
}];
gHistory.updatePlaces(places, {
handleResult: function () {
},
handleError: function () {
do_throw("gHistory.updatePlaces() failed");
},
handleCompletion: function () {
do_check_true(isHostInMozHosts(TEST_URI, true));
run_next_test();
}
});
}
////////////////////////////////////////////////////////////////////////////////
@@ -159,6 +188,7 @@ function test_bookmark_removal()
test_remove_places,
test_bookmark_changes,
test_bookmark_removal,
test_moz_hosts_typed_update,
].forEach(add_test);
function run_test()