blob: 2d2740864c51ab22796fb66b0ac90a798afb2cbf [file] [log] [blame]
// Copyright (c) 2010 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/autocomplete.h"
#include <algorithm>
#include "app/l10n_util.h"
#include "base/basictypes.h"
#include "base/i18n/number_formatting.h"
#include "base/string_util.h"
#include "chrome/browser/autocomplete/history_url_provider.h"
#include "chrome/browser/autocomplete/history_contents_provider.h"
#include "chrome/browser/autocomplete/keyword_provider.h"
#include "chrome/browser/autocomplete/search_provider.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/dom_ui/history_ui.h"
#include "chrome/browser/external_protocol_handler.h"
#include "chrome/browser/net/url_fixer_upper.h"
#include "chrome/browser/pref_service.h"
#include "chrome/browser/profile.h"
#include "chrome/common/notification_service.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "googleurl/src/gurl.h"
#include "googleurl/src/url_canon_ip.h"
#include "googleurl/src/url_util.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "net/base/net_util.h"
#include "net/base/registry_controlled_domain.h"
#include "net/url_request/url_request.h"
using base::TimeDelta;
// AutocompleteInput ----------------------------------------------------------
AutocompleteInput::AutocompleteInput()
: type_(INVALID),
prevent_inline_autocomplete_(false),
prefer_keyword_(false),
synchronous_only_(false) {
}
AutocompleteInput::AutocompleteInput(const std::wstring& text,
const std::wstring& desired_tld,
bool prevent_inline_autocomplete,
bool prefer_keyword,
bool synchronous_only)
: desired_tld_(desired_tld),
prevent_inline_autocomplete_(prevent_inline_autocomplete),
prefer_keyword_(prefer_keyword),
synchronous_only_(synchronous_only) {
// Trim whitespace from edges of input; don't inline autocomplete if there
// was trailing whitespace.
if (TrimWhitespace(text, TRIM_ALL, &text_) & TRIM_TRAILING)
prevent_inline_autocomplete_ = true;
type_ = Parse(text_, desired_tld, &parts_, &scheme_);
if (type_ == INVALID)
return;
if ((type_ == UNKNOWN) || (type_ == REQUESTED_URL) || (type_ == URL)) {
GURL canonicalized_url(URLFixerUpper::FixupURL(WideToUTF8(text_),
WideToUTF8(desired_tld_)));
if (canonicalized_url.is_valid() &&
(!canonicalized_url.IsStandard() || canonicalized_url.SchemeIsFile() ||
!canonicalized_url.host().empty()))
canonicalized_url_ = canonicalized_url;
}
if (type_ == FORCED_QUERY && text_[0] == L'?')
text_.erase(0, 1);
}
AutocompleteInput::~AutocompleteInput() {
}
// static
std::string AutocompleteInput::TypeToString(Type type) {
switch (type) {
case INVALID: return "invalid";
case UNKNOWN: return "unknown";
case REQUESTED_URL: return "requested-url";
case URL: return "url";
case QUERY: return "query";
case FORCED_QUERY: return "forced-query";
default:
NOTREACHED();
return std::string();
}
}
// static
AutocompleteInput::Type AutocompleteInput::Parse(
const std::wstring& text,
const std::wstring& desired_tld,
url_parse::Parsed* parts,
std::wstring* scheme) {
const size_t first_non_white = text.find_first_not_of(kWhitespaceWide, 0);
if (first_non_white == std::wstring::npos)
return INVALID; // All whitespace.
if (text.at(first_non_white) == L'?') {
// If the first non-whitespace character is a '?', we magically treat this
// as a query.
return FORCED_QUERY;
}
// Ask our parsing back-end to help us understand what the user typed. We
// use the URLFixerUpper here because we want to be smart about what we
// consider a scheme. For example, we shouldn't consider www.google.com:80
// to have a scheme.
url_parse::Parsed local_parts;
if (!parts)
parts = &local_parts;
const std::wstring parsed_scheme(URLFixerUpper::SegmentURL(text, parts));
if (scheme)
*scheme = parsed_scheme;
if (parsed_scheme == L"file") {
// A user might or might not type a scheme when entering a file URL.
return URL;
}
// If the user typed a scheme, and it's HTTP or HTTPS, we know how to parse it
// well enough that we can fall through to the heuristics below. If it's
// something else, we can just determine our action based on what we do with
// any input of this scheme. In theory we could do better with some schemes
// (e.g. "ftp" or "view-source") but I'll wait to spend the effort on that
// until I run into some cases that really need it.
if (parts->scheme.is_nonempty() &&
(parsed_scheme != L"http") && (parsed_scheme != L"https")) {
// See if we know how to handle the URL internally.
if (URLRequest::IsHandledProtocol(WideToASCII(parsed_scheme)))
return URL;
// There are also some schemes that we convert to other things before they
// reach the renderer or else the renderer handles internally without
// reaching the URLRequest logic. We thus won't catch these above, but we
// should still claim to handle them.
if (LowerCaseEqualsASCII(parsed_scheme, chrome::kViewSourceScheme) ||
LowerCaseEqualsASCII(parsed_scheme, chrome::kJavaScriptScheme) ||
LowerCaseEqualsASCII(parsed_scheme, chrome::kDataScheme))
return URL;
// Finally, check and see if the user has explicitly opened this scheme as
// a URL before. We need to do this last because some schemes may be in
// here as "blocked" (e.g. "javascript") because we don't want pages to open
// them, but users still can.
switch (ExternalProtocolHandler::GetBlockState(parsed_scheme)) {
case ExternalProtocolHandler::DONT_BLOCK:
return URL;
case ExternalProtocolHandler::BLOCK:
// If we don't want the user to open the URL, don't let it be navigated
// to at all.
return QUERY;
default:
// We don't know about this scheme. It's likely to be a search operator
// like "site:" or "link:". We classify it as UNKNOWN so the user has
// the option of treating it as a URL if we're wrong.
// Note that SegmentURL() is smart so we aren't tricked by "c:\foo" or
// "www.example.com:81" in this case.
return UNKNOWN;
}
}
// Either the user didn't type a scheme, in which case we need to distinguish
// between an HTTP URL and a query, or the scheme is HTTP or HTTPS, in which
// case we should reject invalid formulations.
// If we have an empty host it can't be a URL.
if (!parts->host.is_nonempty())
return QUERY;
// Likewise, the RCDS can reject certain obviously-invalid hosts. (We also
// use the registry length later below.)
const std::wstring host(text.substr(parts->host.begin, parts->host.len));
const size_t registry_length =
net::RegistryControlledDomainService::GetRegistryLength(host, false);
if (registry_length == std::wstring::npos) {
// Try to append the desired_tld.
if (!desired_tld.empty()) {
std::wstring host_with_tld(host);
if (host[host.length() - 1] != '.')
host_with_tld += '.';
host_with_tld += desired_tld;
if (net::RegistryControlledDomainService::GetRegistryLength(
host_with_tld, false) != std::wstring::npos)
return REQUESTED_URL; // Something like "99999999999" that looks like a
// bad IP address, but becomes valid on attaching
// a TLD.
}
return QUERY; // Could be a broken IP address, etc.
}
// See if the hostname is valid. While IE and GURL allow hostnames to contain
// many other characters (perhaps for weird intranet machines), it's extremely
// unlikely that a user would be trying to type those in for anything other
// than a search query.
url_canon::CanonHostInfo host_info;
const std::string canonicalized_host(net::CanonicalizeHost(host, &host_info));
if ((host_info.family == url_canon::CanonHostInfo::NEUTRAL) &&
!net::IsCanonicalizedHostCompliant(canonicalized_host,
WideToUTF8(desired_tld))) {
// Invalid hostname. There are several possible cases:
// * Our checker is too strict and the user pasted in a real-world URL
// that's "invalid" but resolves. To catch these, we return UNKNOWN when
// the user explicitly typed a scheme, so we'll still search by default
// but we'll show the accidental search infobar if necessary.
// * The user is typing a multi-word query. If we see a space anywhere in
// the hostname we assume this is a search and return QUERY.
// * Our checker is too strict and the user is typing a real-world hostname
// that's "invalid" but resolves. We return UNKNOWN if the TLD is known.
// Note that we explicitly excluded hosts with spaces above so that
// "toys at amazon.com" will be treated as a search.
// * The user is typing some garbage string. Return QUERY.
//
// Thus we fall down in the following cases:
// * Trying to navigate to a hostname with spaces
// * Trying to navigate to a hostname with invalid characters and an unknown
// TLD
// These are rare, though probably possible in intranets.
return (parts->scheme.is_nonempty() ||
((registry_length != 0) && (host.find(' ') == std::wstring::npos))) ?
UNKNOWN : QUERY;
}
// Presence of a port means this is likely a URL, if the port is really a port
// number. If it's just garbage after a colon, this is a query.
if (parts->port.is_nonempty()) {
int port;
return (StringToInt(WideToUTF16(
text.substr(parts->port.begin, parts->port.len)), &port) &&
(port >= 0) && (port <= 65535)) ? URL : QUERY;
}
// Presence of a username could either indicate a URL or an email address
// ("user@mail.com"). E-mail addresses are likely queries so we only open
// this as a URL if the user explicitly typed a scheme.
if (parts->username.is_nonempty() && parts->scheme.is_nonempty())
return URL;
// Presence of a password means this is likely a URL. Note that unless the
// user has typed an explicit "http://" or similar, we'll probably think that
// the username is some unknown scheme, and bail out in the scheme-handling
// code above.
if (parts->password.is_nonempty())
return URL;
// See if the host is an IP address.
if (host_info.family == url_canon::CanonHostInfo::IPV4) {
// If the user originally typed a host that looks like an IP address (a
// dotted quad), they probably want to open it. If the original input was
// something else (like a single number), they probably wanted to search for
// it, unless they explicitly typed a scheme. This is true even if the URL
// appears to have a path: "1.2/45" is more likely a search (for the answer
// to a math problem) than a URL.
if ((host_info.num_ipv4_components == 4) || parts->scheme.is_nonempty())
return URL;
return desired_tld.empty() ? UNKNOWN : REQUESTED_URL;
}
if (host_info.family == url_canon::CanonHostInfo::IPV6)
return URL;
// The host doesn't look like a number, so see if the user's given us a path.
if (parts->path.is_nonempty()) {
// Most inputs with paths are URLs, even ones without known registries (e.g.
// intranet URLs). However, if the user didn't type a scheme, there's no
// known registry, and the path has a space, this is more likely a query
// with a slash in the first term (e.g. "ps/2 games") than a URL. We can
// still open URLs with spaces in the path by escaping the space, and we
// will still inline autocomplete them if users have typed them in the past,
// but we default to searching since that's the common case.
return (!parts->scheme.is_nonempty() && (registry_length == 0) &&
(text.substr(parts->path.begin, parts->path.len).find(' ') !=
std::wstring::npos)) ? UNKNOWN : URL;
}
// If we reach here with a username, our input looks like "user@host"; this is
// the case mentioned above, where we think this is more likely an email
// address than an HTTP auth attempt, so search for it.
if (parts->username.is_nonempty())
return UNKNOWN;
// We have a bare host string. See if it has a known TLD or the user typed a
// scheme. If so, it's probably a URL.
if (parts->scheme.is_nonempty() || (registry_length != 0))
return URL;
// No TLD that we know about. This could be:
// * A string that the user wishes to add a desired_tld to to get a URL. If
// we reach this point, we know there's no known TLD on the string, so the
// fixup code will be willing to add one; thus this is a URL.
// * A single word "foo"; possibly an intranet site, but more likely a search.
// This is ideally an UNKNOWN, and we can let the Alternate Nav URL code
// catch our mistakes.
// * A URL with a valid TLD we don't know about yet. If e.g. a registrar adds
// "xxx" as a TLD, then until we add it to our data file, Chrome won't know
// "foo.xxx" is a real URL. So ideally this is a URL, but we can't really
// distinguish this case from:
// * A "URL-like" string that's not really a URL (like
// "browser.tabs.closeButtons" or "java.awt.event.*"). This is ideally a
// QUERY. Since the above case and this one are indistinguishable, and this
// case is likely to be much more common, just say these are both UNKNOWN,
// which should default to the right thing and let users correct us on a
// case-by-case basis.
return desired_tld.empty() ? UNKNOWN : REQUESTED_URL;
}
// static
void AutocompleteInput::ParseForEmphasizeComponents(
const std::wstring& text,
const std::wstring& desired_tld,
url_parse::Component* scheme,
url_parse::Component* host) {
url_parse::Parsed parts;
std::wstring scheme_str;
Parse(text, desired_tld, &parts, &scheme_str);
*scheme = parts.scheme;
*host = parts.host;
int after_scheme_and_colon = parts.scheme.end() + 1;
// For the view-source scheme, we should emphasize the scheme and host of the
// URL qualified by the view-source prefix.
if (LowerCaseEqualsASCII(scheme_str, chrome::kViewSourceScheme) &&
(static_cast<int>(text.length()) > after_scheme_and_colon)) {
// Obtain the URL prefixed by view-source and parse it.
std::wstring real_url(text.substr(after_scheme_and_colon));
url_parse::Parsed real_parts;
AutocompleteInput::Parse(real_url, desired_tld, &real_parts, NULL);
if (real_parts.scheme.is_nonempty() || real_parts.host.is_nonempty()) {
if (real_parts.scheme.is_nonempty()) {
*scheme = url_parse::Component(
after_scheme_and_colon + real_parts.scheme.begin,
real_parts.scheme.len);
} else {
scheme->reset();
}
if (real_parts.host.is_nonempty()) {
*host = url_parse::Component(
after_scheme_and_colon + real_parts.host.begin,
real_parts.host.len);
} else {
host->reset();
}
}
}
}
// static
std::wstring AutocompleteInput::FormattedStringWithEquivalentMeaning(
const GURL& url,
const std::wstring& formatted_url) {
if (!net::CanStripTrailingSlash(url))
return formatted_url;
const std::wstring url_with_path(formatted_url + L"/");
return (AutocompleteInput::Parse(formatted_url, std::wstring(), NULL, NULL) ==
AutocompleteInput::Parse(url_with_path, std::wstring(), NULL, NULL)) ?
formatted_url : url_with_path;
}
bool AutocompleteInput::Equals(const AutocompleteInput& other) const {
return (text_ == other.text_) &&
(type_ == other.type_) &&
(desired_tld_ == other.desired_tld_) &&
(scheme_ == other.scheme_) &&
(prevent_inline_autocomplete_ == other.prevent_inline_autocomplete_) &&
(prefer_keyword_ == other.prefer_keyword_) &&
(synchronous_only_ == other.synchronous_only_);
}
void AutocompleteInput::Clear() {
text_.clear();
type_ = INVALID;
parts_ = url_parse::Parsed();
scheme_.clear();
desired_tld_.clear();
prevent_inline_autocomplete_ = false;
prefer_keyword_ = false;
}
// AutocompleteMatch ----------------------------------------------------------
AutocompleteMatch::AutocompleteMatch()
: provider(NULL),
relevance(0),
deletable(false),
inline_autocomplete_offset(std::wstring::npos),
transition(PageTransition::GENERATED),
is_history_what_you_typed_match(false),
type(SEARCH_WHAT_YOU_TYPED),
template_url(NULL),
starred(false) {
}
AutocompleteMatch::AutocompleteMatch(AutocompleteProvider* provider,
int relevance,
bool deletable,
Type type)
: provider(provider),
relevance(relevance),
deletable(deletable),
inline_autocomplete_offset(std::wstring::npos),
transition(PageTransition::TYPED),
is_history_what_you_typed_match(false),
type(type),
template_url(NULL),
starred(false) {
}
AutocompleteMatch::~AutocompleteMatch() {
}
// static
std::string AutocompleteMatch::TypeToString(Type type) {
const char* strings[NUM_TYPES] = {
"url-what-you-typed",
"history-url",
"history-title",
"history-body",
"history-keyword",
"navsuggest",
"search-what-you-typed",
"search-history",
"search-suggest",
"search-other-engine",
"open-history-page",
};
DCHECK(arraysize(strings) == NUM_TYPES);
return strings[type];
}
// static
int AutocompleteMatch::TypeToIcon(Type type) {
int icons[NUM_TYPES] = {
IDR_OMNIBOX_HTTP,
IDR_OMNIBOX_HTTP,
IDR_OMNIBOX_HISTORY,
IDR_OMNIBOX_HISTORY,
IDR_OMNIBOX_HISTORY,
IDR_OMNIBOX_HTTP,
IDR_OMNIBOX_SEARCH,
IDR_OMNIBOX_SEARCH,
IDR_OMNIBOX_SEARCH,
IDR_OMNIBOX_SEARCH,
IDR_OMNIBOX_MORE,
};
DCHECK(arraysize(icons) == NUM_TYPES);
return icons[type];
}
// static
bool AutocompleteMatch::MoreRelevant(const AutocompleteMatch& elem1,
const AutocompleteMatch& elem2) {
// For equal-relevance matches, we sort alphabetically, so that providers
// who return multiple elements at the same priority get a "stable" sort
// across multiple updates.
if (elem1.relevance == elem2.relevance)
return elem1.contents > elem2.contents;
// A negative relevance indicates the real relevance can be determined by
// negating the value. If both relevances are negative, negate the result
// so that we end up with positive relevances, then negative relevances with
// the negative relevances sorted by absolute values.
const bool result = elem1.relevance > elem2.relevance;
return (elem1.relevance < 0 && elem2.relevance < 0) ? !result : result;
}
// static
bool AutocompleteMatch::DestinationSortFunc(const AutocompleteMatch& elem1,
const AutocompleteMatch& elem2) {
// Sort identical destination_urls together. Place the most relevant matches
// first, so that when we call std::unique(), these are the ones that get
// preserved.
return (elem1.destination_url != elem2.destination_url) ?
(elem1.destination_url < elem2.destination_url) :
MoreRelevant(elem1, elem2);
}
// static
bool AutocompleteMatch::DestinationsEqual(const AutocompleteMatch& elem1,
const AutocompleteMatch& elem2) {
return elem1.destination_url == elem2.destination_url;
}
// static
void AutocompleteMatch::ClassifyMatchInString(
const std::wstring& find_text,
const std::wstring& text,
int style,
ACMatchClassifications* classification) {
ClassifyLocationInString(text.find(find_text), find_text.length(),
text.length(), style, classification);
}
void AutocompleteMatch::ClassifyLocationInString(
size_t match_location,
size_t match_length,
size_t overall_length,
int style,
ACMatchClassifications* classification) {
classification->clear();
// Don't classify anything about an empty string
// (AutocompleteMatch::Validate() checks this).
if (overall_length == 0)
return;
// Mark pre-match portion of string (if any).
if (match_location != 0) {
classification->push_back(ACMatchClassification(0, style));
}
// Mark matching portion of string.
if (match_location == std::wstring::npos) {
// No match, above classification will suffice for whole string.
return;
}
// Classifying an empty match makes no sense and will lead to validation
// errors later.
DCHECK(match_length > 0);
classification->push_back(ACMatchClassification(match_location,
(style | ACMatchClassification::MATCH) & ~ACMatchClassification::DIM));
// Mark post-match portion of string (if any).
const size_t after_match(match_location + match_length);
if (after_match < overall_length) {
classification->push_back(ACMatchClassification(after_match, style));
}
}
#ifndef NDEBUG
void AutocompleteMatch::Validate() const {
ValidateClassifications(contents, contents_class);
ValidateClassifications(description, description_class);
}
void AutocompleteMatch::ValidateClassifications(
const std::wstring& text,
const ACMatchClassifications& classifications) const {
if (text.empty()) {
DCHECK(classifications.size() == 0);
return;
}
// The classifications should always cover the whole string.
DCHECK(classifications.size() > 0) << "No classification for text";
DCHECK(classifications[0].offset == 0) << "Classification misses beginning";
if (classifications.size() == 1)
return;
// The classifications should always be sorted.
size_t last_offset = classifications[0].offset;
for (ACMatchClassifications::const_iterator i(classifications.begin() + 1);
i != classifications.end(); ++i) {
DCHECK(i->offset > last_offset) << "Classification unsorted";
DCHECK(i->offset < text.length()) << "Classification out of bounds";
last_offset = i->offset;
}
}
#endif
// AutocompleteProvider -------------------------------------------------------
// static
const size_t AutocompleteProvider::kMaxMatches = 3;
AutocompleteProvider::ACProviderListener::~ACProviderListener() {
}
AutocompleteProvider::AutocompleteProvider(ACProviderListener* listener,
Profile* profile,
const char* name)
: profile_(profile),
listener_(listener),
done_(true),
name_(name) {
}
void AutocompleteProvider::SetProfile(Profile* profile) {
DCHECK(profile);
DCHECK(done_); // The controller should have already stopped us.
profile_ = profile;
}
void AutocompleteProvider::Stop() {
done_ = true;
}
void AutocompleteProvider::DeleteMatch(const AutocompleteMatch& match) {
}
AutocompleteProvider::~AutocompleteProvider() {
Stop();
}
// static
bool AutocompleteProvider::HasHTTPScheme(const std::wstring& input) {
std::string utf8_input(WideToUTF8(input));
url_parse::Component scheme;
if (url_util::FindAndCompareScheme(utf8_input, chrome::kViewSourceScheme,
&scheme))
utf8_input.erase(0, scheme.end() + 1);
return url_util::FindAndCompareScheme(utf8_input, chrome::kHttpScheme, NULL);
}
void AutocompleteProvider::UpdateStarredStateOfMatches() {
if (matches_.empty())
return;
if (!profile_)
return;
BookmarkModel* bookmark_model = profile_->GetBookmarkModel();
if (!bookmark_model || !bookmark_model->IsLoaded())
return;
for (ACMatches::iterator i = matches_.begin(); i != matches_.end(); ++i)
i->starred = bookmark_model->IsBookmarked(GURL(i->destination_url));
}
std::wstring AutocompleteProvider::StringForURLDisplay(const GURL& url,
bool check_accept_lang,
bool trim_http) const {
std::wstring languages = (check_accept_lang && profile_) ?
UTF8ToWide(profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)) :
std::wstring();
return net::FormatUrl(
url,
languages,
net::kFormatUrlOmitAll & ~(trim_http ? 0 : net::kFormatUrlOmitHTTP),
UnescapeRule::SPACES, NULL, NULL, NULL);
}
// AutocompleteResult ---------------------------------------------------------
// static
const size_t AutocompleteResult::kMaxMatches = 6;
void AutocompleteResult::Selection::Clear() {
destination_url = GURL();
provider_affinity = NULL;
is_history_what_you_typed_match = false;
}
AutocompleteResult::AutocompleteResult() {
// Reserve space for the max number of matches we'll show. The +1 accounts
// for the history shortcut match as it isn't included in max_matches.
matches_.reserve(kMaxMatches + 1);
// It's probably safe to do this in the initializer list, but there's little
// penalty to doing it here and it ensures our object is fully constructed
// before calling member functions.
default_match_ = end();
}
void AutocompleteResult::CopyFrom(const AutocompleteResult& rhs) {
if (this == &rhs)
return;
matches_ = rhs.matches_;
// Careful! You can't just copy iterators from another container, you have to
// reconstruct them.
default_match_ = (rhs.default_match_ == rhs.end()) ?
end() : (begin() + (rhs.default_match_ - rhs.begin()));
alternate_nav_url_ = rhs.alternate_nav_url_;
}
void AutocompleteResult::AppendMatches(const ACMatches& matches) {
std::copy(matches.begin(), matches.end(), std::back_inserter(matches_));
default_match_ = end();
alternate_nav_url_ = GURL();
}
void AutocompleteResult::AddMatch(const AutocompleteMatch& match) {
DCHECK(default_match_ != end());
ACMatches::iterator insertion_point =
std::upper_bound(begin(), end(), match, &AutocompleteMatch::MoreRelevant);
ACMatches::iterator::difference_type default_offset =
default_match_ - begin();
if ((insertion_point - begin()) <= default_offset)
++default_offset;
matches_.insert(insertion_point, match);
default_match_ = begin() + default_offset;
}
void AutocompleteResult::SortAndCull(const AutocompleteInput& input) {
// Remove duplicates.
std::sort(matches_.begin(), matches_.end(),
&AutocompleteMatch::DestinationSortFunc);
matches_.erase(std::unique(matches_.begin(), matches_.end(),
&AutocompleteMatch::DestinationsEqual),
matches_.end());
// Find the top max_matches.
if (matches_.size() > kMaxMatches) {
std::partial_sort(matches_.begin(), matches_.begin() + kMaxMatches,
matches_.end(), &AutocompleteMatch::MoreRelevant);
matches_.erase(matches_.begin() + kMaxMatches, matches_.end());
}
// HistoryContentsProvider uses a negative relevance as a way to avoid
// starving out other provider matches, yet we may end up using this match. To
// make sure such matches are sorted correctly we search for all
// relevances < 0 and negate them. If we change our relevance algorithm to
// properly mix different providers' matches, this can go away.
for (ACMatches::iterator i = matches_.begin(); i != matches_.end(); ++i) {
if (i->relevance < 0)
i->relevance = -i->relevance;
}
// Put the final result set in order.
std::sort(matches_.begin(), matches_.end(), &AutocompleteMatch::MoreRelevant);
default_match_ = begin();
// Set the alternate nav URL.
alternate_nav_url_ = GURL();
if (((input.type() == AutocompleteInput::UNKNOWN) ||
(input.type() == AutocompleteInput::REQUESTED_URL)) &&
(default_match_ != end()) &&
(default_match_->transition != PageTransition::TYPED) &&
(input.canonicalized_url() != default_match_->destination_url))
alternate_nav_url_ = input.canonicalized_url();
}
#ifndef NDEBUG
void AutocompleteResult::Validate() const {
for (const_iterator i(begin()); i != end(); ++i)
i->Validate();
}
#endif
// AutocompleteController -----------------------------------------------------
const int AutocompleteController::kNoItemSelected = -1;
namespace {
// The time we'll wait between sending updates to our observers (balances
// flicker against lag).
const int kUpdateDelayMs = 350;
};
AutocompleteController::AutocompleteController(Profile* profile)
: updated_latest_result_(false),
delay_interval_has_passed_(false),
have_committed_during_this_query_(false),
done_(true) {
providers_.push_back(new SearchProvider(this, profile));
providers_.push_back(new HistoryURLProvider(this, profile));
providers_.push_back(new KeywordProvider(this, profile));
history_contents_provider_ = new HistoryContentsProvider(this, profile);
providers_.push_back(history_contents_provider_);
for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i)
(*i)->AddRef();
}
AutocompleteController::~AutocompleteController() {
// The providers may have tasks outstanding that hold refs to them. We need
// to ensure they won't call us back if they outlive us. (Practically,
// calling Stop() should also cancel those tasks and make it so that we hold
// the only refs.) We also don't want to bother notifying anyone of our
// result changes here, because the notification observer is in the midst of
// shutdown too, so we don't ask Stop() to clear |result_| (and notify).
result_.Reset(); // Not really necessary.
Stop(false);
for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i)
(*i)->Release();
providers_.clear(); // Not really necessary.
}
void AutocompleteController::SetProfile(Profile* profile) {
Stop(true);
for (ACProviders::iterator i(providers_.begin()); i != providers_.end(); ++i)
(*i)->SetProfile(profile);
input_.Clear(); // Ensure we don't try to do a "minimal_changes" query on a
// different profile.
}
void AutocompleteController::Start(const std::wstring& text,
const std::wstring& desired_tld,
bool prevent_inline_autocomplete,
bool prefer_keyword,
bool synchronous_only) {
const std::wstring old_input_text(input_.text());
const bool old_synchronous_only = input_.synchronous_only();
input_ = AutocompleteInput(text, desired_tld, prevent_inline_autocomplete,
prefer_keyword, synchronous_only);
// See if we can avoid rerunning autocomplete when the query hasn't changed
// much. When the user presses or releases the ctrl key, the desired_tld
// changes, and when the user finishes an IME composition, inline autocomplete
// may no longer be prevented. In both these cases the text itself hasn't
// changed since the last query, and some providers can do much less work (and
// get matches back more quickly). Taking advantage of this reduces flicker.
//
// NOTE: This comes after constructing |input_| above since that construction
// can change the text string (e.g. by stripping off a leading '?').
const bool minimal_changes = (input_.text() == old_input_text) &&
(input_.synchronous_only() == old_synchronous_only);
// If we're interrupting an old query, and committing its result won't shrink
// the visible set (which would probably re-expand soon, thus looking very
// flickery), then go ahead and commit what we've got, in order to feel more
// responsive when the user is typing rapidly. In this case it's important
// that we don't update the edit, as the user has already changed its contents
// and anything we might do with it (e.g. inline autocomplete) likely no
// longer applies.
if (!minimal_changes && !done_ && (latest_result_.size() >= result_.size()))
CommitResult(false);
// If the timer is already running, it could fire shortly after starting this
// query, when we're likely to only have the synchronous results back, thus
// almost certainly causing flicker. Reset it, except when we haven't
// committed anything for the past query, in which case the user is typing
// quickly and we need to keep running the timer lest we lag too far behind.
if (have_committed_during_this_query_) {
update_delay_timer_.Stop();
delay_interval_has_passed_ = false;
}
// Start the new query.
have_committed_during_this_query_ = false;
for (ACProviders::iterator i(providers_.begin()); i != providers_.end();
++i) {
(*i)->Start(input_, minimal_changes);
if (synchronous_only)
DCHECK((*i)->done());
}
CheckIfDone();
UpdateLatestResult(true);
}
void AutocompleteController::Stop(bool clear_result) {
for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end();
++i) {
(*i)->Stop();
}
update_delay_timer_.Stop();
updated_latest_result_ = false;
delay_interval_has_passed_ = false;
done_ = true;
if (clear_result && !result_.empty()) {
result_.Reset();
NotificationService::current()->Notify(
NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
Source<AutocompleteController>(this),
Details<const AutocompleteResult>(&result_));
// NOTE: We don't notify AUTOCOMPLETE_CONTROLLER_DEFAULT_MATCH_UPDATED since
// we're trying to only clear the popup, not touch the edit... this is all
// a mess and should be cleaned up :(
}
latest_result_.CopyFrom(result_);
}
void AutocompleteController::DeleteMatch(const AutocompleteMatch& match) {
DCHECK(match.deletable);
match.provider->DeleteMatch(match); // This may synchronously call back to
// OnProviderUpdate().
CommitResult(true); // Ensure any new result gets committed immediately. If
// it was committed already or hasn't been modified, this
// is harmless.
}
void AutocompleteController::CommitIfQueryHasNeverBeenCommitted() {
if (!have_committed_during_this_query_)
CommitResult(true);
}
void AutocompleteController::OnProviderUpdate(bool updated_matches) {
CheckIfDone();
if (updated_matches || done_) {
UpdateLatestResult(false);
return;
}
}
void AutocompleteController::UpdateLatestResult(bool is_synchronous_pass) {
// Add all providers' matches.
latest_result_.Reset();
for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end();
++i)
latest_result_.AppendMatches((*i)->matches());
updated_latest_result_ = true;
// Sort the matches and trim to a small number of "best" matches.
latest_result_.SortAndCull(input_);
if (history_contents_provider_)
AddHistoryContentsShortcut();
#ifndef NDEBUG
latest_result_.Validate();
#endif
if (is_synchronous_pass) {
if (!update_delay_timer_.IsRunning()) {
update_delay_timer_.Start(
TimeDelta::FromMilliseconds(kUpdateDelayMs),
this, &AutocompleteController::DelayTimerFired);
}
NotificationService::current()->Notify(
NotificationType::AUTOCOMPLETE_CONTROLLER_DEFAULT_MATCH_UPDATED,
Source<AutocompleteController>(this),
Details<const AutocompleteResult>(&latest_result_));
}
// If nothing is visible, commit immediately so that the first character the
// user types produces an instant response. If the query has finished and we
// haven't ever committed a result set, commit immediately to minimize lag.
// Otherwise, only commit when it's been at least one delay interval since the
// last commit, to minimize flicker.
if (result_.empty() || (done_ && !have_committed_during_this_query_) ||
delay_interval_has_passed_)
CommitResult(true);
}
void AutocompleteController::DelayTimerFired() {
delay_interval_has_passed_ = true;
CommitResult(true);
}
void AutocompleteController::CommitResult(bool notify_default_match) {
if (done_) {
update_delay_timer_.Stop();
delay_interval_has_passed_ = false;
}
// Don't send update notifications when nothing's actually changed.
if (!updated_latest_result_)
return;
updated_latest_result_ = false;
delay_interval_has_passed_ = false;
have_committed_during_this_query_ = true;
result_.CopyFrom(latest_result_);
NotificationService::current()->Notify(
NotificationType::AUTOCOMPLETE_CONTROLLER_RESULT_UPDATED,
Source<AutocompleteController>(this),
Details<const AutocompleteResult>(&result_));
if (notify_default_match) {
// This notification must be sent after the other so the popup has time to
// update its state before the edit calls into it.
// TODO(pkasting): Eliminate this ordering requirement.
NotificationService::current()->Notify(
NotificationType::AUTOCOMPLETE_CONTROLLER_DEFAULT_MATCH_UPDATED,
Source<AutocompleteController>(this),
Details<const AutocompleteResult>(&result_));
}
if (!done_)
update_delay_timer_.Reset();
}
ACMatches AutocompleteController::GetMatchesNotInLatestResult(
const AutocompleteProvider* provider) const {
DCHECK(provider);
// Determine the set of destination URLs.
std::set<GURL> destination_urls;
for (AutocompleteResult::const_iterator i(latest_result_.begin());
i != latest_result_.end(); ++i)
destination_urls.insert(i->destination_url);
ACMatches matches;
const ACMatches& provider_matches = provider->matches();
for (ACMatches::const_iterator i = provider_matches.begin();
i != provider_matches.end(); ++i) {
if (destination_urls.find(i->destination_url) == destination_urls.end())
matches.push_back(*i);
}
return matches;
}
void AutocompleteController::AddHistoryContentsShortcut() {
DCHECK(history_contents_provider_);
// Only check the history contents provider if the history contents provider
// is done and has matches.
if (!history_contents_provider_->done() ||
!history_contents_provider_->db_match_count()) {
return;
}
if ((history_contents_provider_->db_match_count() <=
(latest_result_.size() + 1)) ||
(history_contents_provider_->db_match_count() == 1)) {
// We only want to add a shortcut if we're not already showing the matches.
ACMatches matches(GetMatchesNotInLatestResult(history_contents_provider_));
if (matches.empty())
return;
if (matches.size() == 1) {
// Only one match not shown, add it. The relevance may be negative,
// which means we need to negate it to get the true relevance.
AutocompleteMatch& match = matches.front();
if (match.relevance < 0)
match.relevance = -match.relevance;
latest_result_.AddMatch(match);
return;
} // else, fall through and add item.
}
AutocompleteMatch match(NULL, 0, false, AutocompleteMatch::OPEN_HISTORY_PAGE);
match.fill_into_edit = input_.text();
// Mark up the text such that the user input text is bold.
size_t keyword_offset = std::wstring::npos; // Offset into match.contents.
if (history_contents_provider_->db_match_count() ==
history_contents_provider_->kMaxMatchCount) {
// History contents searcher has maxed out.
match.contents = l10n_util::GetStringF(IDS_OMNIBOX_RECENT_HISTORY_MANY,
input_.text(),
&keyword_offset);
} else {
// We can report exact matches when there aren't too many.
std::vector<size_t> content_param_offsets;
match.contents = l10n_util::GetStringF(
IDS_OMNIBOX_RECENT_HISTORY,
UTF16ToWide(base::FormatNumber(history_contents_provider_->
db_match_count())),
input_.text(),
&content_param_offsets);
// content_param_offsets is ordered based on supplied params, we expect
// that the second one contains the query (first is the number).
if (content_param_offsets.size() == 2) {
keyword_offset = content_param_offsets[1];
} else {
// See comments on an identical NOTREACHED() in search_provider.cc.
NOTREACHED();
}
}
// NOTE: This comparison succeeds when keyword_offset == std::wstring::npos.
if (keyword_offset > 0) {
match.contents_class.push_back(
ACMatchClassification(0, ACMatchClassification::NONE));
}
match.contents_class.push_back(
ACMatchClassification(keyword_offset, ACMatchClassification::MATCH));
if (keyword_offset + input_.text().size() < match.contents.size()) {
match.contents_class.push_back(
ACMatchClassification(keyword_offset + input_.text().size(),
ACMatchClassification::NONE));
}
match.destination_url =
HistoryUI::GetHistoryURLWithSearchText(WideToUTF16(input_.text()));
match.transition = PageTransition::AUTO_BOOKMARK;
match.provider = history_contents_provider_;
latest_result_.AddMatch(match);
}
void AutocompleteController::CheckIfDone() {
for (ACProviders::const_iterator i(providers_.begin()); i != providers_.end();
++i) {
if (!(*i)->done()) {
done_ = false;
return;
}
}
done_ = true;
}