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