Bug 393678 - location bar autocomplete should take word boundaries in account. r=dietrich, b-ff3=beltzner

This commit is contained in:
edward.lee@engineering.uiuc.edu 2008-03-18 19:11:05 -07:00
Родитель 47f9534c38
Коммит 2658d703d0
6 изменённых файлов: 390 добавлений и 7 удалений

Просмотреть файл

@ -213,6 +213,7 @@ pref("browser.urlbar.doubleClickSelectsAll", false);
#endif
pref("browser.urlbar.autoFill", false);
pref("browser.urlbar.matchOnlyTyped", false);
pref("browser.urlbar.matchOnWordBoundary", true);
pref("browser.urlbar.filter.javascript", true);
// the maximum number of results to show in autocomplete when doing richResults

Просмотреть файл

@ -113,6 +113,7 @@
#define PREF_BROWSER_HISTORY_EXPIRE_DAYS_MAX "history_expire_days"
#define PREF_BROWSER_HISTORY_EXPIRE_SITES "history_expire_sites"
#define PREF_AUTOCOMPLETE_ONLY_TYPED "urlbar.matchOnlyTyped"
#define PREF_AUTOCOMPLETE_ON_WORD_BOUNDARY "urlbar.matchOnWordBoundary"
#define PREF_AUTOCOMPLETE_FILTER_JAVASCRIPT "urlbar.filter.javascript"
#define PREF_AUTOCOMPLETE_ENABLED "urlbar.autocomplete.enabled"
#define PREF_AUTOCOMPLETE_MAX_RICH_RESULTS "urlbar.maxRichResults"
@ -329,6 +330,7 @@ nsNavHistory::nsNavHistory() : mBatchLevel(0),
mExpireNowTimer(nsnull),
mExpire(this),
mAutoCompleteOnlyTyped(PR_FALSE),
mAutoCompleteOnWordBoundary(PR_TRUE),
mAutoCompleteMaxResults(25),
mAutoCompleteSearchChunkSize(100),
mAutoCompleteSearchTimeout(100),
@ -472,6 +474,7 @@ nsNavHistory::Init()
nsCOMPtr<nsIPrefBranch2> pbi = do_QueryInterface(mPrefBranch);
if (pbi) {
pbi->AddObserver(PREF_AUTOCOMPLETE_ONLY_TYPED, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_ON_WORD_BOUNDARY, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_FILTER_JAVASCRIPT, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_MAX_RICH_RESULTS, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_SEARCH_CHUNK_SIZE, this, PR_FALSE);
@ -1798,6 +1801,8 @@ nsNavHistory::LoadPrefs(PRBool aInitializing)
PRBool oldCompleteOnlyTyped = mAutoCompleteOnlyTyped;
mPrefBranch->GetBoolPref(PREF_AUTOCOMPLETE_ONLY_TYPED,
&mAutoCompleteOnlyTyped);
mPrefBranch->GetBoolPref(PREF_AUTOCOMPLETE_ON_WORD_BOUNDARY,
&mAutoCompleteOnWordBoundary);
mPrefBranch->GetBoolPref(PREF_AUTOCOMPLETE_FILTER_JAVASCRIPT,
&mAutoCompleteFilterJavascript);
mPrefBranch->GetIntPref(PREF_AUTOCOMPLETE_MAX_RICH_RESULTS,

Просмотреть файл

@ -656,6 +656,7 @@ protected:
nsresult InitAutoComplete();
nsresult CreateAutoCompleteQueries();
PRBool mAutoCompleteOnlyTyped;
PRBool mAutoCompleteOnWordBoundary;
PRBool mAutoCompleteFilterJavascript;
PRInt32 mAutoCompleteMaxResults;
PRInt32 mAutoCompleteSearchChunkSize;

Просмотреть файл

@ -91,6 +91,93 @@ StartsWithJS(const nsAString &aString)
return StringBeginsWith(aString, NS_LITERAL_STRING("javascript:"));
}
/**
* Returns true if the unicode character is a word boundary. I.e., anything
* that *isn't* used to build up a word from a string of characters. We are
* conservative here because anything that we don't list will be treated as
* word boundary. This means searching for that not-actually-a-word-boundary
* character can still be matched in the middle of a word.
*/
inline PRBool
IsWordBoundary(const PRUnichar &aChar)
{
// Lower-case alphabetic, so upper-case matches CamelCase. Because
// upper-case is treated as a word boundary, matches will also happen
// _after_ an upper-case character.
return !(PRUnichar('a') <= aChar && aChar <= PRUnichar('z'));
}
/**
* Returns true if the token matches the target on a word boundary
*
* @param aToken
* Token to search for that must match on a word boundary
* @param aTarget
* Target string to search against
*/
PRBool
FindOnBoundary(const nsAString &aToken, const nsAString &aTarget)
{
// Define a const instance of this class so it is created once
const nsCaseInsensitiveStringComparator caseInsensitiveCompare;
// Can't match anything if there's nothing to match
if (aTarget.IsEmpty())
return PR_FALSE;
nsAString::const_iterator tokenStart, tokenEnd;
aToken.BeginReading(tokenStart);
aToken.EndReading(tokenEnd);
nsAString::const_iterator targetStart, targetEnd;
aTarget.BeginReading(targetStart);
aTarget.EndReading(targetEnd);
// Go straight into checking the token at the beginning of the target because
// the beginning is considered a word boundary
do {
// We're on a word boundary, so prepare to match by copying the iterators
nsAString::const_iterator testToken(tokenStart);
nsAString::const_iterator testTarget(targetStart);
// Keep trying to match the token one by one until it doesn't match
while (!caseInsensitiveCompare(*testToken, *testTarget)) {
// We matched something, so move down one
testToken++;
testTarget++;
// Matched the token! We're done!
if (testToken == tokenEnd)
return PR_TRUE;
// If we ran into the end while matching the token, we won't find it
if (testTarget == targetEnd)
return PR_FALSE;
}
// Unconditionally move past the current position in the target, but if
// we're not currently on a word boundary, eat up as many non-word boundary
// characters as possible -- don't kill characters if we're currently on a
// word boundary so that we can match tokens that start on a word boundary.
if (!IsWordBoundary(*targetStart++))
while (targetStart != targetEnd && !IsWordBoundary(*targetStart))
targetStart++;
// If we hit the end eating up non-boundaries then boundaries, we're done
} while (targetStart != targetEnd);
return PR_FALSE;
}
/**
* A local wrapper to CaseInsensitiveFindInReadable that isn't overloaded
*/
inline PRBool
FindAnywhere(const nsAString &aToken, const nsAString &aTarget)
{
return CaseInsensitiveFindInReadable(aToken, aTarget);
}
// nsNavHistory::InitAutoComplete
nsresult
nsNavHistory::InitAutoComplete()
@ -487,6 +574,10 @@ nsNavHistory::AutoCompleteProcessSearch(mozIStorageStatement* aQuery,
PRBool filterJavascript = mAutoCompleteFilterJavascript &&
!StartsWithJS(mCurrentSearchString);
// Determine what type of search to try matching tokens against targets
PRBool (*tokenMatchesTarget)(const nsAString &, const nsAString &) =
mAutoCompleteOnWordBoundary ? FindOnBoundary : FindAnywhere;
PRBool hasMore = PR_FALSE;
// Determine the result of the search
while (NS_SUCCEEDED(aQuery->ExecuteStep(&hasMore)) && hasMore) {
@ -547,19 +638,19 @@ nsNavHistory::AutoCompleteProcessSearch(mozIStorageStatement* aQuery,
// Check if the current token matches the bookmark
PRBool bookmarkMatch = parentId &&
CaseInsensitiveFindInReadable(*token, entryBookmarkTitle);
(*tokenMatchesTarget)(*token, entryBookmarkTitle);
// If any part of the search string is in the bookmark title, show
// that in the result instead of the page title
useBookmark |= bookmarkMatch;
// If the token is in any of the tags, remember to show tags
PRBool tagsMatch = CaseInsensitiveFindInReadable(*token, entryTags);
PRBool tagsMatch = (*tokenMatchesTarget)(*token, entryTags);
showTags |= tagsMatch;
// True if any of them match; false makes us quit the loop
matchAll = bookmarkMatch || tagsMatch ||
CaseInsensitiveFindInReadable(*token, entryTitle) ||
CaseInsensitiveFindInReadable(*token, entryURL);
(*tokenMatchesTarget)(*token, entryTitle) ||
(*tokenMatchesTarget)(*token, entryURL);
}
// Skip if we don't match all terms in the bookmark, tag, title or url

Просмотреть файл

@ -174,9 +174,9 @@ var tests = [
function() ensure_tag_results([uri5, uri6], "ba chee"),
function() ensure_tag_results([uri5], "ba chee aaa"),
function() ensure_tag_results([uri6], "ba chee bbb"),
function() ensure_tag_results([uri5, uri6], "eese bar"),
function() ensure_tag_results([uri5], "heese bar aaa"),
function() ensure_tag_results([uri6], "ees bar bbb"),
function() ensure_tag_results([uri5, uri6], "cheese bar"),
function() ensure_tag_results([uri5], "cheese bar aaa"),
function() ensure_tag_results([uri6], "chees bar bbb"),
function() ensure_tag_results([uri6], "cheese bar foo"),
function() ensure_tag_results([], "foo bar cheese aaa"),
function() ensure_tag_results([uri6], "foo bar cheese bbb"),

Просмотреть файл

@ -0,0 +1,285 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Places Word Boundary Search Test Code.
*
* The Initial Developer of the Original Code is
* Edward Lee <edward.lee@engineering.uiuc.edu>.
* Portions created by the Initial Developer are Copyright (C) 2008
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/**
* Test bug 393678 to make sure matches against the url, title, tags are only
* made on word boundaries instead of in the middle of words.
*/
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
let current_test = 0;
function AutoCompleteInput(aSearches) {
this.searches = aSearches;
}
AutoCompleteInput.prototype = {
timeout: 10,
textValue: "",
searches: null,
searchParam: "",
popupOpen: false,
minResultsForPopup: 0,
invalidate: function() {},
disableAutoComplete: false,
completeDefaultIndex: false,
get popup() { return this; },
onSearchBegin: function() {},
onSearchComplete: function() {},
setSelectedIndex: function() {},
get searchCount() { return this.searches.length; },
getSearchAt: function(aIndex) this.searches[aIndex],
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput, Ci.nsIAutoCompletePopup])
};
function ensure_results(aSearch, aExpected)
{
let controller = Cc["@mozilla.org/autocomplete/controller;1"].
getService(Ci.nsIAutoCompleteController);
// Make an AutoCompleteInput that uses our searches
// and confirms results on search complete
let input = new AutoCompleteInput(["history"]);
controller.input = input;
let numSearchesStarted = 0;
input.onSearchBegin = function() {
numSearchesStarted++;
do_check_eq(numSearchesStarted, 1);
};
input.onSearchComplete = function() {
do_check_eq(numSearchesStarted, 1);
// Check to see the expected uris and titles match up (in any order)
for (let i = 0; i < controller.matchCount; i++) {
let value = controller.getValueAt(i);
let comment = controller.getCommentAt(i);
print("Looking for an expected result of " + value + ", " + comment + "...");
let j;
for (j = 0; j < aExpected.length; j++) {
let [uri, title] = aExpected[j];
// Skip processed expected results
if (uri == undefined) continue;
// Load the real uri and titles
[uri, title] = [iosvc.newURI(kURIs[uri], null, null).spec, kTitles[title]];
// Got a match on both uri and title?
if (uri == value && title == comment) {
print("Got it at index " + j + "!!");
// Make it undefined so we don't process it again
aExpected[j] = [,];
break;
}
}
// We didn't hit the break, so we must have not found it
if (j == aExpected.length)
do_throw("Didn't find the current result (" + value + ", " + comment + ") in expected: " + aExpected);
}
// Make sure we have the right number of results
do_check_eq(controller.matchCount, aExpected.length);
// If we expect results, make sure we got matches
do_check_eq(controller.searchStatus, aExpected.length ?
Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
// Fetch the next test if we have more
if (++current_test < gTests.length)
run_test();
do_test_finished();
};
print("Searching for.. " + aSearch);
controller.startSearch(aSearch);
}
// Get history services
try {
var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
getService(Ci.nsINavHistoryService);
var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory);
var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
getService(Ci.nsINavBookmarksService);
var tagsvc = Cc["@mozilla.org/browser/tagging-service;1"].
getService(Ci.nsITaggingService);
var iosvc = Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
} catch(ex) {
do_throw("Could not get services\n");
}
// Some date not too long ago
let gDate = new Date(Date.now() - 1000 * 60 * 60) * 1000;
function addPageBook(aURI, aTitle, aBook, aTags, aKey)
{
let uri = iosvc.newURI(kURIs[aURI], null, null);
let title = kTitles[aTitle];
let out = [aURI, aTitle, aBook, aTags, aKey];
out.push("\nuri=" + kURIs[aURI]);
out.push("\ntitle=" + title);
// Add the page and a visit for good measure
bhist.addPageWithDetails(uri, title, gDate);
// Add a bookmark if we need to
if (aBook != undefined) {
let book = kTitles[aBook];
let bmid = bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, uri,
bmsvc.DEFAULT_INDEX, book);
out.push("\nbook=" + book);
// Add a keyword to the bookmark if we need to
if (aKey != undefined)
bmsvc.setKeywordForBookmark(bmid, aKey);
// Add tags if we need to
if (aTags != undefined && aTags.length > 0) {
// Convert each tag index into the title
let tags = aTags.map(function(aTag) kTitles[aTag]);
tagsvc.tagURI(uri, tags);
out.push("\ntags=" + tags);
}
}
print("\nAdding page/book/tag: " + out.join(", "));
}
function run_test() {
print("\n");
// Search is asynchronous, so don't let the test finish immediately
do_test_pending();
// Load the test and print a description then run the test
let [description, search, expected, func] = gTests[current_test];
print(description);
// Do an extra function if necessary
if (func)
func();
ensure_results(search, expected);
}
// *************************************************
// *** vvv Custom Test Stuff Goes Below Here vvv ***
// *************************************************
let katakana = ["\u30a8", "\u30c9"]; // E, Do
let ideograph = ["\u4efb", "\u5929", "\u5802"]; // Nin Ten Do
// Define some shared uris and titles (each page needs its own uri)
let kURIs = [
"http://matchme/",
"http://dontmatchme/",
"http://title/1",
"http://title/2",
"http://tag/1",
"http://tag/2",
"http://crazytitle/",
"http://katakana/",
"http://ideograph/",
"http://camel/pleaseMatchMe/",
];
let kTitles = [
"title1",
"matchme2",
"dontmatchme3",
"!@#$%^&*()_+{}|:<>?word",
katakana.join(""),
ideograph.join(""),
];
// Boundaries on the url
addPageBook(0, 0);
addPageBook(1, 0);
// Boundaries on the title
addPageBook(2, 1);
addPageBook(3, 2);
// Boundaries on the tag
addPageBook(4, 0, 0, [1]);
addPageBook(5, 0, 0, [2]);
// Lots of word boundaries before a word
addPageBook(6, 3);
// Katakana
addPageBook(7, 4);
// Ideograph
addPageBook(8, 5);
// CamelCase
addPageBook(9, 0);
// For each test, provide a title, the search terms, and an array of
// [uri,title] indices of the pages that should be returned, followed by an
// optional function
let gTests = [
["0: Match 'match' at the beginning or after / or on a CamelCase",
"match", [[0,0],[2,1],[4,0],[9,0]]],
["1: Match 'dont' at the beginning or after /",
"dont", [[1,0],[3,2],[5,0]]],
["2: Match '2' after the slash and after a word (in tags too)",
"2", [[2,1],[3,2],[4,0],[5,0]]],
["3: Match 't' at the beginning or after /",
"t", [[0,0],[1,0],[2,1],[3,2],[4,0],[5,0],[9,0]]],
["4: Match 'word' after many consecutive word boundaries",
"word", [[6,3]]],
["5: Match a word boundary ':' for everything",
":", [[0,0],[1,0],[2,1],[3,2],[4,0],[5,0],[6,3],[7,4],[8,5],[9,0]]],
["6: Match word boundaries '()_+' that are among word boundaries",
"()_+", [[6,3]]],
["7: Katakana characters form a string, so match the beginning",
katakana[0], [[7,4]]],
/*["8: Middle of a katakana word shouldn't be matched",
katakana[1], []],*/
["9: Ideographs are treated as words so 'nin' is one word",
ideograph[0], [[8,5]]],
["10: Ideographs are treated as words so 'ten' is another word",
ideograph[1], [[8,5]]],
["11: Ideographs are treated as words so 'do' is yet another",
ideograph[2], [[8,5]]],
["12: Extra negative assert that we don't match in the middle",
"ch", []],
];