First attempt on Shortcuts provider - previously selected URLs are given boost in this provider.
Still not implemented: permanent storage of the data, so each restart of the Chrome starts the provider with a clean slate.
The scores will probably need to be adjusted as well.
TEST=unit-tested
BUG=none
Review URL: http://codereview.chromium.org/7035018
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@90980 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/autocomplete/autocomplete.cc b/chrome/browser/autocomplete/autocomplete.cc
index 9fe2688..40ad6d26 100644
--- a/chrome/browser/autocomplete/autocomplete.cc
+++ b/chrome/browser/autocomplete/autocomplete.cc
@@ -22,6 +22,7 @@
#include "chrome/browser/autocomplete/history_url_provider.h"
#include "chrome/browser/autocomplete/keyword_provider.h"
#include "chrome/browser/autocomplete/search_provider.h"
+#include "chrome/browser/autocomplete/shortcuts_provider.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/external_protocol/external_protocol_handler.h"
#include "chrome/browser/net/url_fixer_upper.h"
@@ -791,6 +792,9 @@
switches::kDisableHistoryQuickProvider);
if (hqp_enabled)
providers_.push_back(new HistoryQuickProvider(this, profile));
+ if (CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kEnableShortcutsProvider))
+ providers_.push_back(new ShortcutsProvider(this, profile));
if (!CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDisableHistoryURLProvider))
providers_.push_back(new HistoryURLProvider(this, profile));
diff --git a/chrome/browser/autocomplete/shortcuts_provider.cc b/chrome/browser/autocomplete/shortcuts_provider.cc
new file mode 100644
index 0000000..14d9102
--- /dev/null
+++ b/chrome/browser/autocomplete/shortcuts_provider.cc
@@ -0,0 +1,399 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/autocomplete/shortcuts_provider.h"
+
+#include <algorithm>
+#include <cmath>
+#include <map>
+#include <vector>
+
+#include "base/i18n/break_iterator.h"
+#include "base/i18n/case_conversion.h"
+#include "base/logging.h"
+#include "base/metrics/histogram.h"
+#include "base/string_number_conversions.h"
+#include "base/string_util.h"
+#include "base/time.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/history_notifications.h"
+#include "chrome/browser/net/url_fixer_upper.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/common/url_constants.h"
+#include "content/common/notification_details.h"
+#include "content/common/notification_source.h"
+#include "content/common/notification_type.h"
+#include "googleurl/src/url_parse.h"
+#include "googleurl/src/url_util.h"
+#include "net/base/escape.h"
+#include "net/base/net_util.h"
+
+namespace {
+// Adds match at the end if and only if its style is different from the last
+// match.
+void AddLastMatchIfNeeded(ACMatchClassifications* matches,
+ int position,
+ int style) {
+ DCHECK(matches);
+ if (matches->empty() || matches->back().style != style)
+ matches->push_back(ACMatchClassification(position, style));
+}
+
+// Takes Match classification vector and removes all matched positions,
+// compacting repetitions if necessary.
+void StripMatchMarkersFromClassifications(ACMatchClassifications* matches) {
+ DCHECK(matches);
+ ACMatchClassifications unmatched;
+ for (ACMatchClassifications::iterator i = matches->begin();
+ i != matches->end(); ++i) {
+ AddLastMatchIfNeeded(&unmatched, i->offset,
+ i->style & ~ACMatchClassification::MATCH);
+ }
+ matches->swap(unmatched);
+}
+
+class RemoveMatchPredicate {
+ public:
+ explicit RemoveMatchPredicate(const std::set<GURL>& urls)
+ : urls_(urls) {
+ }
+ bool operator()(AutocompleteMatch match) {
+ return urls_.find(match.destination_url) != urls_.end();
+ }
+ private:
+ // Lifetime of the object is less than the lifetime of passed |urls|, so
+ // it is safe to store reference.
+ const std::set<GURL>& urls_;
+};
+
+} // namespace
+
+// A match needs to score at least 1200 to be default, so set the max below
+// this. For ease of unit testing, make it divisible by 4 (since some tests
+// check for half or quarter of the max score).
+const int ShortcutsProvider::kMaxScore = 1196;
+
+ShortcutsProvider::ShortcutsProvider(ACProviderListener* listener,
+ Profile* profile)
+ : AutocompleteProvider(listener, profile, "ShortcutsProvider"),
+ languages_(profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)) {
+ notification_registrar_.Add(this, NotificationType::OMNIBOX_OPENED_URL,
+ Source<Profile>(profile));
+ notification_registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED,
+ Source<Profile>(profile));
+}
+
+ShortcutsProvider::~ShortcutsProvider() {
+}
+
+void ShortcutsProvider::Start(const AutocompleteInput& input,
+ bool minimal_changes) {
+ matches_.clear();
+
+ if (input.type() == AutocompleteInput::INVALID)
+ return;
+
+ base::TimeTicks start_time = base::TimeTicks::Now();
+ GetMatches(input);
+ if (input.text().length() < 6) {
+ base::TimeTicks end_time = base::TimeTicks::Now();
+ std::string name = "ShortcutsProvider.QueryIndexTime." +
+ base::IntToString(input.text().size());
+ base::Histogram* counter = base::Histogram::FactoryGet(
+ name, 1, 1000, 50, base::Histogram::kUmaTargetedHistogramFlag);
+ counter->Add(static_cast<int>((end_time - start_time).InMilliseconds()));
+ }
+ UpdateStarredStateOfMatches();
+}
+
+void ShortcutsProvider::DeleteMatch(const AutocompleteMatch& match) {
+ // When a user deletes a match, he probably means for the URL to disappear out
+ // of history entirely. So nuke all shortcuts that map to this URL.
+ std::set<GURL> url;
+ url.insert(match.destination_url);
+ // Immediately delete matches and shortcuts with the URL, so we can update the
+ // controller synchronously.
+ DeleteShortcutsWithURLs(url);
+ DeleteMatchesWithURLs(url);
+
+ // Delete the match from the history DB. This will eventually result in a
+ // second call to DeleteShortcutsWithURLs(), which is harmless.
+ HistoryService* const history_service =
+ profile_->GetHistoryService(Profile::EXPLICIT_ACCESS);
+
+ DCHECK(history_service && match.destination_url.is_valid());
+ history_service->DeleteURL(match.destination_url);
+}
+
+void ShortcutsProvider::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type == NotificationType::HISTORY_URLS_DELETED) {
+ const std::set<GURL>& urls =
+ Details<const history::URLsDeletedDetails>(details)->urls;
+ DeleteShortcutsWithURLs(urls);
+ return;
+ }
+
+ DCHECK(type == NotificationType::OMNIBOX_OPENED_URL);
+
+ AutocompleteLog* log = Details<AutocompleteLog>(details).ptr();
+ string16 text_lowercase(base::i18n::ToLower(log->text));
+
+ int number_of_hits = 1;
+ const AutocompleteMatch& match(log->result.match_at(log->selected_index));
+ for (ShortcutMap::iterator it = FindFirstMatch(text_lowercase);
+ it != shortcuts_map_.end() &&
+ StartsWith(it->first, text_lowercase, true); ++it) {
+ if (match.destination_url == it->second.url) {
+ number_of_hits = it->second.number_of_hits + 1;
+ shortcuts_map_.erase(it);
+ break;
+ }
+ }
+ Shortcut shortcut(log->text, match.destination_url, match.contents,
+ match.contents_class, match.description, match.description_class);
+ shortcut.number_of_hits = number_of_hits;
+ shortcuts_map_.insert(std::make_pair(text_lowercase, shortcut));
+ // TODO(georgey): Update db here.
+}
+
+void ShortcutsProvider::DeleteMatchesWithURLs(const std::set<GURL>& urls) {
+ remove_if(matches_.begin(), matches_.end(), RemoveMatchPredicate(urls));
+ listener_->OnProviderUpdate(true);
+}
+
+void ShortcutsProvider::DeleteShortcutsWithURLs(const std::set<GURL>& urls) {
+ for (ShortcutMap::iterator it = shortcuts_map_.begin();
+ it != shortcuts_map_.end();) {
+ if (urls.find(it->second.url) != urls.end())
+ shortcuts_map_.erase(it++);
+ else
+ ++it;
+ }
+ // TODO(georgey): Update db here.
+}
+
+void ShortcutsProvider::GetMatches(const AutocompleteInput& input) {
+ // Get the URLs from the shortcuts database with keys that partially or
+ // completely match the search term.
+ string16 term_string(base::i18n::ToLower(input.text()));
+ DCHECK(!term_string.empty());
+
+ for (ShortcutMap::iterator it = FindFirstMatch(term_string);
+ it != shortcuts_map_.end() &&
+ StartsWith(it->first, term_string, true); ++it)
+ matches_.push_back(ShortcutToACMatch(input, term_string, it));
+ std::partial_sort(matches_.begin(),
+ matches_.begin() +
+ std::min(AutocompleteProvider::kMaxMatches, matches_.size()),
+ matches_.end(), &AutocompleteMatch::MoreRelevant);
+ if (matches_.size() > AutocompleteProvider::kMaxMatches) {
+ matches_.erase(matches_.begin() + AutocompleteProvider::kMaxMatches,
+ matches_.end());
+ }
+}
+
+AutocompleteMatch ShortcutsProvider::ShortcutToACMatch(
+ const AutocompleteInput& input,
+ const string16& term_string,
+ ShortcutMap::iterator it) {
+ AutocompleteMatch match(this, CalculateScore(term_string, it->second),
+ true, AutocompleteMatch::HISTORY_TITLE);
+ match.destination_url = it->second.url;
+ DCHECK(match.destination_url.is_valid());
+ match.fill_into_edit = UTF8ToUTF16(it->second.url.spec());
+
+ match.contents = it->second.contents;
+ match.contents_class = ClassifyAllMatchesInString(term_string,
+ match.contents,
+ it->second.contents_class);
+
+ match.description = it->second.description;
+ match.description_class = ClassifyAllMatchesInString(
+ term_string, match.description, it->second.description_class);
+
+ return match;
+}
+
+// static
+ACMatchClassifications ShortcutsProvider::ClassifyAllMatchesInString(
+ const string16& find_text,
+ const string16& text,
+ const ACMatchClassifications& original_matches) {
+ DCHECK(!original_matches.empty());
+ DCHECK(!find_text.empty());
+
+ base::i18n::BreakIterator term_iter(find_text,
+ base::i18n::BreakIterator::BREAK_WORD);
+ if (!term_iter.Init())
+ return original_matches;
+
+ std::vector<string16> terms;
+ while (term_iter.Advance()) {
+ if (term_iter.IsWord())
+ terms.push_back(term_iter.GetString());
+ }
+ // Sort the strings so that longer strings appear after their prefixes, if
+ // any.
+ std::sort(terms.begin(), terms.end());
+
+ // Find the earliest match of any word in |find_text| in the |text|. Add to
+ // |matches|. Move pointer after match. Repeat until all matches are found.
+ string16 text_lowercase(base::i18n::ToLower(text));
+ ACMatchClassifications matches;
+ // |matches| should start at the position 0, if the matched text start from
+ // the position 0, this will be poped from the vector further down.
+ matches.push_back(ACMatchClassification(0, ACMatchClassification::NONE));
+ for (size_t last_position = 0; last_position < text_lowercase.length();) {
+ size_t match_start = text_lowercase.length();
+ size_t match_end = last_position;
+
+ for (size_t i = 0; i < terms.size(); ++i) {
+ size_t match = text_lowercase.find(terms[i], last_position);
+ // Use <= in conjunction with the sort() call above so that longer strings
+ // are matched in preference to their prefixes.
+ if (match != string16::npos && match <= match_start) {
+ match_start = match;
+ match_end = match + terms[i].length();
+ }
+ }
+
+ if (match_start >= match_end)
+ break;
+
+ // Collapse adjacent ranges into one.
+ if (!matches.empty() && matches.back().offset == match_start)
+ matches.pop_back();
+
+ AddLastMatchIfNeeded(&matches, match_start, ACMatchClassification::MATCH);
+ if (match_end < text_lowercase.length())
+ AddLastMatchIfNeeded(&matches, match_end, ACMatchClassification::NONE);
+
+ last_position = match_end;
+ }
+
+ // Merge matches with highlight data.
+ if (matches.empty())
+ return original_matches;
+
+ ACMatchClassifications output;
+ for (ACMatchClassifications::const_iterator i = original_matches.begin(),
+ j = matches.begin(); i != original_matches.end();) {
+ AddLastMatchIfNeeded(&output, std::max(i->offset, j->offset),
+ i->style | j->style);
+ if ((j + 1) == matches.end() || (((i + 1) != original_matches.end()) &&
+ ((j + 1)->offset > (i + 1)->offset)))
+ ++i;
+ else
+ ++j;
+ }
+
+ return output;
+}
+
+ShortcutsProvider::ShortcutMap::iterator ShortcutsProvider::FindFirstMatch(
+ const string16& keyword) {
+ ShortcutMap::iterator it = shortcuts_map_.lower_bound(keyword);
+ // Lower bound not necessarily matches the keyword, check for item pointed by
+ // the lower bound iterator to at least start with keyword.
+ return ((it == shortcuts_map_.end()) ||
+ StartsWith(it->first, keyword, true)) ? it : shortcuts_map_.end();
+}
+
+// static
+int ShortcutsProvider::CalculateScore(const string16& terms,
+ const Shortcut& shortcut) {
+ DCHECK(!terms.empty());
+ DCHECK_LE(terms.length(), shortcut.text.length());
+
+ // The initial score is based on how much of the shortcut the user has typed.
+ double base_score = kMaxScore * static_cast<double>(terms.length()) /
+ shortcut.text.length();
+
+ // Then we decay this by half each week.
+ const double kLn2 = 0.6931471805599453;
+ base::TimeDelta time_passed = base::Time::Now() - shortcut.last_access_time;
+ // Clamp to 0 in case time jumps backwards (e.g. due to DST).
+ double decay_exponent = std::max(0.0, kLn2 * static_cast<double>(
+ time_passed.InMicroseconds()) / base::Time::kMicrosecondsPerWeek);
+
+ // We modulate the decay factor based on how many times the shortcut has been
+ // used. Newly created shortcuts decay at full speed; otherwise, decaying by
+ // half takes |n| times as much time, where n increases by
+ // (1.0 / each 5 additional hits), up to a maximum of 5x as long.
+ const double kMaxDecaySpeedDivisor = 5.0;
+ const double kNumUsesPerDecaySpeedDivisorIncrement = 5.0;
+ double decay_divisor = std::min(kMaxDecaySpeedDivisor,
+ (shortcut.number_of_hits + kNumUsesPerDecaySpeedDivisorIncrement - 1) /
+ kNumUsesPerDecaySpeedDivisorIncrement);
+
+ return static_cast<int>((base_score / exp(decay_exponent / decay_divisor)) +
+ 0.5);
+}
+
+// static
+string16 ShortcutsProvider::SpansToString(const ACMatchClassifications& value) {
+ string16 spans;
+ string16 comma(ASCIIToUTF16(","));
+ for (size_t i = 0; i < value.size(); ++i) {
+ if (i)
+ spans.append(comma);
+ spans.append(base::IntToString16(value[i].offset));
+ spans.append(comma);
+ spans.append(base::IntToString16(value[i].style));
+ }
+ return spans;
+}
+
+// static
+ACMatchClassifications ShortcutsProvider::SpansFromString(
+ const string16& value) {
+ ACMatchClassifications spans;
+ std::vector<string16> tokens;
+ Tokenize(value, ASCIIToUTF16(","), &tokens);
+ // The number of tokens should be even.
+ if ((tokens.size() & 1) == 1) {
+ NOTREACHED();
+ return spans;
+ }
+ for (size_t i = 0; i < tokens.size(); i += 2) {
+ int span_start = 0;
+ int span_type = ACMatchClassification::NONE;
+ if (!base::StringToInt(tokens[i], &span_start) ||
+ !base::StringToInt(tokens[i + 1], &span_type)) {
+ NOTREACHED();
+ return spans;
+ }
+ spans.push_back(ACMatchClassification(span_start, span_type));
+ }
+ return spans;
+}
+
+ShortcutsProvider::Shortcut::Shortcut(
+ const string16& text,
+ const GURL& url,
+ const string16& contents,
+ const ACMatchClassifications& in_contents_class,
+ const string16& description,
+ const ACMatchClassifications& in_description_class)
+ : text(text),
+ url(url),
+ contents(contents),
+ contents_class(in_contents_class),
+ description(description),
+ description_class(in_description_class),
+ last_access_time(base::Time::Now()),
+ number_of_hits(1) {
+ StripMatchMarkersFromClassifications(&contents_class);
+ StripMatchMarkersFromClassifications(&description_class);
+}
+
+ShortcutsProvider::Shortcut::Shortcut()
+ : last_access_time(base::Time::Now()),
+ number_of_hits(0) {
+}
diff --git a/chrome/browser/autocomplete/shortcuts_provider.h b/chrome/browser/autocomplete/shortcuts_provider.h
new file mode 100644
index 0000000..74fd3a5
--- /dev/null
+++ b/chrome/browser/autocomplete/shortcuts_provider.h
@@ -0,0 +1,130 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_AUTOCOMPLETE_SHORTCUTS_PROVIDER_H_
+#define CHROME_BROWSER_AUTOCOMPLETE_SHORTCUTS_PROVIDER_H_
+#pragma once
+
+#include <map>
+#include <set>
+#include <string>
+
+#include "base/gtest_prod_util.h"
+#include "base/time.h"
+#include "chrome/browser/autocomplete/autocomplete_match.h"
+#include "chrome/browser/autocomplete/history_provider.h"
+#include "content/common/notification_observer.h"
+#include "content/common/notification_registrar.h"
+
+class Profile;
+struct AutocompleteLog;
+
+// Provider of recently autocompleted links. Provides autocomplete suggestions
+// from previously selected suggestions. The more often a user selects a
+// suggestion for a given search term the higher will be that suggestion's
+// ranking for future uses of that search term.
+class ShortcutsProvider : public AutocompleteProvider,
+ public NotificationObserver {
+ public:
+ ShortcutsProvider(ACProviderListener* listener, Profile* profile);
+ virtual ~ShortcutsProvider();
+
+ // Performs the autocompletion synchronously. Since no asynch completion is
+ // performed |minimal_changes| is ignored.
+ virtual void Start(const AutocompleteInput& input,
+ bool minimal_changes) OVERRIDE;
+
+ virtual void DeleteMatch(const AutocompleteMatch& match) OVERRIDE;
+
+ private:
+ friend class ShortcutsProviderTest;
+ FRIEND_TEST_ALL_PREFIXES(ShortcutsProviderTest, ClassifyAllMatchesInString);
+ FRIEND_TEST_ALL_PREFIXES(ShortcutsProviderTest, CalculateScore);
+ FRIEND_TEST_ALL_PREFIXES(ShortcutsProviderTest, DeleteMatch);
+
+ // The following struct encapsulates one previously selected omnibox shortcut.
+ struct Shortcut {
+ Shortcut(const string16& text,
+ const GURL& url,
+ const string16& contents,
+ const ACMatchClassifications& contents_class,
+ const string16& description,
+ const ACMatchClassifications& description_class);
+ // Required for STL, we don't use this directly.
+ Shortcut();
+
+ string16 text; // The user's original input string.
+ GURL url; // The corresponding destination URL.
+
+ // Contents and description from the original match, along with their
+ // corresponding markup. We need these in order to correctly mark which
+ // parts are URLs, dim, etc. However, we strip all MATCH classifications
+ // from these since we'll mark the matching portions ourselves as we match
+ // the user's current typing against these Shortcuts.
+ string16 contents;
+ ACMatchClassifications contents_class;
+ string16 description;
+ ACMatchClassifications description_class;
+
+ base::Time last_access_time; // Last time shortcut was selected.
+ int number_of_hits; // How many times shortcut was selected.
+ };
+
+ // Maps the original match (|text| in the Shortcut) to Shortcut for quick
+ // search.
+ typedef std::multimap<string16, Shortcut> ShortcutMap;
+
+ // Clamp relevance scores to ensure none of our matches will become the
+ // default. This prevents us from having to worry about inline autocompletion.
+ static const int kMaxScore;
+
+ // NotificationObserver:
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) OVERRIDE;
+
+ void DeleteMatchesWithURLs(const std::set<GURL>& urls);
+ void DeleteShortcutsWithURLs(const std::set<GURL>& urls);
+
+ // Performs the autocomplete matching and scoring.
+ void GetMatches(const AutocompleteInput& input);
+
+ AutocompleteMatch ShortcutToACMatch(const AutocompleteInput& input,
+ const string16& terms,
+ ShortcutMap::iterator it);
+
+ // Given |text| and a corresponding base set of classifications
+ // |original_class|, adds ACMatchClassification::MATCH markers for all
+ // instances of the words from |find_text| within |text| and returns the
+ // resulting classifications.
+ //
+ // For example, given the |text|
+ // "Sports and News at sports.somesite.com - visit us!" and |original_class|
+ // {{0, NONE}, {18, URL}, {37, NONE}} (marking "sports.somesite.com" as a
+ // URL), calling with |find_text| set to "sp ew" would return
+ // {{0, MATCH}, {2, NONE}, {12, MATCH}, {14, NONE}, {18, URL|MATCH},
+ // {20, URL}, {37, NONE}}
+ static ACMatchClassifications ClassifyAllMatchesInString(
+ const string16& find_text,
+ const string16& text,
+ const ACMatchClassifications& original_class);
+
+ // Returns iterator to first item in |shortcuts_map_| matching |keyword|.
+ // Returns shortcuts_map_.end() if there are no matches.
+ ShortcutMap::iterator FindFirstMatch(const string16& keyword);
+
+ static int CalculateScore(const string16& terms, const Shortcut& shortcut);
+
+ // Helpers dealing with database update.
+ // Converts spans vector to comma-separated string.
+ static string16 SpansToString(const ACMatchClassifications& value);
+ // Coverts comma-separated unsigned integer values into spans vector.
+ static ACMatchClassifications SpansFromString(const string16& value);
+
+ std::string languages_;
+ NotificationRegistrar notification_registrar_;
+ ShortcutMap shortcuts_map_;
+};
+
+#endif // CHROME_BROWSER_AUTOCOMPLETE_SHORTCUTS_PROVIDER_H_
diff --git a/chrome/browser/autocomplete/shortcuts_provider_unittest.cc b/chrome/browser/autocomplete/shortcuts_provider_unittest.cc
new file mode 100644
index 0000000..7db7df3
--- /dev/null
+++ b/chrome/browser/autocomplete/shortcuts_provider_unittest.cc
@@ -0,0 +1,586 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/autocomplete/shortcuts_provider.h"
+
+#include <algorithm>
+#include <functional>
+#include <set>
+#include <string>
+#include <vector>
+
+#include "base/memory/scoped_ptr.h"
+#include "base/message_loop.h"
+#include "base/stringprintf.h"
+#include "base/utf_string_conversions.h"
+#include "chrome/browser/autocomplete/autocomplete.h"
+#include "chrome/browser/autocomplete/autocomplete_match.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/in_memory_url_index.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/testing_browser_process.h"
+#include "chrome/test/testing_browser_process_test.h"
+#include "chrome/test/testing_profile.h"
+#include "content/browser/browser_thread.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using base::Time;
+using base::TimeDelta;
+
+struct TestShortcutInfo {
+ std::string url;
+ std::string title; // The text that orginally was searched for.
+ std::string contents;
+ std::string contents_class;
+ std::string description;
+ std::string description_class;
+ int typed_count;
+ int days_from_now;
+} shortcut_test_db[] = {
+ { "http://www.google.com/", "goog",
+ "Google", "0,1,4,0", "Google", "0,3,4,1", 100, 1 },
+ { "http://slashdot.org/", "slash",
+ "slashdot.org", "0,3,5,1",
+ "Slashdot - News for nerds, stuff that matters", "0,2,5,0", 100, 0},
+ { "http://slashdot.org/", "news",
+ "slashdot.org", "0,1",
+ "Slashdot - News for nerds, stuff that matters", "0,0,11,2,15,0", 5, 0},
+ { "http://sports.yahoo.com/", "news",
+ "sports.yahoo.com", "0,1",
+ "Yahoo! Sports - Sports News, Scores, Rumors, Fantasy Games, and more",
+ "0,0,23,2,27,0", 5, 2},
+ { "http://www.cnn.com/index.html", "news weather",
+ "www.cnn.com/index.html", "0,1",
+ "CNN.com - Breaking News, U.S., World, Weather, Entertainment & Video",
+ "0,0,19,2,23,0,38,2,45,0", 10, 1},
+ { "http://sports.yahoo.com/", "nhl scores",
+ "sports.yahoo.com", "0,1",
+ "Yahoo! Sports - Sports News, Scores, Rumors, Fantasy Games, and more",
+ "0,0,29,2,35,0", 10, 1},
+ { "http://www.nhl.com/scores/index.html", "nhl scores",
+ "www.nhl.com/scores/index.html", "0,1,4,3,7,1",
+ "January 13, 2010 - NHL.com - Scores", "0,0,19,2,22,0,29,2,35,0", 1, 5},
+ { "http://www.testsite.com/a.html", "just",
+ "www.testsite.com/a.html", "0,1",
+ "Test - site - just a test", "0,0,14,2,18,0", 1, 5},
+ { "http://www.testsite.com/b.html", "just",
+ "www.testsite.com/b.html", "0,1",
+ "Test - site - just a test", "0,0,14,2,18,0", 2, 5},
+ { "http://www.testsite.com/c.html", "just",
+ "www.testsite.com/c.html", "0,1",
+ "Test - site - just a test", "0,0,14,2,18,0", 1, 8},
+ { "http://www.testsite.com/d.html", "just a",
+ "www.testsite.com/d.html", "0,1",
+ "Test - site - just a test", "0,0,14,2,18,0", 1, 12},
+ { "http://www.testsite.com/e.html", "just a t",
+ "www.testsite.com/e.html", "0,1",
+ "Test - site - just a test", "0,0,14,2,18,0", 1, 12},
+ { "http://www.testsite.com/f.html", "just a te",
+ "www.testsite.com/f.html", "0,1",
+ "Test - site - just a test", "0,0,14,2,18,0", 1, 12},
+ { "http://www.daysagotest.com/a.html", "ago",
+ "www.daysagotest.com/a.html", "0,1,8,3,11,1",
+ "Test - site", "0,0", 1, 1},
+ { "http://www.daysagotest.com/b.html", "ago",
+ "www.daysagotest.com/b.html", "0,1,8,3,11,1",
+ "Test - site", "0,0", 1, 2},
+ { "http://www.daysagotest.com/c.html", "ago",
+ "www.daysagotest.com/c.html", "0,1,8,3,11,1",
+ "Test - site", "0,0", 1, 3},
+ { "http://www.daysagotest.com/d.html", "ago",
+ "www.daysagotest.com/d.html", "0,1,8,3,11,1",
+ "Test - site", "0,0", 1, 4},
+};
+
+class ShortcutsProviderTest : public TestingBrowserProcessTest,
+ public ACProviderListener {
+ public:
+ ShortcutsProviderTest();
+
+ // ACProviderListener
+ virtual void OnProviderUpdate(bool updated_matches);
+
+ protected:
+ void SetUp();
+ void TearDown();
+
+ // Fills test data into the provider.
+ void FillData();
+
+ // Runs an autocomplete query on |text| and checks to see that the returned
+ // results' destination URLs match those provided. |expected_urls| does not
+ // need to be in sorted order.
+ void RunTest(const string16 text,
+ std::vector<std::string> expected_urls,
+ std::string expected_top_result);
+
+ MessageLoopForUI message_loop_;
+ BrowserThread ui_thread_;
+ BrowserThread file_thread_;
+
+ scoped_ptr<TestingProfile> profile_;
+
+ ACMatches ac_matches_; // The resulting matches after running RunTest.
+
+ scoped_refptr<ShortcutsProvider> provider_;
+};
+
+ShortcutsProviderTest::ShortcutsProviderTest()
+ : ui_thread_(BrowserThread::UI, &message_loop_),
+ file_thread_(BrowserThread::FILE, &message_loop_) {
+}
+
+void ShortcutsProviderTest::OnProviderUpdate(bool updated_matches) {
+}
+
+void ShortcutsProviderTest::SetUp() {
+ profile_.reset(new TestingProfile());
+ profile_->CreateHistoryService(true, false);
+ provider_ = new ShortcutsProvider(this, profile_.get());
+ FillData();
+}
+
+void ShortcutsProviderTest::TearDown() {
+ provider_ = NULL;
+}
+
+void ShortcutsProviderTest::FillData() {
+ DCHECK(provider_.get());
+ provider_->shortcuts_map_.clear();
+ for (size_t i = 0; i < arraysize(shortcut_test_db); ++i) {
+ const TestShortcutInfo& cur = shortcut_test_db[i];
+ const GURL current_url(cur.url);
+ Time visit_time = Time::Now() - TimeDelta::FromDays(cur.days_from_now);
+ ShortcutsProvider::Shortcut shortcut(
+ ASCIIToUTF16(cur.title),
+ current_url,
+ ASCIIToUTF16(cur.contents),
+ provider_->SpansFromString(ASCIIToUTF16(cur.contents_class)),
+ ASCIIToUTF16(cur.description),
+ provider_->SpansFromString(ASCIIToUTF16(cur.description_class)));
+ shortcut.last_access_time = visit_time;
+ provider_->shortcuts_map_.insert(
+ std::pair<string16, ShortcutsProvider::Shortcut>(
+ ASCIIToUTF16(cur.title), shortcut));
+ }
+}
+
+class SetShouldContain : public std::unary_function<const std::string&,
+ std::set<std::string> > {
+ public:
+ explicit SetShouldContain(const ACMatches& matched_urls) {
+ for (ACMatches::const_iterator iter = matched_urls.begin();
+ iter != matched_urls.end(); ++iter)
+ matches_.insert(iter->destination_url.spec());
+ }
+
+ void operator()(const std::string& expected) {
+ EXPECT_EQ(1U, matches_.erase(expected));
+ }
+
+ std::set<std::string> Leftovers() const { return matches_; }
+
+ private:
+ std::set<std::string> matches_;
+};
+
+void ShortcutsProviderTest::RunTest(const string16 text,
+ std::vector<std::string> expected_urls,
+ std::string expected_top_result) {
+ std::sort(expected_urls.begin(), expected_urls.end());
+
+ MessageLoop::current()->RunAllPending();
+ AutocompleteInput input(text, string16(), false, false, true,
+ AutocompleteInput::ALL_MATCHES);
+ provider_->Start(input, false);
+ EXPECT_TRUE(provider_->done());
+
+ ac_matches_ = provider_->matches();
+
+ // We should have gotten back at most AutocompleteProvider::kMaxMatches.
+ EXPECT_LE(ac_matches_.size(), AutocompleteProvider::kMaxMatches);
+
+ // If the number of expected and actual matches aren't equal then we need
+ // test no further, but let's do anyway so that we know which URLs failed.
+ EXPECT_EQ(expected_urls.size(), ac_matches_.size());
+
+ // Verify that all expected URLs were found and that all found URLs
+ // were expected.
+ std::set<std::string> Leftovers =
+ for_each(expected_urls.begin(), expected_urls.end(),
+ SetShouldContain(ac_matches_)).Leftovers();
+ EXPECT_EQ(0U, Leftovers.size());
+
+ // See if we got the expected top scorer.
+ if (!ac_matches_.empty()) {
+ std::partial_sort(ac_matches_.begin(), ac_matches_.begin() + 1,
+ ac_matches_.end(), AutocompleteMatch::MoreRelevant);
+ EXPECT_EQ(expected_top_result, ac_matches_[0].destination_url.spec());
+ }
+}
+
+TEST_F(ShortcutsProviderTest, SimpleSingleMatch) {
+ string16 text(ASCIIToUTF16("go"));
+ std::string expected_url("http://www.google.com/");
+ std::vector<std::string> expected_urls;
+ expected_urls.push_back(expected_url);
+ RunTest(text, expected_urls, expected_url);
+}
+
+TEST_F(ShortcutsProviderTest, MultiMatch) {
+ string16 text(ASCIIToUTF16("NEWS"));
+ std::vector<std::string> expected_urls;
+ // Scores high because of completion length.
+ expected_urls.push_back("http://slashdot.org/");
+ // Scores high because of visit count.
+ expected_urls.push_back("http://sports.yahoo.com/");
+ // Scores high because of visit count but less match span,
+ // which is more important.
+ expected_urls.push_back("http://www.cnn.com/index.html");
+ RunTest(text, expected_urls, "http://slashdot.org/");
+}
+
+TEST_F(ShortcutsProviderTest, VisitCountMatches) {
+ string16 text(ASCIIToUTF16("just"));
+ std::vector<std::string> expected_urls;
+ expected_urls.push_back("http://www.testsite.com/b.html");
+ expected_urls.push_back("http://www.testsite.com/a.html");
+ expected_urls.push_back("http://www.testsite.com/c.html");
+ RunTest(text, expected_urls, "http://www.testsite.com/b.html");
+}
+
+TEST_F(ShortcutsProviderTest, TypedCountMatches) {
+ string16 text(ASCIIToUTF16("just a"));
+ std::vector<std::string> expected_urls;
+ expected_urls.push_back("http://www.testsite.com/d.html");
+ expected_urls.push_back("http://www.testsite.com/e.html");
+ expected_urls.push_back("http://www.testsite.com/f.html");
+ RunTest(text, expected_urls, "http://www.testsite.com/d.html");
+}
+
+TEST_F(ShortcutsProviderTest, DaysAgoMatches) {
+ string16 text(ASCIIToUTF16("ago"));
+ std::vector<std::string> expected_urls;
+ expected_urls.push_back("http://www.daysagotest.com/a.html");
+ expected_urls.push_back("http://www.daysagotest.com/b.html");
+ expected_urls.push_back("http://www.daysagotest.com/c.html");
+ RunTest(text, expected_urls, "http://www.daysagotest.com/a.html");
+}
+
+TEST_F(ShortcutsProviderTest, ClassifyAllMatchesInString) {
+ string16 data(ASCIIToUTF16("A man, a plan, a canal Panama"));
+ ACMatchClassifications matches;
+ // Non-matched text does not have special attributes.
+ matches.push_back(ACMatchClassification(0, ACMatchClassification::NONE));
+
+ ACMatchClassifications spans_a =
+ ShortcutsProvider::ClassifyAllMatchesInString(ASCIIToUTF16("man"),
+ data,
+ matches);
+ // ACMatch spans should be: '--MMM------------------------'
+ ASSERT_EQ(3U, spans_a.size());
+ EXPECT_EQ(0U, spans_a[0].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_a[0].style);
+ EXPECT_EQ(2U, spans_a[1].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_a[1].style);
+ EXPECT_EQ(5U, spans_a[2].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_a[2].style);
+
+ ACMatchClassifications spans_b =
+ ShortcutsProvider::ClassifyAllMatchesInString(ASCIIToUTF16("man p"),
+ data,
+ matches);
+ // ACMatch spans should be: '--MMM----M-------------M-----'
+ ASSERT_EQ(7U, spans_b.size());
+ EXPECT_EQ(0U, spans_b[0].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_b[0].style);
+ EXPECT_EQ(2U, spans_b[1].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_b[1].style);
+ EXPECT_EQ(5U, spans_b[2].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_b[2].style);
+ EXPECT_EQ(9U, spans_b[3].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_b[3].style);
+ EXPECT_EQ(10U, spans_b[4].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_b[4].style);
+ EXPECT_EQ(23U, spans_b[5].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_b[5].style);
+ EXPECT_EQ(24U, spans_b[6].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_b[6].style);
+
+ ACMatchClassifications spans_c =
+ ShortcutsProvider::ClassifyAllMatchesInString(
+ ASCIIToUTF16("man plan panama"), data, matches);
+ // ACMatch spans should be:'--MMM----MMMM----------MMMMMM'
+ ASSERT_EQ(6U, spans_c.size());
+ EXPECT_EQ(0U, spans_c[0].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_c[0].style);
+ EXPECT_EQ(2U, spans_c[1].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_c[1].style);
+ EXPECT_EQ(5U, spans_c[2].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_c[2].style);
+ EXPECT_EQ(9U, spans_c[3].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_c[3].style);
+ EXPECT_EQ(13U, spans_c[4].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_c[4].style);
+ EXPECT_EQ(23U, spans_c[5].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_c[5].style);
+
+ data = ASCIIToUTF16(
+ "Yahoo! Sports - Sports News, Scores, Rumors, Fantasy Games, and more");
+ matches.clear();
+ matches.push_back(ACMatchClassification(0, ACMatchClassification::NONE));
+
+ ACMatchClassifications spans_d =
+ ShortcutsProvider::ClassifyAllMatchesInString(ASCIIToUTF16("ne"),
+ data,
+ matches);
+ // ACMatch spans should match first two letters of the "news".
+ ASSERT_EQ(3U, spans_d.size());
+ EXPECT_EQ(0U, spans_d[0].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_d[0].style);
+ EXPECT_EQ(23U, spans_d[1].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_d[1].style);
+ EXPECT_EQ(25U, spans_d[2].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_d[2].style);
+
+ ACMatchClassifications spans_e =
+ ShortcutsProvider::ClassifyAllMatchesInString(ASCIIToUTF16("news r"),
+ data,
+ matches);
+ // ACMatch spans should be the same as original matches.
+ ASSERT_EQ(15U, spans_e.size());
+ EXPECT_EQ(0U, spans_e[0].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_e[0].style);
+ // "r" in "Sports".
+ EXPECT_EQ(10U, spans_e[1].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_e[1].style);
+ EXPECT_EQ(11U, spans_e[2].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_e[2].style);
+ // "r" in second "Sports".
+ EXPECT_EQ(19U, spans_e[3].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_e[3].style);
+ EXPECT_EQ(20U, spans_e[4].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_e[4].style);
+ // "News".
+ EXPECT_EQ(23U, spans_e[5].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_e[5].style);
+ EXPECT_EQ(27U, spans_e[6].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_e[6].style);
+ // "r" in "Scores".
+ EXPECT_EQ(32U, spans_e[7].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_e[7].style);
+ EXPECT_EQ(33U, spans_e[8].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_e[8].style);
+ // First "r" in "Rumors".
+ EXPECT_EQ(37U, spans_e[9].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_e[9].style);
+ EXPECT_EQ(38U, spans_e[10].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_e[10].style);
+ // Second "r" in "Rumors".
+ EXPECT_EQ(41U, spans_e[11].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_e[11].style);
+ EXPECT_EQ(42U, spans_e[12].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_e[12].style);
+ // "r" in "more".
+ EXPECT_EQ(66U, spans_e[13].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_e[13].style);
+ EXPECT_EQ(67U, spans_e[14].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_e[14].style);
+
+ data = ASCIIToUTF16("livescore.goal.com");
+ matches.clear();
+ // Matches for URL.
+ matches.push_back(ACMatchClassification(0, ACMatchClassification::URL));
+
+ ACMatchClassifications spans_f =
+ ShortcutsProvider::ClassifyAllMatchesInString(ASCIIToUTF16("go"),
+ data,
+ matches);
+ // ACMatch spans should match first two letters of the "goal".
+ ASSERT_EQ(3U, spans_f.size());
+ EXPECT_EQ(0U, spans_f[0].offset);
+ EXPECT_EQ(ACMatchClassification::URL, spans_f[0].style);
+ EXPECT_EQ(10U, spans_f[1].offset);
+ EXPECT_EQ(ACMatchClassification::URL | ACMatchClassification::MATCH,
+ spans_f[1].style);
+ EXPECT_EQ(12U, spans_f[2].offset);
+ EXPECT_EQ(ACMatchClassification::URL, spans_f[2].style);
+
+ data = ASCIIToUTF16("Email login: mail.somecorp.com");
+ matches.clear();
+ // Matches for URL.
+ matches.push_back(ACMatchClassification(0, ACMatchClassification::NONE));
+ matches.push_back(ACMatchClassification(13, ACMatchClassification::URL));
+
+ ACMatchClassifications spans_g =
+ ShortcutsProvider::ClassifyAllMatchesInString(ASCIIToUTF16("ail"),
+ data,
+ matches);
+ ASSERT_EQ(6U, spans_g.size());
+ EXPECT_EQ(0U, spans_g[0].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_g[0].style);
+ EXPECT_EQ(2U, spans_g[1].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_g[1].style);
+ EXPECT_EQ(5U, spans_g[2].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_g[2].style);
+ EXPECT_EQ(13U, spans_g[3].offset);
+ EXPECT_EQ(ACMatchClassification::URL, spans_g[3].style);
+ EXPECT_EQ(14U, spans_g[4].offset);
+ EXPECT_EQ(ACMatchClassification::URL | ACMatchClassification::MATCH,
+ spans_g[4].style);
+ EXPECT_EQ(17U, spans_g[5].offset);
+ EXPECT_EQ(ACMatchClassification::URL, spans_g[5].style);
+
+ ACMatchClassifications spans_h =
+ ShortcutsProvider::ClassifyAllMatchesInString(ASCIIToUTF16("lo log"),
+ data,
+ matches);
+ ASSERT_EQ(4U, spans_h.size());
+ EXPECT_EQ(0U, spans_h[0].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_h[0].style);
+ EXPECT_EQ(6U, spans_h[1].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_h[1].style);
+ EXPECT_EQ(9U, spans_h[2].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_h[2].style);
+ EXPECT_EQ(13U, spans_h[3].offset);
+ EXPECT_EQ(ACMatchClassification::URL, spans_h[3].style);
+
+ ACMatchClassifications spans_i =
+ ShortcutsProvider::ClassifyAllMatchesInString(ASCIIToUTF16("ail em"),
+ data,
+ matches);
+ // 'Email' and 'ail' should be matched.
+ ASSERT_EQ(5U, spans_i.size());
+ EXPECT_EQ(0U, spans_i[0].offset);
+ EXPECT_EQ(ACMatchClassification::MATCH, spans_i[0].style);
+ EXPECT_EQ(5U, spans_i[1].offset);
+ EXPECT_EQ(ACMatchClassification::NONE, spans_i[1].style);
+ EXPECT_EQ(13U, spans_i[2].offset);
+ EXPECT_EQ(ACMatchClassification::URL, spans_i[2].style);
+ EXPECT_EQ(14U, spans_i[3].offset);
+ EXPECT_EQ(ACMatchClassification::URL | ACMatchClassification::MATCH,
+ spans_i[3].style);
+ EXPECT_EQ(17U, spans_i[4].offset);
+ EXPECT_EQ(ACMatchClassification::URL, spans_i[4].style);
+}
+
+TEST_F(ShortcutsProviderTest, CalculateScore) {
+ ACMatchClassifications spans_content;
+ spans_content.push_back(ACMatchClassification(0, ACMatchClassification::URL));
+ spans_content.push_back(ACMatchClassification(
+ 4, ACMatchClassification::MATCH | ACMatchClassification::URL));
+ spans_content.push_back(ACMatchClassification(8, ACMatchClassification::URL));
+ ACMatchClassifications spans_description;
+ spans_description.push_back(
+ ACMatchClassification(0, ACMatchClassification::NONE));
+ spans_description.push_back(
+ ACMatchClassification(2, ACMatchClassification::MATCH));
+ ShortcutsProvider::Shortcut shortcut(
+ ASCIIToUTF16("test"),
+ GURL("http://www.test.com"),
+ ASCIIToUTF16("www.test.com"),
+ spans_content,
+ ASCIIToUTF16("A test"),
+ spans_description);
+
+ // Yes, these tests could fail if CalculateScore() takes a lot of time,
+ // but even for the last test the time to change score by 1 is around
+ // two minutes, so if it fails because of timing we've got some problems.
+
+ // Maximal score.
+ shortcut.last_access_time = Time::Now();
+ EXPECT_EQ(ShortcutsProvider::CalculateScore(ASCIIToUTF16("test"), shortcut),
+ ShortcutsProvider::kMaxScore);
+
+ // Score decreases as percent of the match is decreased.
+ EXPECT_EQ(ShortcutsProvider::CalculateScore(ASCIIToUTF16("tes"), shortcut),
+ (ShortcutsProvider::kMaxScore / 4) * 3);
+ EXPECT_EQ(ShortcutsProvider::CalculateScore(ASCIIToUTF16("te"), shortcut),
+ ShortcutsProvider::kMaxScore / 2);
+ EXPECT_EQ(ShortcutsProvider::CalculateScore(ASCIIToUTF16("t"), shortcut),
+ ShortcutsProvider::kMaxScore / 4);
+
+ // Should decay twice in a week.
+ shortcut.last_access_time = Time::Now() - TimeDelta::FromDays(7);
+ EXPECT_EQ(ShortcutsProvider::CalculateScore(ASCIIToUTF16("test"), shortcut),
+ ShortcutsProvider::kMaxScore / 2);
+
+ // Should decay four times in two weeks.
+ shortcut.last_access_time = Time::Now() - TimeDelta::FromDays(14);
+ EXPECT_EQ(ShortcutsProvider::CalculateScore(ASCIIToUTF16("test"), shortcut),
+ ShortcutsProvider::kMaxScore / 4);
+
+ // But not if it was activly clicked on. 6 hits slow decaying power twice.
+ shortcut.number_of_hits = 6;
+ shortcut.last_access_time = Time::Now() - TimeDelta::FromDays(14);
+ EXPECT_EQ(ShortcutsProvider::CalculateScore(ASCIIToUTF16("test"), shortcut),
+ ShortcutsProvider::kMaxScore / 2);
+}
+
+TEST_F(ShortcutsProviderTest, DeleteMatch) {
+ TestShortcutInfo shortcuts_to_test_delete[3] = {
+ { "http://www.deletetest.com/1.html", "delete",
+ "http://www.deletetest.com/1.html", "0,2",
+ "Erase this shortcut!", "0,0", 1, 1},
+ { "http://www.deletetest.com/1.html", "erase",
+ "http://www.deletetest.com/1.html", "0,2",
+ "Erase this shortcut!", "0,0", 1, 1},
+ { "http://www.deletetest.com/2.html", "delete",
+ "http://www.deletetest.com/2.html", "0,2",
+ "Erase this shortcut!", "0,0", 1, 1},
+ };
+
+ size_t original_shortcuts_count = provider_->shortcuts_map_.size();
+
+ for (size_t i = 0; i < arraysize(shortcuts_to_test_delete); ++i) {
+ const TestShortcutInfo& cur = shortcuts_to_test_delete[i];
+ const GURL current_url(cur.url);
+ Time visit_time = Time::Now() - TimeDelta::FromDays(cur.days_from_now);
+ ShortcutsProvider::Shortcut shortcut(
+ ASCIIToUTF16(cur.title),
+ current_url,
+ ASCIIToUTF16(cur.contents),
+ provider_->SpansFromString(ASCIIToUTF16(cur.contents_class)),
+ ASCIIToUTF16(cur.description),
+ provider_->SpansFromString(ASCIIToUTF16(cur.description_class)));
+ shortcut.last_access_time = visit_time;
+ provider_->shortcuts_map_.insert(
+ std::pair<string16, ShortcutsProvider::Shortcut>(
+ ASCIIToUTF16(cur.title), shortcut));
+ }
+
+ EXPECT_EQ(original_shortcuts_count + 3, provider_->shortcuts_map_.size());
+ EXPECT_FALSE(provider_->shortcuts_map_.end() ==
+ provider_->shortcuts_map_.find(ASCIIToUTF16("delete")));
+ EXPECT_FALSE(provider_->shortcuts_map_.end() ==
+ provider_->shortcuts_map_.find(ASCIIToUTF16("erase")));
+
+ AutocompleteMatch match(provider_, 1200, true,
+ AutocompleteMatch::HISTORY_TITLE);
+
+ match.destination_url = GURL(shortcuts_to_test_delete[0].url);
+ match.contents = ASCIIToUTF16(shortcuts_to_test_delete[0].contents);
+ match.description = ASCIIToUTF16(shortcuts_to_test_delete[0].description);
+
+ provider_->DeleteMatch(match);
+
+ // |shortcuts_to_test_delete[0]| and |shortcuts_to_test_delete[1]| should be
+ // deleted, but not |shortcuts_to_test_delete[2]| as it has different url.
+ EXPECT_EQ(original_shortcuts_count + 1, provider_->shortcuts_map_.size());
+ EXPECT_FALSE(provider_->shortcuts_map_.end() ==
+ provider_->shortcuts_map_.find(ASCIIToUTF16("delete")));
+ EXPECT_TRUE(provider_->shortcuts_map_.end() ==
+ provider_->shortcuts_map_.find(ASCIIToUTF16("erase")));
+
+ match.destination_url = GURL(shortcuts_to_test_delete[2].url);
+ match.contents = ASCIIToUTF16(shortcuts_to_test_delete[2].contents);
+ match.description = ASCIIToUTF16(shortcuts_to_test_delete[2].description);
+
+ provider_->DeleteMatch(match);
+ EXPECT_EQ(original_shortcuts_count, provider_->shortcuts_map_.size());
+ EXPECT_TRUE(provider_->shortcuts_map_.end() ==
+ provider_->shortcuts_map_.find(ASCIIToUTF16("delete")));
+}
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index 379020a..5fbbf16 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -118,6 +118,8 @@
'browser/autocomplete/keyword_provider.h',
'browser/autocomplete/search_provider.cc',
'browser/autocomplete/search_provider.h',
+ 'browser/autocomplete/shortcuts_provider.cc',
+ 'browser/autocomplete/shortcuts_provider.h',
'browser/autocomplete_history_manager.cc',
'browser/autocomplete_history_manager.h',
'browser/autofill/address.cc',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index 3bb2d8aa..d1da145c 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -1252,6 +1252,7 @@
'browser/autocomplete/history_url_provider_unittest.cc',
'browser/autocomplete/keyword_provider_unittest.cc',
'browser/autocomplete/search_provider_unittest.cc',
+ 'browser/autocomplete/shortcuts_provider_unittest.cc',
'browser/autocomplete_history_manager_unittest.cc',
'browser/autofill/address_field_unittest.cc',
'browser/autofill/address_unittest.cc',
diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc
index 914ed25..db7980ea 100644
--- a/chrome/common/chrome_switches.cc
+++ b/chrome/common/chrome_switches.cc
@@ -480,6 +480,9 @@
// parameter to indicate if the provider should be the default.
const char kEnableSearchProviderApiV2[] = "enable-search-provider-api-v2";
+// Enable the use of the ShortcutsProvider for autocomplete results.
+const char kEnableShortcutsProvider[] = "enable-shortcuts-provider";
+
// On platforms that support it, enable smooth scroll animation.
const char kEnableSmoothScrolling[] = "enable-smooth-scrolling";
diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h
index aa3dcd0..b330eafd 100644
--- a/chrome/common/chrome_switches.h
+++ b/chrome/common/chrome_switches.h
@@ -139,6 +139,7 @@
extern const char kEnableRemoting[];
extern const char kEnableResourceContentSettings[];
extern const char kEnableSearchProviderApiV2[];
+extern const char kEnableShortcutsProvider[];
extern const char kEnableSmoothScrolling[];
extern const char kEnableSSLCachedInfo[];
extern const char kEnableSync[];