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_system.h"
19 #include "chrome/browser/extensions/extension_util.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/search_engines/template_url.h"
22 #include "chrome/browser/search_engines/template_url_service.h"
23 #include "chrome/browser/search_engines/template_url_service_factory.h"
24 #include "content/public/browser/notification_details.h"
25 #include "content/public/browser/notification_source.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 !extension_util::IsIncognitoEnabled(extension
->id(),
211 return base::string16();
217 AutocompleteMatch
KeywordProvider::CreateVerbatimMatch(
218 const base::string16
& text
,
219 const base::string16
& keyword
,
220 const AutocompleteInput
& input
) {
221 // A verbatim match is allowed to be the default match.
222 return CreateAutocompleteMatch(
223 GetTemplateURLService()->GetTemplateURLForKeyword(keyword
), input
,
224 keyword
.length(), SplitReplacementStringFromInput(text
, true), true, 0);
227 void KeywordProvider::Start(const AutocompleteInput
& input
,
228 bool minimal_changes
) {
229 // This object ensures we end keyword mode if we exit the function without
230 // toggling keyword mode to on.
231 ScopedEndExtensionKeywordMode
keyword_mode_toggle(this);
235 if (!minimal_changes
) {
238 // Input has changed. Increment the input ID so that we can discard any
239 // stale extension suggestions that may be incoming.
240 current_input_id_
= ++global_input_uid_
;
243 // Split user input into a keyword and some query input.
245 // We want to suggest keywords even when users have started typing URLs, on
246 // the assumption that they might not realize they no longer need to go to a
247 // site to be able to search it. So we call CleanUserInputKeyword() to strip
248 // any initial scheme and/or "www.". NOTE: Any heuristics or UI used to
249 // automatically/manually create keywords will need to be in sync with
250 // whatever we do here!
252 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for
253 // keywords, we might suggest keywords that haven't even been partially typed,
254 // if the user uses them enough and isn't obviously typing something else. In
255 // this case we'd consider all input here to be query input.
256 base::string16 keyword
, remaining_input
;
257 if (!ExtractKeywordFromInput(input
, &keyword
, &remaining_input
))
260 // Get the best matches for this keyword.
262 // NOTE: We could cache the previous keywords and reuse them here in the
263 // |minimal_changes| case, but since we'd still have to recalculate their
264 // relevances and we can just recreate the results synchronously anyway, we
267 // TODO(pkasting): http://b/893701 We should remember the user's use of a
268 // search query both from the autocomplete popup and from web pages
270 TemplateURLService::TemplateURLVector matches
;
271 GetTemplateURLService()->FindMatchingKeywords(
272 keyword
, !remaining_input
.empty(), &matches
);
274 for (TemplateURLService::TemplateURLVector::iterator
i(matches
.begin());
275 i
!= matches
.end(); ) {
276 const TemplateURL
* template_url
= *i
;
278 // Prune any extension keywords that are disallowed in incognito mode (if
279 // we're incognito), or disabled.
281 (template_url
->GetType() == TemplateURL::OMNIBOX_API_EXTENSION
)) {
282 ExtensionService
* service
= extensions::ExtensionSystem::Get(profile_
)->
284 const extensions::Extension
* extension
=
285 service
->GetExtensionById(template_url
->GetExtensionId(), false);
287 extension
&& (!profile_
->IsOffTheRecord() ||
288 extension_util::IsIncognitoEnabled(extension
->id(),
291 i
= matches
.erase(i
);
296 // Prune any substituting keywords if there is no substitution.
297 if (template_url
->SupportsReplacement() && remaining_input
.empty() &&
298 !input
.allow_exact_keyword_match()) {
299 i
= matches
.erase(i
);
307 std::sort(matches
.begin(), matches
.end(), CompareQuality());
309 // Limit to one exact or three inexact matches, and mark them up for display
310 // in the autocomplete popup.
311 // Any exact match is going to be the highest quality match, and thus at the
312 // front of our vector.
313 if (matches
.front()->keyword() == keyword
) {
314 const TemplateURL
* template_url
= matches
.front();
315 const bool is_extension_keyword
=
316 template_url
->GetType() == TemplateURL::OMNIBOX_API_EXTENSION
;
318 // Only create an exact match if |remaining_input| is empty or if
319 // this is an extension keyword. If |remaining_input| is a
320 // non-empty non-extension keyword (i.e., a regular keyword that
321 // supports replacement and that has extra text following it),
322 // then SearchProvider creates the exact (a.k.a. verbatim) match.
323 if (!remaining_input
.empty() && !is_extension_keyword
)
326 // TODO(pkasting): We should probably check that if the user explicitly
327 // typed a scheme, that scheme matches the one in |template_url|.
329 // When creating an exact match (either for the keyword itself, no
330 // remaining query or an extension keyword, possibly with remaining
331 // input), allow the match to be the default match.
332 matches_
.push_back(CreateAutocompleteMatch(
333 template_url
, input
, keyword
.length(), remaining_input
, true, -1));
335 if (profile_
&& is_extension_keyword
) {
336 if (input
.matches_requested() == AutocompleteInput::ALL_MATCHES
) {
337 if (template_url
->GetExtensionId() != current_keyword_extension_id_
)
338 MaybeEndExtensionKeywordMode();
339 if (current_keyword_extension_id_
.empty())
340 EnterExtensionKeywordMode(template_url
->GetExtensionId());
341 keyword_mode_toggle
.StayInKeywordMode();
344 extensions::ApplyDefaultSuggestionForExtensionKeyword(
345 profile_
, template_url
,
349 if (minimal_changes
&&
350 (input
.matches_requested() != AutocompleteInput::BEST_MATCH
)) {
351 // If the input hasn't significantly changed, we can just use the
352 // suggestions from last time. We need to readjust the relevance to
353 // ensure it is less than the main match's relevance.
354 for (size_t i
= 0; i
< extension_suggest_matches_
.size(); ++i
) {
355 matches_
.push_back(extension_suggest_matches_
[i
]);
356 matches_
.back().relevance
= matches_
[0].relevance
- (i
+ 1);
358 } else if (input
.matches_requested() == AutocompleteInput::ALL_MATCHES
) {
359 extension_suggest_last_input_
= input
;
360 extension_suggest_matches_
.clear();
362 bool have_listeners
=
363 extensions::ExtensionOmniboxEventRouter::OnInputChanged(
364 profile_
, template_url
->GetExtensionId(),
365 base::UTF16ToUTF8(remaining_input
), current_input_id_
);
367 // We only have to wait for suggest results if there are actually
368 // extensions listening for input changes.
374 if (matches
.size() > kMaxMatches
)
375 matches
.erase(matches
.begin() + kMaxMatches
, matches
.end());
376 for (TemplateURLService::TemplateURLVector::const_iterator
i(
377 matches
.begin()); i
!= matches
.end(); ++i
) {
378 matches_
.push_back(CreateAutocompleteMatch(
379 *i
, input
, keyword
.length(), remaining_input
, false, -1));
384 void KeywordProvider::Stop(bool clear_cached_results
) {
386 MaybeEndExtensionKeywordMode();
389 KeywordProvider::~KeywordProvider() {}
392 bool KeywordProvider::ExtractKeywordFromInput(const AutocompleteInput
& input
,
393 base::string16
* keyword
,
394 base::string16
* remaining_input
) {
395 if ((input
.type() == AutocompleteInput::INVALID
) ||
396 (input
.type() == AutocompleteInput::FORCED_QUERY
))
399 *keyword
= TemplateURLService::CleanUserInputKeyword(
400 SplitKeywordFromInput(input
.text(), true, remaining_input
));
401 return !keyword
->empty();
405 int KeywordProvider::CalculateRelevance(AutocompleteInput::Type type
,
407 bool supports_replacement
,
409 bool allow_exact_keyword_match
) {
410 // This function is responsible for scoring suggestions of keywords
411 // themselves and the suggestion of the verbatim query on an
412 // extension keyword. SearchProvider::CalculateRelevanceForKeywordVerbatim()
413 // scores verbatim query suggestions for non-extension keywords.
414 // These two functions are currently in sync, but there's no reason
415 // we couldn't decide in the future to score verbatim matches
416 // differently for extension and non-extension keywords. If you
417 // make such a change, however, you should update this comment to
418 // describe it, so it's clear why the functions diverge.
420 return (type
== AutocompleteInput::URL
) ? 700 : 450;
421 if (!supports_replacement
|| (allow_exact_keyword_match
&& prefer_keyword
))
423 return (allow_exact_keyword_match
&& (type
== AutocompleteInput::QUERY
)) ?
427 AutocompleteMatch
KeywordProvider::CreateAutocompleteMatch(
428 const TemplateURL
* template_url
,
429 const AutocompleteInput
& input
,
430 size_t prefix_length
,
431 const base::string16
& remaining_input
,
432 bool allowed_to_be_default_match
,
434 DCHECK(template_url
);
435 const bool supports_replacement
=
436 template_url
->url_ref().SupportsReplacement();
438 // Create an edit entry of "[keyword] [remaining input]". This is helpful
439 // even when [remaining input] is empty, as the user can select the popup
440 // choice and immediately begin typing in query input.
441 const base::string16
& keyword
= template_url
->keyword();
442 const bool keyword_complete
= (prefix_length
== keyword
.length());
445 CalculateRelevance(input
.type(), keyword_complete
,
446 // When the user wants keyword matches to take
447 // preference, score them highly regardless of
448 // whether the input provides query text.
449 supports_replacement
, input
.prefer_keyword(),
450 input
.allow_exact_keyword_match());
452 AutocompleteMatch
match(this, relevance
, false,
453 supports_replacement
? AutocompleteMatchType::SEARCH_OTHER_ENGINE
:
454 AutocompleteMatchType::HISTORY_KEYWORD
);
455 match
.allowed_to_be_default_match
= allowed_to_be_default_match
;
456 match
.fill_into_edit
= keyword
;
457 if (!remaining_input
.empty() || supports_replacement
)
458 match
.fill_into_edit
.push_back(L
' ');
459 match
.fill_into_edit
.append(remaining_input
);
460 // If we wanted to set |result.inline_autocompletion| correctly, we'd need
461 // CleanUserInputKeyword() to return the amount of adjustment it's made to
462 // the user's input. Because right now inexact keyword matches can't score
463 // more highly than a "what you typed" match from one of the other providers,
464 // we just don't bother to do this, and leave inline autocompletion off.
466 // Create destination URL and popup entry content by substituting user input
467 // into keyword templates.
468 FillInURLAndContents(remaining_input
, template_url
, &match
);
470 match
.keyword
= keyword
;
471 match
.transition
= content::PAGE_TRANSITION_KEYWORD
;
476 void KeywordProvider::FillInURLAndContents(
477 const base::string16
& remaining_input
,
478 const TemplateURL
* element
,
479 AutocompleteMatch
* match
) const {
480 DCHECK(!element
->short_name().empty());
481 const TemplateURLRef
& element_ref
= element
->url_ref();
482 DCHECK(element_ref
.IsValid());
483 int message_id
= (element
->GetType() == TemplateURL::OMNIBOX_API_EXTENSION
) ?
484 IDS_EXTENSION_KEYWORD_COMMAND
: IDS_KEYWORD_SEARCH
;
485 if (remaining_input
.empty()) {
486 // Allow extension keyword providers to accept empty string input. This is
487 // useful to allow extensions to do something in the case where no input is
489 if (element_ref
.SupportsReplacement() &&
490 (element
->GetType() != TemplateURL::OMNIBOX_API_EXTENSION
)) {
491 // No query input; return a generic, no-destination placeholder.
492 match
->contents
.assign(
493 l10n_util::GetStringFUTF16(message_id
,
494 element
->AdjustedShortNameForLocaleDirection(),
495 l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE
)));
496 match
->contents_class
.push_back(
497 ACMatchClassification(0, ACMatchClassification::DIM
));
499 // Keyword that has no replacement text (aka a shorthand for a URL).
500 match
->destination_url
= GURL(element
->url());
501 match
->contents
.assign(element
->short_name());
502 AutocompleteMatch::ClassifyLocationInString(0, match
->contents
.length(),
503 match
->contents
.length(), ACMatchClassification::NONE
,
504 &match
->contents_class
);
507 // Create destination URL by escaping user input and substituting into
508 // keyword template URL. The escaping here handles whitespace in user
509 // input, but we rely on later canonicalization functions to do more
510 // fixup to make the URL valid if necessary.
511 DCHECK(element_ref
.SupportsReplacement());
512 TemplateURLRef::SearchTermsArgs
search_terms_args(remaining_input
);
513 search_terms_args
.append_extra_query_params
=
514 element
== GetTemplateURLService()->GetDefaultSearchProvider();
515 match
->destination_url
=
516 GURL(element_ref
.ReplaceSearchTerms(search_terms_args
));
517 std::vector
<size_t> content_param_offsets
;
518 match
->contents
.assign(l10n_util::GetStringFUTF16(message_id
,
519 element
->short_name(),
521 &content_param_offsets
));
522 DCHECK_EQ(2U, content_param_offsets
.size());
523 AutocompleteMatch::ClassifyLocationInString(content_param_offsets
[1],
524 remaining_input
.length(), match
->contents
.length(),
525 ACMatchClassification::NONE
, &match
->contents_class
);
529 void KeywordProvider::Observe(int type
,
530 const content::NotificationSource
& source
,
531 const content::NotificationDetails
& details
) {
532 TemplateURLService
* model
= GetTemplateURLService();
533 const AutocompleteInput
& input
= extension_suggest_last_input_
;
536 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_INPUT_ENTERED
:
537 // Input has been accepted, so we're done with this input session. Ensure
538 // we don't send the OnInputCancelled event, or handle any more stray
539 // suggestions_ready events.
540 current_keyword_extension_id_
.clear();
541 current_input_id_
= 0;
544 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED
: {
545 // It's possible to change the default suggestion while not in an editing
547 base::string16 keyword
, remaining_input
;
548 if (matches_
.empty() || current_keyword_extension_id_
.empty() ||
549 !ExtractKeywordFromInput(input
, &keyword
, &remaining_input
))
552 const TemplateURL
* template_url(
553 model
->GetTemplateURLForKeyword(keyword
));
554 extensions::ApplyDefaultSuggestionForExtensionKeyword(
555 profile_
, template_url
,
558 listener_
->OnProviderUpdate(true);
562 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY
: {
563 const omnibox_api::SendSuggestions::Params
& suggestions
=
565 omnibox_api::SendSuggestions::Params
>(details
).ptr();
566 if (suggestions
.request_id
!= current_input_id_
)
567 return; // This is an old result. Just ignore.
569 base::string16 keyword
, remaining_input
;
570 bool result
= ExtractKeywordFromInput(input
, &keyword
, &remaining_input
);
572 const TemplateURL
* template_url
=
573 model
->GetTemplateURLForKeyword(keyword
);
575 // TODO(mpcomplete): consider clamping the number of suggestions to
576 // AutocompleteProvider::kMaxMatches.
577 for (size_t i
= 0; i
< suggestions
.suggest_results
.size(); ++i
) {
578 const omnibox_api::SuggestResult
& suggestion
=
579 *suggestions
.suggest_results
[i
];
580 // We want to order these suggestions in descending order, so start with
581 // the relevance of the first result (added synchronously in Start()),
582 // and subtract 1 for each subsequent suggestion from the extension.
583 // We recompute the first match's relevance; we know that |complete|
584 // is true, because we wouldn't get results from the extension unless
585 // the full keyword had been typed.
586 int first_relevance
= CalculateRelevance(input
.type(), true, true,
587 input
.prefer_keyword(), input
.allow_exact_keyword_match());
588 // Because these matches are async, we should never let them become the
589 // default match, lest we introduce race conditions in the omnibox user
591 extension_suggest_matches_
.push_back(CreateAutocompleteMatch(
592 template_url
, input
, keyword
.length(),
593 base::UTF8ToUTF16(suggestion
.content
), false,
594 first_relevance
- (i
+ 1)));
596 AutocompleteMatch
* match
= &extension_suggest_matches_
.back();
597 match
->contents
.assign(base::UTF8ToUTF16(suggestion
.description
));
598 match
->contents_class
=
599 extensions::StyleTypesToACMatchClassifications(suggestion
);
600 match
->description
.clear();
601 match
->description_class
.clear();
605 matches_
.insert(matches_
.end(), extension_suggest_matches_
.begin(),
606 extension_suggest_matches_
.end());
607 listener_
->OnProviderUpdate(!extension_suggest_matches_
.empty());
617 TemplateURLService
* KeywordProvider::GetTemplateURLService() const {
618 TemplateURLService
* service
= profile_
?
619 TemplateURLServiceFactory::GetForProfile(profile_
) : model_
;
620 // Make sure the model is loaded. This is cheap and quickly bails out if
621 // the model is already loaded.
627 void KeywordProvider::EnterExtensionKeywordMode(
628 const std::string
& extension_id
) {
629 DCHECK(current_keyword_extension_id_
.empty());
630 current_keyword_extension_id_
= extension_id
;
632 extensions::ExtensionOmniboxEventRouter::OnInputStarted(
633 profile_
, current_keyword_extension_id_
);
636 void KeywordProvider::MaybeEndExtensionKeywordMode() {
637 if (!current_keyword_extension_id_
.empty()) {
638 extensions::ExtensionOmniboxEventRouter::OnInputCancelled(
639 profile_
, current_keyword_extension_id_
);
641 current_keyword_extension_id_
.clear();