Bug 970456 - "Be" doesn't autocomplete to my friend "Ben" in my compose window anymore (because I have frequently-contacted holBErt contacts, with "be" in the middle of their last name). r=neil, a=mkmelin

This commit is contained in:
Magnus Melin 2014-11-13 23:12:16 +02:00
Родитель b553eaefce
Коммит 29d5933ce3
7 изменённых файлов: 283 добавлений и 78 удалений

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

@ -124,6 +124,42 @@ nsAbAutoCompleteSearch.prototype = {
return popularityIndex;
},
/**
* Gets the score of the (full) address, given the search input. We want
* results that match the beginning of a "word" in the result to score better
* than a result that matches only in the middle of the word.
*
* @param aAddress - full lower-cased address, including display name and address
* @param aSearchString - search string provided by user
* @return a score; a higher score is better than a lower one
*/
_getScore: function(aAddress, aSearchString) {
const BEST = 100;
// We'll do this case-insensitively and ignore the domain.
let atIdx = aAddress.lastIndexOf("@");
if (atIdx != -1) // mail lists don't have an @
aAddress = aAddress.substr(0, atIdx);
aSearchString = aSearchString.toLocaleLowerCase();
let idx = aAddress.indexOf(aSearchString);
if (idx == 0)
return BEST;
if (idx == -1)
return 0;
// We want to treat firstname, lastname and word boundary(ish) parts of
// the email address the same. E.g. for "John Doe (:xx) <jd.who@example.com>"
// all of these should score (almost) the same: "John", "Doe", "xx",
// ":xx:", "jd", "who".
let prevCh = aAddress.charAt(idx - 1);
if (/[ :."'(\-_<&]/.test(prevCh)) {
// -1, so exact begins-with match will still be the first hit.
return BEST - 1;
}
// The match was inside a word -> we don't care about the position.
return 0;
},
/**
* Searches cards in the given directory. If a card is matched (and isn't
* a mailing list) then the function will add a result for each email address
@ -201,30 +237,26 @@ nsAbAutoCompleteSearch.prototype = {
*
* @param directory The directory that the card is in.
* @param card The card that could be a duplicate.
* @param emailAddress The emailAddress (name/address combination) to check
* for duplicates against.
* @param lcEmailAddress The emailAddress (name/address combination) to check
* for duplicates against. Lowercased.
* @param currentResults The current results list.
*/
_checkDuplicate: function _checkDuplicate(directory, card, emailAddress,
currentResults) {
let lcEmailAddress = emailAddress.toLocaleLowerCase();
_checkDuplicate: function (directory, card, lcEmailAddress, currentResults) {
let existingResult = currentResults._collectedValues.get(lcEmailAddress);
if (!existingResult)
return false;
let popIndex = this._getPopularityIndex(directory, card);
if (existingResult) {
// It's a duplicate, is the new one more popular?
if (popIndex > existingResult.popularity) {
// Yes it is, so delete this element, return false and allow
// _addToResult to sort the new element into the correct place.
currentResults._collectedValues.delete(lcEmailAddress);
return false;
}
// Not more popular, but still a duplicate. Return true and _addToResult
// will just forget about it.
return true;
// It's a duplicate, is the new one more popular?
if (popIndex > existingResult.popularity) {
// Yes it is, so delete this element, return false and allow
// _addToResult to sort the new element into the correct place.
currentResults._collectedValues.delete(lcEmailAddress);
return false;
}
return false;
// Not more popular, but still a duplicate. Return true and _addToResult
// will just forget about it.
return true;
},
/**
@ -250,19 +282,21 @@ nsAbAutoCompleteSearch.prototype = {
return;
let emailAddress = mbox.toString();
let lcEmailAddress = emailAddress.toLocaleLowerCase();
// If it is a duplicate, then just return and don't add it. The
// _checkDuplicate function deals with it all for us.
if (this._checkDuplicate(directory, card, emailAddress, result))
if (this._checkDuplicate(directory, card, lcEmailAddress, result))
return;
result._collectedValues.set(emailAddress.toLocaleLowerCase(), {
result._collectedValues.set(lcEmailAddress, {
value: emailAddress,
comment: commentColumn,
card: card,
isPrimaryEmail: isPrimaryEmail,
emailToUse: emailToUse,
popularity: this._getPopularityIndex(directory, card)
popularity: this._getPopularityIndex(directory, card),
score: this._getScore(lcEmailAddress, result.searchString)
});
},
@ -361,15 +395,16 @@ nsAbAutoCompleteSearch.prototype = {
}
result._searchResults = [...result._collectedValues.values()];
// Order by descending popularity, then primary email before secondary
// for the same card, then for differing cards sort by email.
let order_by_popularity_and_email = function(a, b) {
return (b.popularity - a.popularity) ||
result._searchResults.sort(function(a, b) {
// Order by 1) descending score, then 2) descending popularity,
// then 3) primary email before secondary for the same card, then
// 4) by differing cards sort by email.
return (b.score - a.score) ||
(b.popularity - a.popularity) ||
((a.card == b.card && a.isPrimaryEmail) ? -1 : 0) ||
((a.value < b.value) ? -1 : (a.value == b.value) ? 0 : 1);
// TODO: this should actually use a.value.localeCompare(b.value) .
}
result._searchResults.sort(order_by_popularity_and_email);
});
}
if (result.matchCount) {

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

@ -12,19 +12,19 @@ const ACR = Components.interfaces.nsIAutoCompleteResult;
// on a pattern rather just doing the odd spot check.
//
// Note the expected arrays are in expected sort order as well.
const results = [ { email: "d <ema@foo.invalid>", dirName: kPABData.dirName },
{ email: "di <emai@foo.invalid>", dirName: kPABData.dirName },
{ email: "dis <email@foo.invalid>", dirName: kPABData.dirName },
{ email: "disp <e@foo.invalid>", dirName: kPABData.dirName },
{ email: "displ <em@foo.invalid>", dirName: kPABData.dirName },
{ email: "DisplayName1 <PrimaryEmail1@test.invalid>",
const results = [ { email: "d <ema@foo.invalid>", dirName: kPABData.dirName }, // 0
{ email: "di <emai@foo.invalid>", dirName: kPABData.dirName }, // 1
{ email: "dis <email@foo.invalid>", dirName: kPABData.dirName }, // 2
{ email: "disp <e@foo.invalid>", dirName: kPABData.dirName }, // 3
{ email: "displ <em@foo.invalid>", dirName: kPABData.dirName }, // 4
{ email: "DisplayName1 <PrimaryEmail1@test.invalid>", // 5
dirName: kCABData.dirName },
{ email: "t <list>", dirName: kPABData.dirName },
{ email: "te <lis>", dirName: kPABData.dirName },
{ email: "tes <li>", dirName: kPABData.dirName },
{ email: "t <list>", dirName: kPABData.dirName }, // 6
{ email: "te <lis>", dirName: kPABData.dirName }, // 7
{ email: "tes <li>", dirName: kPABData.dirName }, // 8
// this contact has a nickname of "abcdef"
{ email: "test <l>", dirName: kPABData.dirName } ];
{ email: "test <l>", dirName: kPABData.dirName } // 9
];
const firstNames = [ { search: "f", expected: [5, 0, 1, 2, 3, 4, 9] },
{ search: "fi", expected: [5, 0, 1, 3, 4] },
{ search: "fir", expected: [5, 0, 1, 4] },
@ -32,7 +32,7 @@ const firstNames = [ { search: "f", expected: [5, 0, 1, 2, 3, 4, 9] },
{ search: "first", expected: [5, 1] },
{ search: "firstn", expected: [5] } ];
const lastNames = [ { search: "l", expected: [5, 0, 1, 2, 3, 4, 6, 7, 8, 9] },
const lastNames = [ { search: "l", expected: [6, 7, 8, 9, 5, 0, 1, 2, 3, 4] },
{ search: "la", expected: [5, 0, 2, 3, 4] },
{ search: "las", expected: [5, 0, 3, 4] },
{ search: "last", expected: [5, 0, 4] },
@ -53,20 +53,20 @@ const nickNames = [ { search: "n", expected: [5, 0, 1, 2, 3, 4] },
{ search: "nickn", expected: [5, 3] },
{ search: "nickna", expected: [5] } ];
const emails = [ { search: "e", expected: [5, 0, 1, 2, 3, 4, 7, 8, 9] },
{ search: "em", expected: [5, 0, 1, 2, 4] },
{ search: "ema", expected: [5, 0, 1, 2] },
{ search: "emai", expected: [5, 1, 2] },
{ search: "email", expected: [5, 2] } ];
const emails = [ { search: "e", expected: [0, 1, 2, 3, 4, 5, 7, 8, 9] },
{ search: "em", expected: [0, 1, 2, 4, 5] },
{ search: "ema", expected: [0, 1, 2, 5] },
{ search: "emai", expected: [1, 2, 5] },
{ search: "email", expected: [2, 5] } ];
// "l" case tested above
const lists = [ { search: "li", expected: [5, 0, 1, 2, 3, 4, 6, 7, 8] },
const lists = [ { search: "li", expected: [6, 7, 8, 5, 0, 1, 2, 3, 4] },
{ search: "lis", expected: [6, 7] },
{ search: "list", expected: [6] },
{ search: "t", expected: [5, 0, 1, 4, 6, 7, 8, 9] },
{ search: "te", expected: [5, 7, 8, 9] },
{ search: "tes", expected: [5, 8, 9] },
{ search: "test", expected: [5, 9] },
{ search: "t", expected: [6, 7, 8, 9, 5, 0, 1, 4] },
{ search: "te", expected: [7, 8, 9, 5] },
{ search: "tes", expected: [8, 9, 5] },
{ search: "test", expected: [9, 5] },
{ search: "abcdef", expected: [9] } // Bug 441586
];
@ -172,8 +172,8 @@ function run_test() {
do_check_eq(obs._result.matchCount, 2);
do_check_eq(obs._result.defaultIndex, 0);
do_check_eq(obs._result.getValueAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
do_check_eq(obs._result.getLabelAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
do_check_eq(obs._result.getValueAt(0), "dis <email@foo.invalid>");
do_check_eq(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
do_check_eq(obs._result.getCommentAt(0), "");
do_check_eq(obs._result.getStyleAt(0), "local-abook");
do_check_eq(obs._result.getImageAt(0), "");
@ -198,9 +198,9 @@ function run_test() {
do_check_eq(obs._result.matchCount, 2);
do_check_eq(obs._result.defaultIndex, 0);
do_check_eq(obs._result.getValueAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
do_check_eq(obs._result.getLabelAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
do_check_eq(obs._result.getCommentAt(0), kCABData.dirName);
do_check_eq(obs._result.getValueAt(0), "dis <email@foo.invalid>");
do_check_eq(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
do_check_eq(obs._result.getCommentAt(0), kPABData.dirName);
do_check_eq(obs._result.getStyleAt(0), "local-abook");
do_check_eq(obs._result.getImageAt(0), "");
@ -214,18 +214,27 @@ function run_test() {
do_check_eq(obs._result.matchCount, 2);
do_check_eq(obs._result.defaultIndex, 0);
do_check_eq(obs._result.getValueAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
do_check_eq(obs._result.getLabelAt(0), "DisplayName1 <PrimaryEmail1@test.invalid>");
do_check_eq(obs._result.getCommentAt(0), kCABData.dirName);
do_check_eq(obs._result.getValueAt(0), "dis <email@foo.invalid>");
do_check_eq(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
do_check_eq(obs._result.getCommentAt(0), kPABData.dirName);
do_check_eq(obs._result.getStyleAt(0), "local-abook");
do_check_eq(obs._result.getImageAt(0), "");
// Now check multiple matches
function checkInputItem(element, index, array) {
print("Checking " + element.search);
print("Search #" + index + ": search=" + element.search);
acs.startSearch(element.search, param, null, obs);
for (var i = 0; i < obs._result.matchCount; i++) {
print("... got " + i + ": " + obs._result.getValueAt(i));
}
for (var i = 0; i < element.expected.length; i++) {
print("... expected " + i + " (result " + element.expected[i] + "): " +
results[element.expected[i]].email);
}
do_check_eq(obs._search, acs);
do_check_eq(obs._result.searchString, element.search);
do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);

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

@ -30,7 +30,7 @@ const cards = [
];
const duplicates = [
{ search: "test", expected: [2, 1] },
{ search: "test", expected: [1, 2] },
{ search: "first", expected: [6, 5, 3] },
{ search: "(bracket)", expected: [7, 8] }
];
@ -80,18 +80,24 @@ function run_test()
var obs = new acObserver();
function checkInputItem(element, index, array) {
print("Checking " + element.search);
print("Search #" + index + ": search=" + element.search);
acs.startSearch(element.search, JSON.stringify({ type: "addr_to" }), null, obs);
for (var i = 0; i < obs._result.matchCount; i++) {
print("... got " + i + ": " + obs._result.getValueAt(i));
}
for (var i = 0; i < element.expected.length; i++) {
print("... expected " + i + " (card " + element.expected[i] + "): " +
cards[element.expected[i]].value);
}
do_check_eq(obs._search, acs);
do_check_eq(obs._result.searchString, element.search);
do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
do_check_eq(obs._result.errorDescription, null);
do_check_eq(obs._result.matchCount, element.expected.length);
for (var i = 0; i < element.expected.length; ++i)
print(obs._result.getValueAt(i));
for (var i = 0; i < element.expected.length; ++i) {
do_check_eq(obs._result.getValueAt(i), cards[element.expected[i]].value);
do_check_eq(obs._result.getLabelAt(i), cards[element.expected[i]].value);

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

@ -42,27 +42,27 @@ const searches = [ "primary", "second", "firstName", "thename", "sortbasic",
"testsort", "2testsort", "3testsort" ];
const expectedResults = [ [ "primary@test.invalid",
"second@test.invalid" ], // searching for primary/second returns
[ "primary@test.invalid", // both the emails as the new search query
"second@test.invalid" ], // looks in both the fields.
"second@test.invalid"], // searching for primary/second returns
[ "second@test.invalid", // both the emails as the new search query
"primary@test.invalid" ], // looks in both the fields.
[ "test1@test.invalid",
"test2@test.invalid" ],
[ "name@test.invalid",
"thename@test.invalid" ],
[ "thename@test.invalid",
"name@test.invalid"],
[ "sortbasic <foo_b@test.invalid>",
"sortbasic <foo_a@test.invalid>" ],
[ "3testsort <j@test.invalid>",
[ "testsort <c@test.invalid>",
"testsort <a@test.invalid>",
"testsort <d@test.invalid>",
"testsort <e@test.invalid>",
"3testsort <j@test.invalid>",
"3testsort <h@test.invalid>",
"3testsort <g@test.invalid>",
"3testsort <f@test.invalid>",
"2testsort <c@test.invalid>",
"2testsort <a@test.invalid>",
"2testsort <d@test.invalid>",
"2testsort <e@test.invalid>",
"testsort <c@test.invalid>",
"testsort <a@test.invalid>",
"testsort <d@test.invalid>",
"testsort <e@test.invalid>" ],
"2testsort <e@test.invalid>"],
[ "2testsort <c@test.invalid>",
"2testsort <a@test.invalid>",
"2testsort <d@test.invalid>",
@ -131,7 +131,7 @@ function run_test()
print("Checking Initial Searches");
function checkSearch(element, index, array) {
print("Checking " + element);
print("Search #" + index + ": search=" + element);
acs.startSearch(element, JSON.stringify({ type: "addr_to" }), null, obs);
do_check_eq(obs._search, acs);

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

@ -23,7 +23,7 @@ const firstNames = [ { search: "f", expected: [4, 0, 1, 2, 3, 8] },
{ search: "firs", expected: [0, 1] },
{ search: "first", expected: [1] } ];
const lastNames = [ { search: "l", expected: [4, 0, 1, 2, 3, 5, 6, 7, 8] },
const lastNames = [ { search: "l", expected: [5, 6, 7, 8, 4, 0, 1, 2, 3] },
{ search: "la", expected: [4, 0, 2, 3] },
{ search: "las", expected: [4, 0, 3] },
{ search: "last", expected: [4, 0] },
@ -63,9 +63,18 @@ function run_test() {
// Now check multiple matches
function checkInputItem(element, index, array) {
print("Checking " + element.search);
print("Search #" + index + ": search=" + element.search);
acs.startSearch(element.search, JSON.stringify({ type: "addr_to" }), null, obs);
for (var i = 0; i < obs._result.matchCount; i++) {
print("... got " + i + ": " + obs._result.getValueAt(i));
}
for (var i = 0; i < element.expected.length; i++) {
print("... expected " + i + " (card " + element.expected[i] + "): " +
results[element.expected[i]].email);
}
do_check_eq(obs._search, acs);
do_check_eq(obs._result.searchString, element.search);
do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);

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

@ -0,0 +1,145 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/**
* Tests for for nsAbAutoCompleteSearch scoring.
*/
const ACR = Components.interfaces.nsIAutoCompleteResult;
const cards = [
{ // 0
email: "jd.who@example.com", displayName: "John Doe (:xx)",
popularityIndex: 0, firstName: "John", value: "John Doe (:xx) <jd.who@example.com>"
},
{ // 1
email: "janey_who@example.com", displayName: "Jane Doe",
popularityIndex: 0, value: "Jane Doe <janey_who@example.com>"
},
{ // 2
email: "pf@example.com", displayName: "Paul \"Shitbreak\" Finch",
popularityIndex: 0, value: "Paul \"Shitbreak\" Finch <pf@example.com>"
},
{ // 3
email: "js@example.com", displayName: "Janine (Stifflers Mom)",
popularityIndex: 0, value: "Janine (Stifflers Mom) <js@example.com>"
},
{ // 4
email: "ex0@example.com", displayName: "Ajden",
popularityIndex: 0, value: "Ajden <ex0@example.com>"
},
{ // 5
email: "5@example.com", displayName: "Foxx",
popularityIndex: 0, value: "Foxx <5@example.com>"
},
{ // 6
email: "6@example.com", displayName: "thewho",
popularityIndex: 0, value: "thewho <6@example.com>"
},
{ // 7
email: "7@example.com", displayName: "fakeshit",
popularityIndex: 0, value: "fakeshit <7@example.com>"
},
{ // 8
email: "8@example.com", displayName: "mastiff",
popularityIndex: 0, value: "mastiff <8@example.com>"
},
{ // 9
email: "9@example.com", displayName: "anyjohn",
popularityIndex: 0, value: "anyjohn <9@example.com>"
},
{ // 10
email: "10@example.com", displayName: "däsh l18n",
popularityIndex: 0, value: "däsh l18n <10@example.com>"
}
];
const inputs = [
{ search: "john", expected: [0, 9] },
{ search: "doe", expected: [1, 0] },
{ search: "jd", expected: [0, 4] },
{ search: "who", expected: [1, 0, 6] },
{ search: "xx", expected: [0, 5] },
{ search: "jan", expected: [1, 3] },
{ search: "sh", expected: [2, 10, 7] },
{ search: "st", expected: [3,8] }
];
function acObserver() {}
acObserver.prototype = {
_search: null,
_result: null,
onSearchResult: function (aSearch, aResult) {
this._search = aSearch;
this._result = aResult;
}
};
function run_test()
{
// We set up the cards for this test manually as it is easier to set the
// popularity index and we don't need many.
// Ensure all the directories are initialised.
MailServices.ab.directories;
let ab = MailServices.ab.getDirectory(kPABData.URI);
function createAndAddCard(element) {
var card = Cc["@mozilla.org/addressbook/cardproperty;1"]
.createInstance(Ci.nsIAbCard);
card.primaryEmail = element.email;
card.displayName = element.displayName;
card.setProperty("PopularityIndex", element.popularityIndex);
card.firstName = element.firstName;
ab.addCard(card);
}
cards.forEach(createAndAddCard);
// Test - duplicate elements
var acs = Components.classes["@mozilla.org/autocomplete/search;1?name=addrbook"]
.getService(Components.interfaces.nsIAutoCompleteSearch);
var obs = new acObserver();
function checkInputItem(element, index, array) {
print("Search #" + index + ": search=" + element.search);
acs.startSearch(element.search, JSON.stringify({ type: "addr_to" }), null, obs);
for (var i = 0; i < obs._result.matchCount; i++) {
print("... got " + i + ": " + obs._result.getValueAt(i));
}
for (var i = 0; i < element.expected.length; i++) {
print("... expected " + i + " (card " + element.expected[i] + "): " +
cards[element.expected[i]].value);
}
do_check_eq(obs._search, acs);
do_check_eq(obs._result.searchString, element.search);
do_check_eq(obs._result.searchResult, ACR.RESULT_SUCCESS);
do_check_eq(obs._result.errorDescription, null);
do_check_eq(obs._result.matchCount, element.expected.length);
for (var i = 0; i < element.expected.length; ++i) {
do_check_eq(obs._result.getValueAt(i), cards[element.expected[i]].value);
do_check_eq(obs._result.getLabelAt(i), cards[element.expected[i]].value);
}
}
inputs.forEach(checkInputItem);
}

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

@ -23,6 +23,7 @@ support-files = data/*
[test_nsAbAutoCompleteSearch3.js]
[test_nsAbAutoCompleteSearch4.js]
[test_nsAbAutoCompleteSearch5.js]
[test_nsAbAutoCompleteSearch6.js]
[test_nsAbManager1.js]
[test_nsAbManager2.js]
[test_nsIAbCard.js]