1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/omnibox/autocomplete_match.h"
7 #include "base/command_line.h"
8 #include "base/i18n/time_formatting.h"
9 #include "base/logging.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/string_piece.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/time/time.h"
16 #include "components/omnibox/autocomplete_provider.h"
17 #include "components/omnibox/omnibox_switches.h"
18 #include "components/omnibox/suggestion_answer.h"
19 #include "components/search_engines/template_url.h"
20 #include "components/search_engines/template_url_service.h"
21 #include "grit/components_scaled_resources.h"
25 bool IsTrivialClassification(const ACMatchClassifications
& classifications
) {
26 return classifications
.empty() ||
27 ((classifications
.size() == 1) &&
28 (classifications
.back().style
== ACMatchClassification::NONE
));
33 // AutocompleteMatch ----------------------------------------------------------
36 const base::char16
AutocompleteMatch::kInvalidChars
[] = {
38 0x2028, // Line separator
39 0x2029, // Paragraph separator
43 AutocompleteMatch::AutocompleteMatch()
48 allowed_to_be_default_match(false),
49 transition(ui::PAGE_TRANSITION_GENERATED
),
50 type(AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED
),
51 from_previous(false) {
54 AutocompleteMatch::AutocompleteMatch(AutocompleteProvider
* provider
,
62 allowed_to_be_default_match(false),
63 transition(ui::PAGE_TRANSITION_TYPED
),
65 from_previous(false) {
68 AutocompleteMatch::AutocompleteMatch(const AutocompleteMatch
& match
)
69 : provider(match
.provider
),
70 relevance(match
.relevance
),
71 typed_count(match
.typed_count
),
72 deletable(match
.deletable
),
73 fill_into_edit(match
.fill_into_edit
),
74 inline_autocompletion(match
.inline_autocompletion
),
75 allowed_to_be_default_match(match
.allowed_to_be_default_match
),
76 destination_url(match
.destination_url
),
77 stripped_destination_url(match
.stripped_destination_url
),
78 contents(match
.contents
),
79 contents_class(match
.contents_class
),
80 description(match
.description
),
81 description_class(match
.description_class
),
82 answer_contents(match
.answer_contents
),
83 answer_type(match
.answer_type
),
84 answer(SuggestionAnswer::copy(match
.answer
.get())),
85 transition(match
.transition
),
87 associated_keyword(match
.associated_keyword
.get() ?
88 new AutocompleteMatch(*match
.associated_keyword
) : NULL
),
89 keyword(match
.keyword
),
90 from_previous(match
.from_previous
),
91 search_terms_args(match
.search_terms_args
.get() ?
92 new TemplateURLRef::SearchTermsArgs(*match
.search_terms_args
) :
94 additional_info(match
.additional_info
),
95 duplicate_matches(match
.duplicate_matches
) {
98 AutocompleteMatch::~AutocompleteMatch() {
101 AutocompleteMatch
& AutocompleteMatch::operator=(
102 const AutocompleteMatch
& match
) {
106 provider
= match
.provider
;
107 relevance
= match
.relevance
;
108 typed_count
= match
.typed_count
;
109 deletable
= match
.deletable
;
110 fill_into_edit
= match
.fill_into_edit
;
111 inline_autocompletion
= match
.inline_autocompletion
;
112 allowed_to_be_default_match
= match
.allowed_to_be_default_match
;
113 destination_url
= match
.destination_url
;
114 stripped_destination_url
= match
.stripped_destination_url
;
115 contents
= match
.contents
;
116 contents_class
= match
.contents_class
;
117 description
= match
.description
;
118 description_class
= match
.description_class
;
119 answer_contents
= match
.answer_contents
;
120 answer_type
= match
.answer_type
;
121 answer
= SuggestionAnswer::copy(match
.answer
.get());
122 transition
= match
.transition
;
124 associated_keyword
.reset(match
.associated_keyword
.get() ?
125 new AutocompleteMatch(*match
.associated_keyword
) : NULL
);
126 keyword
= match
.keyword
;
127 from_previous
= match
.from_previous
;
128 search_terms_args
.reset(match
.search_terms_args
.get() ?
129 new TemplateURLRef::SearchTermsArgs(*match
.search_terms_args
) : NULL
);
130 additional_info
= match
.additional_info
;
131 duplicate_matches
= match
.duplicate_matches
;
136 int AutocompleteMatch::TypeToIcon(Type type
) {
138 static const int kIcons
[] = {
139 IDR_OMNIBOX_HTTP
, // URL_WHAT_YOU_TYPE
140 IDR_OMNIBOX_HTTP
, // HISTORY_URL
141 IDR_OMNIBOX_HTTP
, // HISTORY_TITLE
142 IDR_OMNIBOX_HTTP
, // HISTORY_BODY
143 IDR_OMNIBOX_HTTP
, // HISTORY_KEYWORD
144 IDR_OMNIBOX_HTTP
, // NAVSUGGEST
145 IDR_OMNIBOX_SEARCH
, // SEARCH_WHAT_YOU_TYPED
146 IDR_OMNIBOX_SEARCH
, // SEARCH_HISTORY
147 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST
148 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST_ENTITY
149 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST_TAIL
150 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST_PERSONALIZED
151 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST_PROFILE
152 IDR_OMNIBOX_SEARCH
, // SEARCH_OTHER_ENGINE
153 IDR_OMNIBOX_EXTENSION_APP
, // EXTENSION_APP
154 IDR_OMNIBOX_SEARCH
, // CONTACT_DEPRECATED
155 IDR_OMNIBOX_HTTP
, // BOOKMARK_TITLE
156 IDR_OMNIBOX_HTTP
, // NAVSUGGEST_PERSONALIZED
157 IDR_OMNIBOX_CALCULATOR
, // CALCULATOR
160 static const int kIcons
[] = {
161 IDR_OMNIBOX_HTTP
, // URL_WHAT_YOU_TYPE
162 IDR_OMNIBOX_HISTORY
, // HISTORY_URL
163 IDR_OMNIBOX_HISTORY
, // HISTORY_TITLE
164 IDR_OMNIBOX_HISTORY
, // HISTORY_BODY
165 IDR_OMNIBOX_HISTORY
, // HISTORY_KEYWORD
166 IDR_OMNIBOX_HTTP
, // NAVSUGGEST
167 IDR_OMNIBOX_SEARCH
, // SEARCH_WHAT_YOU_TYPED
168 IDR_OMNIBOX_HISTORY
, // SEARCH_HISTORY
169 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST
170 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST_ENTITY
171 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST_TAIL
172 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST_PERSONALIZED
173 IDR_OMNIBOX_SEARCH
, // SEARCH_SUGGEST_PROFILE
174 IDR_OMNIBOX_SEARCH
, // SEARCH_OTHER_ENGINE
175 IDR_OMNIBOX_EXTENSION_APP
, // EXTENSION_APP
176 IDR_OMNIBOX_SEARCH
, // CONTACT_DEPRECATED
177 IDR_OMNIBOX_HTTP
, // BOOKMARK_TITLE
178 IDR_OMNIBOX_HTTP
, // NAVSUGGEST_PERSONALIZED
179 IDR_OMNIBOX_CALCULATOR
, // CALCULATOR
182 static_assert(arraysize(kIcons
) == AutocompleteMatchType::NUM_TYPES
,
183 "icons array must have NUM_TYPES elements");
188 bool AutocompleteMatch::MoreRelevant(const AutocompleteMatch
& elem1
,
189 const AutocompleteMatch
& elem2
) {
190 // For equal-relevance matches, we sort alphabetically, so that providers
191 // who return multiple elements at the same priority get a "stable" sort
192 // across multiple updates.
193 return (elem1
.relevance
== elem2
.relevance
) ?
194 (elem1
.contents
< elem2
.contents
) : (elem1
.relevance
> elem2
.relevance
);
198 bool AutocompleteMatch::DestinationsEqual(const AutocompleteMatch
& elem1
,
199 const AutocompleteMatch
& elem2
) {
200 if (elem1
.stripped_destination_url
.is_empty() &&
201 elem2
.stripped_destination_url
.is_empty())
203 return elem1
.stripped_destination_url
== elem2
.stripped_destination_url
;
207 void AutocompleteMatch::ClassifyMatchInString(
208 const base::string16
& find_text
,
209 const base::string16
& text
,
211 ACMatchClassifications
* classification
) {
212 ClassifyLocationInString(text
.find(find_text
), find_text
.length(),
213 text
.length(), style
, classification
);
217 void AutocompleteMatch::ClassifyLocationInString(
218 size_t match_location
,
220 size_t overall_length
,
222 ACMatchClassifications
* classification
) {
223 classification
->clear();
225 // Don't classify anything about an empty string
226 // (AutocompleteMatch::Validate() checks this).
227 if (overall_length
== 0)
230 // Mark pre-match portion of string (if any).
231 if (match_location
!= 0) {
232 classification
->push_back(ACMatchClassification(0, style
));
235 // Mark matching portion of string.
236 if (match_location
== base::string16::npos
) {
237 // No match, above classification will suffice for whole string.
240 // Classifying an empty match makes no sense and will lead to validation
242 DCHECK_GT(match_length
, 0U);
243 classification
->push_back(ACMatchClassification(match_location
,
244 (style
| ACMatchClassification::MATCH
) & ~ACMatchClassification::DIM
));
246 // Mark post-match portion of string (if any).
247 const size_t after_match(match_location
+ match_length
);
248 if (after_match
< overall_length
) {
249 classification
->push_back(ACMatchClassification(after_match
, style
));
254 AutocompleteMatch::ACMatchClassifications
255 AutocompleteMatch::MergeClassifications(
256 const ACMatchClassifications
& classifications1
,
257 const ACMatchClassifications
& classifications2
) {
258 // We must return the empty vector only if both inputs are truly empty.
259 // The result of merging an empty vector with a single (0, NONE)
260 // classification is the latter one-entry vector.
261 if (IsTrivialClassification(classifications1
))
262 return classifications2
.empty() ? classifications1
: classifications2
;
263 if (IsTrivialClassification(classifications2
))
264 return classifications1
;
266 ACMatchClassifications output
;
267 for (ACMatchClassifications::const_iterator i
= classifications1
.begin(),
268 j
= classifications2
.begin(); i
!= classifications1
.end();) {
269 AutocompleteMatch::AddLastClassificationIfNecessary(&output
,
270 std::max(i
->offset
, j
->offset
), i
->style
| j
->style
);
271 const size_t next_i_offset
= (i
+ 1) == classifications1
.end() ?
272 static_cast<size_t>(-1) : (i
+ 1)->offset
;
273 const size_t next_j_offset
= (j
+ 1) == classifications2
.end() ?
274 static_cast<size_t>(-1) : (j
+ 1)->offset
;
275 if (next_i_offset
>= next_j_offset
)
277 if (next_j_offset
>= next_i_offset
)
285 std::string
AutocompleteMatch::ClassificationsToString(
286 const ACMatchClassifications
& classifications
) {
287 std::string serialized_classifications
;
288 for (size_t i
= 0; i
< classifications
.size(); ++i
) {
290 serialized_classifications
+= ',';
291 serialized_classifications
+= base::IntToString(classifications
[i
].offset
) +
292 ',' + base::IntToString(classifications
[i
].style
);
294 return serialized_classifications
;
298 ACMatchClassifications
AutocompleteMatch::ClassificationsFromString(
299 const std::string
& serialized_classifications
) {
300 ACMatchClassifications classifications
;
301 std::vector
<std::string
> tokens
;
302 Tokenize(serialized_classifications
, ",", &tokens
);
303 DCHECK(!(tokens
.size() & 1)); // The number of tokens should be even.
304 for (size_t i
= 0; i
< tokens
.size(); i
+= 2) {
305 int classification_offset
= 0;
306 int classification_style
= ACMatchClassification::NONE
;
307 if (!base::StringToInt(tokens
[i
], &classification_offset
) ||
308 !base::StringToInt(tokens
[i
+ 1], &classification_style
)) {
310 return classifications
;
312 classifications
.push_back(ACMatchClassification(classification_offset
,
313 classification_style
));
315 return classifications
;
319 void AutocompleteMatch::AddLastClassificationIfNecessary(
320 ACMatchClassifications
* classifications
,
323 DCHECK(classifications
);
324 if (classifications
->empty() || classifications
->back().style
!= style
) {
325 DCHECK(classifications
->empty() ||
326 (offset
> classifications
->back().offset
));
327 classifications
->push_back(ACMatchClassification(offset
, style
));
332 base::string16
AutocompleteMatch::SanitizeString(const base::string16
& text
) {
333 // NOTE: This logic is mirrored by |sanitizeString()| in
334 // omnibox_custom_bindings.js.
335 base::string16 result
;
336 base::TrimWhitespace(text
, base::TRIM_LEADING
, &result
);
337 base::RemoveChars(result
, kInvalidChars
, &result
);
342 bool AutocompleteMatch::IsSearchType(Type type
) {
343 return type
== AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED
||
344 type
== AutocompleteMatchType::SEARCH_HISTORY
||
345 type
== AutocompleteMatchType::SEARCH_SUGGEST
||
346 type
== AutocompleteMatchType::SEARCH_OTHER_ENGINE
||
347 type
== AutocompleteMatchType::CALCULATOR
||
348 IsSpecializedSearchType(type
);
352 bool AutocompleteMatch::IsSpecializedSearchType(Type type
) {
353 return type
== AutocompleteMatchType::SEARCH_SUGGEST_ENTITY
||
354 type
== AutocompleteMatchType::SEARCH_SUGGEST_TAIL
||
355 type
== AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED
||
356 type
== AutocompleteMatchType::SEARCH_SUGGEST_PROFILE
;
360 TemplateURL
* AutocompleteMatch::GetTemplateURLWithKeyword(
361 TemplateURLService
* template_url_service
,
362 const base::string16
& keyword
,
363 const std::string
& host
) {
364 if (template_url_service
== NULL
)
366 TemplateURL
* template_url
= keyword
.empty() ?
367 NULL
: template_url_service
->GetTemplateURLForKeyword(keyword
);
368 return (template_url
|| host
.empty()) ?
369 template_url
: template_url_service
->GetTemplateURLForHost(host
);
373 GURL
AutocompleteMatch::GURLToStrippedGURL(
375 TemplateURLService
* template_url_service
,
376 const base::string16
& keyword
) {
380 GURL stripped_destination_url
= url
;
382 // If the destination URL looks like it was generated from a TemplateURL,
383 // remove all substitutions other than the search terms. This allows us
384 // to eliminate cases like past search URLs from history that differ only
385 // by some obscure query param from each other or from the search/keyword
387 TemplateURL
* template_url
= GetTemplateURLWithKeyword(
388 template_url_service
, keyword
, stripped_destination_url
.host());
389 if (template_url
!= NULL
&&
390 template_url
->SupportsReplacement(
391 template_url_service
->search_terms_data())) {
392 base::string16 search_terms
;
393 if (template_url
->ExtractSearchTermsFromURL(
394 stripped_destination_url
,
395 template_url_service
->search_terms_data(),
397 stripped_destination_url
=
398 GURL(template_url
->url_ref().ReplaceSearchTerms(
399 TemplateURLRef::SearchTermsArgs(search_terms
),
400 template_url_service
->search_terms_data()));
404 // |replacements| keeps all the substitions we're going to make to
405 // from {destination_url} to {stripped_destination_url}. |need_replacement|
406 // is a helper variable that helps us keep track of whether we need
407 // to apply the replacement.
408 bool needs_replacement
= false;
409 GURL::Replacements replacements
;
411 // Remove the www. prefix from the host.
412 static const char prefix
[] = "www.";
413 static const size_t prefix_len
= arraysize(prefix
) - 1;
414 std::string host
= stripped_destination_url
.host();
415 if (host
.compare(0, prefix_len
, prefix
) == 0) {
416 replacements
.SetHostStr(base::StringPiece(host
).substr(prefix_len
));
417 needs_replacement
= true;
420 // Remove any trailing slash (if it's not a lone slash), or add a slash (to
421 // make a lone slash) if the path is empty. (We can't unconditionally
422 // remove even lone slashes because for some schemes the path must consist
423 // of at least a slash.)
424 const std::string
& path
= stripped_destination_url
.path();
425 if ((path
.length() > 1) && (path
[path
.length() - 1] == '/')) {
426 replacements
.SetPathStr(
427 base::StringPiece(path
).substr(0, path
.length() - 1));
428 needs_replacement
= true;
429 } else if (path
.empty()) {
430 static const char slash
[] = "/";
431 replacements
.SetPathStr(base::StringPiece(slash
));
432 needs_replacement
= true;
435 // Replace https protocol with http protocol.
436 if (stripped_destination_url
.SchemeIs(url::kHttpsScheme
)) {
437 replacements
.SetScheme(url::kHttpScheme
,
438 url::Component(0, strlen(url::kHttpScheme
)));
439 needs_replacement
= true;
442 if (needs_replacement
)
443 stripped_destination_url
= stripped_destination_url
.ReplaceComponents(
445 return stripped_destination_url
;
448 void AutocompleteMatch::ComputeStrippedDestinationURL(
449 TemplateURLService
* template_url_service
) {
450 stripped_destination_url
=
451 GURLToStrippedGURL(destination_url
, template_url_service
, keyword
);
454 void AutocompleteMatch::EnsureUWYTIsAllowedToBeDefault(
455 const GURL
& canonical_input_url
,
456 TemplateURLService
* template_url_service
) {
457 if (!allowed_to_be_default_match
) {
458 const GURL
& stripped_canonical_input_url
=
459 AutocompleteMatch::GURLToStrippedGURL(
460 canonical_input_url
, template_url_service
, base::string16());
461 ComputeStrippedDestinationURL(template_url_service
);
462 allowed_to_be_default_match
=
463 stripped_canonical_input_url
== stripped_destination_url
;
467 void AutocompleteMatch::GetKeywordUIState(
468 TemplateURLService
* template_url_service
,
469 base::string16
* keyword
,
470 bool* is_keyword_hint
) const {
471 *is_keyword_hint
= associated_keyword
.get() != NULL
;
472 keyword
->assign(*is_keyword_hint
? associated_keyword
->keyword
:
473 GetSubstitutingExplicitlyInvokedKeyword(template_url_service
));
476 base::string16
AutocompleteMatch::GetSubstitutingExplicitlyInvokedKeyword(
477 TemplateURLService
* template_url_service
) const {
478 if (transition
!= ui::PAGE_TRANSITION_KEYWORD
||
479 template_url_service
== NULL
) {
480 return base::string16();
483 const TemplateURL
* t_url
= GetTemplateURL(template_url_service
, false);
485 t_url
->SupportsReplacement(
486 template_url_service
->search_terms_data())) ?
487 keyword
: base::string16();
490 TemplateURL
* AutocompleteMatch::GetTemplateURL(
491 TemplateURLService
* template_url_service
,
492 bool allow_fallback_to_destination_host
) const {
493 return GetTemplateURLWithKeyword(
494 template_url_service
, keyword
,
495 allow_fallback_to_destination_host
?
496 destination_url
.host() : std::string());
499 void AutocompleteMatch::RecordAdditionalInfo(const std::string
& property
,
500 const std::string
& value
) {
501 DCHECK(!property
.empty());
502 DCHECK(!value
.empty());
503 additional_info
[property
] = value
;
506 void AutocompleteMatch::RecordAdditionalInfo(const std::string
& property
,
508 RecordAdditionalInfo(property
, base::IntToString(value
));
511 void AutocompleteMatch::RecordAdditionalInfo(const std::string
& property
,
512 const base::Time
& value
) {
513 RecordAdditionalInfo(property
,
515 base::TimeFormatShortDateAndTime(value
)));
518 std::string
AutocompleteMatch::GetAdditionalInfo(
519 const std::string
& property
) const {
520 AdditionalInfo::const_iterator
i(additional_info
.find(property
));
521 return (i
== additional_info
.end()) ? std::string() : i
->second
;
524 bool AutocompleteMatch::IsVerbatimType() const {
525 const bool is_keyword_verbatim_match
=
526 (type
== AutocompleteMatchType::SEARCH_OTHER_ENGINE
&&
528 provider
->type() == AutocompleteProvider::TYPE_SEARCH
);
529 return type
== AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED
||
530 type
== AutocompleteMatchType::URL_WHAT_YOU_TYPED
||
531 is_keyword_verbatim_match
;
534 bool AutocompleteMatch::SupportsDeletion() const {
538 for (ACMatches::const_iterator
it(duplicate_matches
.begin());
539 it
!= duplicate_matches
.end(); ++it
) {
546 void AutocompleteMatch::PossiblySwapContentsAndDescriptionForURLSuggestion(
547 const AutocompleteInput
& input
) {
548 if (!IsSearchType(type
) && !description
.empty() &&
549 base::CommandLine::ForCurrentProcess()->
550 HasSwitch(switches::kEmphasizeTitlesInOmniboxDropdown
) &&
551 ((input
.type() == metrics::OmniboxInputType::QUERY
) ||
552 (input
.type() == metrics::OmniboxInputType::FORCED_QUERY
))) {
553 std::swap(contents
, description
);
554 std::swap(contents_class
, description_class
);
559 void AutocompleteMatch::Validate() const {
560 ValidateClassifications(contents
, contents_class
);
561 ValidateClassifications(description
, description_class
);
564 void AutocompleteMatch::ValidateClassifications(
565 const base::string16
& text
,
566 const ACMatchClassifications
& classifications
) const {
568 DCHECK(classifications
.empty());
572 // The classifications should always cover the whole string.
573 DCHECK(!classifications
.empty()) << "No classification for \"" << text
<< '"';
574 DCHECK_EQ(0U, classifications
[0].offset
)
575 << "Classification misses beginning for \"" << text
<< '"';
576 if (classifications
.size() == 1)
579 // The classifications should always be sorted.
580 size_t last_offset
= classifications
[0].offset
;
581 for (ACMatchClassifications::const_iterator
i(classifications
.begin() + 1);
582 i
!= classifications
.end(); ++i
) {
583 const char* provider_name
= provider
? provider
->GetName() : "None";
584 DCHECK_GT(i
->offset
, last_offset
)
585 << " Classification for \"" << text
<< "\" with offset of " << i
->offset
586 << " is unsorted in relation to last offset of " << last_offset
587 << ". Provider: " << provider_name
<< ".";
588 DCHECK_LT(i
->offset
, text
.length())
589 << " Classification of [" << i
->offset
<< "," << text
.length()
590 << "] is out of bounds for \"" << text
<< "\". Provider: "
591 << provider_name
<< ".";
592 last_offset
= i
->offset
;