1 // Copyright (c) 2012 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 "chrome/browser/autocomplete/keyword_provider.h"
10 #include "base/strings/string16.h"
11 #include "base/strings/string_util.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/autocomplete/autocomplete_match.h"
14 #include "chrome/browser/autocomplete/autocomplete_provider_listener.h"
15 #include "chrome/browser/chrome_notification_types.h"
16 #include "chrome/browser/extensions/api/omnibox/omnibox_api.h"
17 #include "chrome/browser/extensions/extension_service.h"
18 #include "chrome/browser/extensions/extension_util.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/search_engines/template_url.h"
21 #include "chrome/browser/search_engines/template_url_service.h"
22 #include "chrome/browser/search_engines/template_url_service_factory.h"
23 #include "content/public/browser/notification_details.h"
24 #include "content/public/browser/notification_source.h"
25 #include "extensions/browser/extension_system.h"
26 #include "grit/generated_resources.h"
27 #include "net/base/escape.h"
28 #include "net/base/net_util.h"
29 #include "ui/base/l10n/l10n_util.h"
31 namespace omnibox_api = extensions::api::omnibox;
33 // Helper functor for Start(), for ending keyword mode unless explicitly told
35 class KeywordProvider::ScopedEndExtensionKeywordMode {
37 explicit ScopedEndExtensionKeywordMode(KeywordProvider* provider)
38 : provider_(provider) { }
39 ~ScopedEndExtensionKeywordMode() {
41 provider_->MaybeEndExtensionKeywordMode();
44 void StayInKeywordMode() {
48 KeywordProvider* provider_;
51 KeywordProvider::KeywordProvider(AutocompleteProviderListener* listener,
53 : AutocompleteProvider(listener, profile,
54 AutocompleteProvider::TYPE_KEYWORD),
56 current_input_id_(0) {
57 // Extension suggestions always come from the original profile, since that's
58 // where extensions run. We use the input ID to distinguish whether the
59 // suggestions are meant for us.
61 chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY,
62 content::Source<Profile>(profile->GetOriginalProfile()));
64 this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED,
65 content::Source<Profile>(profile->GetOriginalProfile()));
66 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED,
67 content::Source<Profile>(profile));
70 KeywordProvider::KeywordProvider(AutocompleteProviderListener* listener,
71 TemplateURLService* model)
72 : AutocompleteProvider(listener, NULL, AutocompleteProvider::TYPE_KEYWORD),
74 current_input_id_(0) {
80 // Helper functor for Start(), for sorting keyword matches by quality.
81 class CompareQuality {
83 // A keyword is of higher quality when a greater fraction of it has been
84 // typed, that is, when it is shorter.
86 // TODO(pkasting): http://b/740691 Most recent and most frequent keywords are
87 // probably better rankings than the fraction of the keyword typed. We should
88 // always put any exact matches first no matter what, since the code in
89 // Start() assumes this (and it makes sense).
90 bool operator()(const TemplateURL* t_url1, const TemplateURL* t_url2) const {
91 return t_url1->keyword().length() < t_url2->keyword().length();
95 // We need our input IDs to be unique across all profiles, so we keep a global
96 // UID that each provider uses.
97 static int global_input_uid_;
102 base::string16 KeywordProvider::SplitKeywordFromInput(
103 const base::string16& input,
104 bool trim_leading_whitespace,
105 base::string16* remaining_input) {
106 // Find end of first token. The AutocompleteController has trimmed leading
107 // whitespace, so we need not skip over that.
108 const size_t first_white(input.find_first_of(base::kWhitespaceUTF16));
109 DCHECK_NE(0U, first_white);
110 if (first_white == base::string16::npos)
111 return input; // Only one token provided.
113 // Set |remaining_input| to everything after the first token.
114 DCHECK(remaining_input != NULL);
115 const size_t remaining_start = trim_leading_whitespace ?
116 input.find_first_not_of(base::kWhitespaceUTF16, first_white) :
119 if (remaining_start < input.length())
120 remaining_input->assign(input.begin() + remaining_start, input.end());
122 // Return first token as keyword.
123 return input.substr(0, first_white);
127 base::string16 KeywordProvider::SplitReplacementStringFromInput(
128 const base::string16& input,
129 bool trim_leading_whitespace) {
130 // The input may contain leading whitespace, strip it.
131 base::string16 trimmed_input;
132 TrimWhitespace(input, TRIM_LEADING, &trimmed_input);
134 // And extract the replacement string.
135 base::string16 remaining_input;
136 SplitKeywordFromInput(trimmed_input, trim_leading_whitespace,
138 return remaining_input;
142 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput(
143 TemplateURLService* model,
144 AutocompleteInput* input) {
145 if (!input->allow_exact_keyword_match())
148 base::string16 keyword, remaining_input;
149 if (!ExtractKeywordFromInput(*input, &keyword, &remaining_input))
153 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword);
154 if (template_url && template_url->SupportsReplacement()) {
155 // Adjust cursor position iff it was set before, otherwise leave it as is.
156 size_t cursor_position = base::string16::npos;
157 // The adjustment assumes that the keyword was stripped from the beginning
158 // of the original input.
159 if (input->cursor_position() != base::string16::npos &&
160 !remaining_input.empty() &&
161 EndsWith(input->text(), remaining_input, true)) {
162 int offset = input->text().length() - input->cursor_position();
163 // The cursor should never be past the last character or before the
165 DCHECK_GE(offset, 0);
166 DCHECK_LE(offset, static_cast<int>(input->text().length()));
168 // Normalize the cursor to be exactly after the last character.
169 cursor_position = remaining_input.length();
171 // If somehow the cursor was before the remaining text, set it to 0,
172 // otherwise adjust it relative to the remaining text.
173 cursor_position = offset > static_cast<int>(remaining_input.length()) ?
174 0u : remaining_input.length() - offset;
177 input->UpdateText(remaining_input, cursor_position, input->parts());
184 base::string16 KeywordProvider::GetKeywordForText(
185 const base::string16& text) const {
186 const base::string16 keyword(TemplateURLService::CleanUserInputKeyword(text));
191 TemplateURLService* url_service = GetTemplateURLService();
193 return base::string16();
195 // Don't provide a keyword if it doesn't support replacement.
196 const TemplateURL* const template_url =
197 url_service->GetTemplateURLForKeyword(keyword);
198 if (!template_url || !template_url->SupportsReplacement())
199 return base::string16();
201 // Don't provide a keyword for inactive/disabled extension keywords.
202 if (template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION) {
203 ExtensionService* extension_service =
204 extensions::ExtensionSystem::Get(profile_)->extension_service();
205 const extensions::Extension* extension = extension_service->
206 GetExtensionById(template_url->GetExtensionId(), false);
208 (profile_->IsOffTheRecord() &&
209 !extensions::util::IsIncognitoEnabled(extension->id(), profile_)))
210 return base::string16();
216 AutocompleteMatch KeywordProvider::CreateVerbatimMatch(
217 const base::string16& text,
218 const base::string16& keyword,
219 const AutocompleteInput& input) {
220 // A verbatim match is allowed to be the default match.
221 return CreateAutocompleteMatch(
222 GetTemplateURLService()->GetTemplateURLForKeyword(keyword), input,
223 keyword.length(), SplitReplacementStringFromInput(text, true), true, 0);
226 void KeywordProvider::Start(const AutocompleteInput& input,
227 bool minimal_changes) {
228 // This object ensures we end keyword mode if we exit the function without
229 // toggling keyword mode to on.
230 ScopedEndExtensionKeywordMode keyword_mode_toggle(this);
234 if (!minimal_changes) {
237 // Input has changed. Increment the input ID so that we can discard any
238 // stale extension suggestions that may be incoming.
239 current_input_id_ = ++global_input_uid_;
242 // Split user input into a keyword and some query input.
244 // We want to suggest keywords even when users have started typing URLs, on
245 // the assumption that they might not realize they no longer need to go to a
246 // site to be able to search it. So we call CleanUserInputKeyword() to strip
247 // any initial scheme and/or "www.". NOTE: Any heuristics or UI used to
248 // automatically/manually create keywords will need to be in sync with
249 // whatever we do here!
251 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for
252 // keywords, we might suggest keywords that haven't even been partially typed,
253 // if the user uses them enough and isn't obviously typing something else. In
254 // this case we'd consider all input here to be query input.
255 base::string16 keyword, remaining_input;
256 if (!ExtractKeywordFromInput(input, &keyword, &remaining_input))
259 // Get the best matches for this keyword.
261 // NOTE: We could cache the previous keywords and reuse them here in the
262 // |minimal_changes| case, but since we'd still have to recalculate their
263 // relevances and we can just recreate the results synchronously anyway, we
266 // TODO(pkasting): http://b/893701 We should remember the user's use of a
267 // search query both from the autocomplete popup and from web pages
269 TemplateURLService::TemplateURLVector matches;
270 GetTemplateURLService()->FindMatchingKeywords(
271 keyword, !remaining_input.empty(), &matches);
273 for (TemplateURLService::TemplateURLVector::iterator i(matches.begin());
274 i != matches.end(); ) {
275 const TemplateURL* template_url = *i;
277 // Prune any extension keywords that are disallowed in incognito mode (if
278 // we're incognito), or disabled.
280 (template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION)) {
281 ExtensionService* service = extensions::ExtensionSystem::Get(profile_)->
283 const extensions::Extension* extension =
284 service->GetExtensionById(template_url->GetExtensionId(), false);
286 extension && (!profile_->IsOffTheRecord() ||
287 extensions::util::IsIncognitoEnabled(
288 extension->id(), profile_));
290 i = matches.erase(i);
295 // Prune any substituting keywords if there is no substitution.
296 if (template_url->SupportsReplacement() && remaining_input.empty() &&
297 !input.allow_exact_keyword_match()) {
298 i = matches.erase(i);
306 std::sort(matches.begin(), matches.end(), CompareQuality());
308 // Limit to one exact or three inexact matches, and mark them up for display
309 // in the autocomplete popup.
310 // Any exact match is going to be the highest quality match, and thus at the
311 // front of our vector.
312 if (matches.front()->keyword() == keyword) {
313 const TemplateURL* template_url = matches.front();
314 const bool is_extension_keyword =
315 template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION;
317 // Only create an exact match if |remaining_input| is empty or if
318 // this is an extension keyword. If |remaining_input| is a
319 // non-empty non-extension keyword (i.e., a regular keyword that
320 // supports replacement and that has extra text following it),
321 // then SearchProvider creates the exact (a.k.a. verbatim) match.
322 if (!remaining_input.empty() && !is_extension_keyword)
325 // TODO(pkasting): We should probably check that if the user explicitly
326 // typed a scheme, that scheme matches the one in |template_url|.
328 // When creating an exact match (either for the keyword itself, no
329 // remaining query or an extension keyword, possibly with remaining
330 // input), allow the match to be the default match.
331 matches_.push_back(CreateAutocompleteMatch(
332 template_url, input, keyword.length(), remaining_input, true, -1));
334 if (profile_ && is_extension_keyword) {
335 if (input.matches_requested() == AutocompleteInput::ALL_MATCHES) {
336 if (template_url->GetExtensionId() != current_keyword_extension_id_)
337 MaybeEndExtensionKeywordMode();
338 if (current_keyword_extension_id_.empty())
339 EnterExtensionKeywordMode(template_url->GetExtensionId());
340 keyword_mode_toggle.StayInKeywordMode();
343 extensions::ApplyDefaultSuggestionForExtensionKeyword(
344 profile_, template_url,
348 if (minimal_changes &&
349 (input.matches_requested() != AutocompleteInput::BEST_MATCH)) {
350 // If the input hasn't significantly changed, we can just use the
351 // suggestions from last time. We need to readjust the relevance to
352 // ensure it is less than the main match's relevance.
353 for (size_t i = 0; i < extension_suggest_matches_.size(); ++i) {
354 matches_.push_back(extension_suggest_matches_[i]);
355 matches_.back().relevance = matches_[0].relevance - (i + 1);
357 } else if (input.matches_requested() == AutocompleteInput::ALL_MATCHES) {
358 extension_suggest_last_input_ = input;
359 extension_suggest_matches_.clear();
361 bool have_listeners =
362 extensions::ExtensionOmniboxEventRouter::OnInputChanged(
363 profile_, template_url->GetExtensionId(),
364 base::UTF16ToUTF8(remaining_input), current_input_id_);
366 // We only have to wait for suggest results if there are actually
367 // extensions listening for input changes.
373 if (matches.size() > kMaxMatches)
374 matches.erase(matches.begin() + kMaxMatches, matches.end());
375 for (TemplateURLService::TemplateURLVector::const_iterator i(
376 matches.begin()); i != matches.end(); ++i) {
377 matches_.push_back(CreateAutocompleteMatch(
378 *i, input, keyword.length(), remaining_input, false, -1));
383 void KeywordProvider::Stop(bool clear_cached_results) {
385 MaybeEndExtensionKeywordMode();
388 KeywordProvider::~KeywordProvider() {}
391 bool KeywordProvider::ExtractKeywordFromInput(const AutocompleteInput& input,
392 base::string16* keyword,
393 base::string16* remaining_input) {
394 if ((input.type() == AutocompleteInput::INVALID) ||
395 (input.type() == AutocompleteInput::FORCED_QUERY))
398 *keyword = TemplateURLService::CleanUserInputKeyword(
399 SplitKeywordFromInput(input.text(), true, remaining_input));
400 return !keyword->empty();
404 int KeywordProvider::CalculateRelevance(AutocompleteInput::Type type,
406 bool supports_replacement,
408 bool allow_exact_keyword_match) {
409 // This function is responsible for scoring suggestions of keywords
410 // themselves and the suggestion of the verbatim query on an
411 // extension keyword. SearchProvider::CalculateRelevanceForKeywordVerbatim()
412 // scores verbatim query suggestions for non-extension keywords.
413 // These two functions are currently in sync, but there's no reason
414 // we couldn't decide in the future to score verbatim matches
415 // differently for extension and non-extension keywords. If you
416 // make such a change, however, you should update this comment to
417 // describe it, so it's clear why the functions diverge.
419 return (type == AutocompleteInput::URL) ? 700 : 450;
420 if (!supports_replacement || (allow_exact_keyword_match && prefer_keyword))
422 return (allow_exact_keyword_match && (type == AutocompleteInput::QUERY)) ?
426 AutocompleteMatch KeywordProvider::CreateAutocompleteMatch(
427 const TemplateURL* template_url,
428 const AutocompleteInput& input,
429 size_t prefix_length,
430 const base::string16& remaining_input,
431 bool allowed_to_be_default_match,
433 DCHECK(template_url);
434 const bool supports_replacement =
435 template_url->url_ref().SupportsReplacement();
437 // Create an edit entry of "[keyword] [remaining input]". This is helpful
438 // even when [remaining input] is empty, as the user can select the popup
439 // choice and immediately begin typing in query input.
440 const base::string16& keyword = template_url->keyword();
441 const bool keyword_complete = (prefix_length == keyword.length());
444 CalculateRelevance(input.type(), keyword_complete,
445 // When the user wants keyword matches to take
446 // preference, score them highly regardless of
447 // whether the input provides query text.
448 supports_replacement, input.prefer_keyword(),
449 input.allow_exact_keyword_match());
451 AutocompleteMatch match(this, relevance, false,
452 supports_replacement ? AutocompleteMatchType::SEARCH_OTHER_ENGINE :
453 AutocompleteMatchType::HISTORY_KEYWORD);
454 match.allowed_to_be_default_match = allowed_to_be_default_match;
455 match.fill_into_edit = keyword;
456 if (!remaining_input.empty() || supports_replacement)
457 match.fill_into_edit.push_back(L' ');
458 match.fill_into_edit.append(remaining_input);
459 // If we wanted to set |result.inline_autocompletion| correctly, we'd need
460 // CleanUserInputKeyword() to return the amount of adjustment it's made to
461 // the user's input. Because right now inexact keyword matches can't score
462 // more highly than a "what you typed" match from one of the other providers,
463 // we just don't bother to do this, and leave inline autocompletion off.
465 // Create destination URL and popup entry content by substituting user input
466 // into keyword templates.
467 FillInURLAndContents(remaining_input, template_url, &match);
469 match.keyword = keyword;
470 match.transition = content::PAGE_TRANSITION_KEYWORD;
475 void KeywordProvider::FillInURLAndContents(
476 const base::string16& remaining_input,
477 const TemplateURL* element,
478 AutocompleteMatch* match) const {
479 DCHECK(!element->short_name().empty());
480 const TemplateURLRef& element_ref = element->url_ref();
481 DCHECK(element_ref.IsValid());
482 int message_id = (element->GetType() == TemplateURL::OMNIBOX_API_EXTENSION) ?
483 IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH;
484 if (remaining_input.empty()) {
485 // Allow extension keyword providers to accept empty string input. This is
486 // useful to allow extensions to do something in the case where no input is
488 if (element_ref.SupportsReplacement() &&
489 (element->GetType() != TemplateURL::OMNIBOX_API_EXTENSION)) {
490 // No query input; return a generic, no-destination placeholder.
491 match->contents.assign(
492 l10n_util::GetStringFUTF16(message_id,
493 element->AdjustedShortNameForLocaleDirection(),
494 l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE)));
495 match->contents_class.push_back(
496 ACMatchClassification(0, ACMatchClassification::DIM));
498 // Keyword that has no replacement text (aka a shorthand for a URL).
499 match->destination_url = GURL(element->url());
500 match->contents.assign(element->short_name());
501 AutocompleteMatch::ClassifyLocationInString(0, match->contents.length(),
502 match->contents.length(), ACMatchClassification::NONE,
503 &match->contents_class);
506 // Create destination URL by escaping user input and substituting into
507 // keyword template URL. The escaping here handles whitespace in user
508 // input, but we rely on later canonicalization functions to do more
509 // fixup to make the URL valid if necessary.
510 DCHECK(element_ref.SupportsReplacement());
511 TemplateURLRef::SearchTermsArgs search_terms_args(remaining_input);
512 search_terms_args.append_extra_query_params =
513 element == GetTemplateURLService()->GetDefaultSearchProvider();
514 match->destination_url =
515 GURL(element_ref.ReplaceSearchTerms(search_terms_args));
516 std::vector<size_t> content_param_offsets;
517 match->contents.assign(l10n_util::GetStringFUTF16(message_id,
518 element->short_name(),
520 &content_param_offsets));
521 DCHECK_EQ(2U, content_param_offsets.size());
522 AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1],
523 remaining_input.length(), match->contents.length(),
524 ACMatchClassification::NONE, &match->contents_class);
528 void KeywordProvider::Observe(int type,
529 const content::NotificationSource& source,
530 const content::NotificationDetails& details) {
531 TemplateURLService* model = GetTemplateURLService();
532 const AutocompleteInput& input = extension_suggest_last_input_;
535 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED:
536 // Input has been accepted, so we're done with this input session. Ensure
537 // we don't send the OnInputCancelled event, or handle any more stray
538 // suggestions_ready events.
539 current_keyword_extension_id_.clear();
540 current_input_id_ = 0;
543 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED: {
544 // It's possible to change the default suggestion while not in an editing
546 base::string16 keyword, remaining_input;
547 if (matches_.empty() || current_keyword_extension_id_.empty() ||
548 !ExtractKeywordFromInput(input, &keyword, &remaining_input))
551 const TemplateURL* template_url(
552 model->GetTemplateURLForKeyword(keyword));
553 extensions::ApplyDefaultSuggestionForExtensionKeyword(
554 profile_, template_url,
557 listener_->OnProviderUpdate(true);
561 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY: {
562 const omnibox_api::SendSuggestions::Params& suggestions =
564 omnibox_api::SendSuggestions::Params>(details).ptr();
565 if (suggestions.request_id != current_input_id_)
566 return; // This is an old result. Just ignore.
568 base::string16 keyword, remaining_input;
569 bool result = ExtractKeywordFromInput(input, &keyword, &remaining_input);
571 const TemplateURL* template_url =
572 model->GetTemplateURLForKeyword(keyword);
574 // TODO(mpcomplete): consider clamping the number of suggestions to
575 // AutocompleteProvider::kMaxMatches.
576 for (size_t i = 0; i < suggestions.suggest_results.size(); ++i) {
577 const omnibox_api::SuggestResult& suggestion =
578 *suggestions.suggest_results[i];
579 // We want to order these suggestions in descending order, so start with
580 // the relevance of the first result (added synchronously in Start()),
581 // and subtract 1 for each subsequent suggestion from the extension.
582 // We recompute the first match's relevance; we know that |complete|
583 // is true, because we wouldn't get results from the extension unless
584 // the full keyword had been typed.
585 int first_relevance = CalculateRelevance(input.type(), true, true,
586 input.prefer_keyword(), input.allow_exact_keyword_match());
587 // Because these matches are async, we should never let them become the
588 // default match, lest we introduce race conditions in the omnibox user
590 extension_suggest_matches_.push_back(CreateAutocompleteMatch(
591 template_url, input, keyword.length(),
592 base::UTF8ToUTF16(suggestion.content), false,
593 first_relevance - (i + 1)));
595 AutocompleteMatch* match = &extension_suggest_matches_.back();
596 match->contents.assign(base::UTF8ToUTF16(suggestion.description));
597 match->contents_class =
598 extensions::StyleTypesToACMatchClassifications(suggestion);
599 match->description.clear();
600 match->description_class.clear();
604 matches_.insert(matches_.end(), extension_suggest_matches_.begin(),
605 extension_suggest_matches_.end());
606 listener_->OnProviderUpdate(!extension_suggest_matches_.empty());
616 TemplateURLService* KeywordProvider::GetTemplateURLService() const {
617 TemplateURLService* service = profile_ ?
618 TemplateURLServiceFactory::GetForProfile(profile_) : model_;
619 // Make sure the model is loaded. This is cheap and quickly bails out if
620 // the model is already loaded.
626 void KeywordProvider::EnterExtensionKeywordMode(
627 const std::string& extension_id) {
628 DCHECK(current_keyword_extension_id_.empty());
629 current_keyword_extension_id_ = extension_id;
631 extensions::ExtensionOmniboxEventRouter::OnInputStarted(
632 profile_, current_keyword_extension_id_);
635 void KeywordProvider::MaybeEndExtensionKeywordMode() {
636 if (!current_keyword_extension_id_.empty()) {
637 extensions::ExtensionOmniboxEventRouter::OnInputCancelled(
638 profile_, current_keyword_extension_id_);
640 current_keyword_extension_id_.clear();