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/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" 30 31 namespace omnibox_api = extensions::api::omnibox; 32 33 // Helper functor for Start(), for ending keyword mode unless explicitly told 34 // otherwise. 35 class KeywordProvider::ScopedEndExtensionKeywordMode { 36 public: 37 explicit ScopedEndExtensionKeywordMode(KeywordProvider* provider) 38 : provider_(provider) { } 39 ~ScopedEndExtensionKeywordMode() { 40 if (provider_) 41 provider_->MaybeEndExtensionKeywordMode(); 42 } 43 44 void StayInKeywordMode() { 45 provider_ = NULL; 46 } 47 private: 48 KeywordProvider* provider_; 49 }; 50 51 KeywordProvider::KeywordProvider(AutocompleteProviderListener* listener, 52 Profile* profile) 53 : AutocompleteProvider(listener, profile, 54 AutocompleteProvider::TYPE_KEYWORD), 55 model_(NULL), 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. 60 registrar_.Add(this, 61 chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY, 62 content::Source<Profile>(profile->GetOriginalProfile())); 63 registrar_.Add( 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)); 68 } 69 70 KeywordProvider::KeywordProvider(AutocompleteProviderListener* listener, 71 TemplateURLService* model) 72 : AutocompleteProvider(listener, NULL, AutocompleteProvider::TYPE_KEYWORD), 73 model_(model), 74 current_input_id_(0) { 75 } 76 77 78 namespace { 79 80 // Helper functor for Start(), for sorting keyword matches by quality. 81 class CompareQuality { 82 public: 83 // A keyword is of higher quality when a greater fraction of it has been 84 // typed, that is, when it is shorter. 85 // 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(); 92 } 93 }; 94 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_; 98 99 } // namespace 100 101 // static 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. 112 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) : 117 first_white + 1; 118 119 if (remaining_start < input.length()) 120 remaining_input->assign(input.begin() + remaining_start, input.end()); 121 122 // Return first token as keyword. 123 return input.substr(0, first_white); 124 } 125 126 // static 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); 133 134 // And extract the replacement string. 135 base::string16 remaining_input; 136 SplitKeywordFromInput(trimmed_input, trim_leading_whitespace, 137 &remaining_input); 138 return remaining_input; 139 } 140 141 // static 142 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput( 143 TemplateURLService* model, 144 AutocompleteInput* input) { 145 if (!input->allow_exact_keyword_match()) 146 return NULL; 147 148 base::string16 keyword, remaining_input; 149 if (!ExtractKeywordFromInput(*input, &keyword, &remaining_input)) 150 return NULL; 151 152 DCHECK(model); 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 164 // first character. 165 DCHECK_GE(offset, 0); 166 DCHECK_LE(offset, static_cast<int>(input->text().length())); 167 if (offset <= 0) { 168 // Normalize the cursor to be exactly after the last character. 169 cursor_position = remaining_input.length(); 170 } else { 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; 175 } 176 } 177 input->UpdateText(remaining_input, cursor_position, input->parts()); 178 return template_url; 179 } 180 181 return NULL; 182 } 183 184 base::string16 KeywordProvider::GetKeywordForText( 185 const base::string16& text) const { 186 const base::string16 keyword(TemplateURLService::CleanUserInputKeyword(text)); 187 188 if (keyword.empty()) 189 return keyword; 190 191 TemplateURLService* url_service = GetTemplateURLService(); 192 if (!url_service) 193 return base::string16(); 194 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(); 200 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); 207 if (!extension || 208 (profile_->IsOffTheRecord() && 209 !extension_util::IsIncognitoEnabled(extension->id(), 210 extension_service))) 211 return base::string16(); 212 } 213 214 return keyword; 215 } 216 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); 225 } 226 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); 232 233 matches_.clear(); 234 235 if (!minimal_changes) { 236 done_ = true; 237 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_; 241 } 242 243 // Split user input into a keyword and some query input. 244 // 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! 251 // 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)) 258 return; 259 260 // Get the best matches for this keyword. 261 // 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 265 // don't bother. 266 // 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 269 // themselves. 270 TemplateURLService::TemplateURLVector matches; 271 GetTemplateURLService()->FindMatchingKeywords( 272 keyword, !remaining_input.empty(), &matches); 273 274 for (TemplateURLService::TemplateURLVector::iterator i(matches.begin()); 275 i != matches.end(); ) { 276 const TemplateURL* template_url = *i; 277 278 // Prune any extension keywords that are disallowed in incognito mode (if 279 // we're incognito), or disabled. 280 if (profile_ && 281 (template_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION)) { 282 ExtensionService* service = extensions::ExtensionSystem::Get(profile_)-> 283 extension_service(); 284 const extensions::Extension* extension = 285 service->GetExtensionById(template_url->GetExtensionId(), false); 286 bool enabled = 287 extension && (!profile_->IsOffTheRecord() || 288 extension_util::IsIncognitoEnabled(extension->id(), 289 service)); 290 if (!enabled) { 291 i = matches.erase(i); 292 continue; 293 } 294 } 295 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); 300 continue; 301 } 302 303 ++i; 304 } 305 if (matches.empty()) 306 return; 307 std::sort(matches.begin(), matches.end(), CompareQuality()); 308 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; 317 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) 324 return; 325 326 // TODO(pkasting): We should probably check that if the user explicitly 327 // typed a scheme, that scheme matches the one in |template_url|. 328 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)); 334 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(); 342 } 343 344 extensions::ApplyDefaultSuggestionForExtensionKeyword( 345 profile_, template_url, 346 remaining_input, 347 &matches_[0]); 348 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); 357 } 358 } else if (input.matches_requested() == AutocompleteInput::ALL_MATCHES) { 359 extension_suggest_last_input_ = input; 360 extension_suggest_matches_.clear(); 361 362 bool have_listeners = 363 extensions::ExtensionOmniboxEventRouter::OnInputChanged( 364 profile_, template_url->GetExtensionId(), 365 UTF16ToUTF8(remaining_input), current_input_id_); 366 367 // We only have to wait for suggest results if there are actually 368 // extensions listening for input changes. 369 if (have_listeners) 370 done_ = false; 371 } 372 } 373 } else { 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)); 380 } 381 } 382 } 383 384 void KeywordProvider::Stop(bool clear_cached_results) { 385 done_ = true; 386 MaybeEndExtensionKeywordMode(); 387 } 388 389 KeywordProvider::~KeywordProvider() {} 390 391 // static 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)) 397 return false; 398 399 *keyword = TemplateURLService::CleanUserInputKeyword( 400 SplitKeywordFromInput(input.text(), true, remaining_input)); 401 return !keyword->empty(); 402 } 403 404 // static 405 int KeywordProvider::CalculateRelevance(AutocompleteInput::Type type, 406 bool complete, 407 bool supports_replacement, 408 bool prefer_keyword, 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. 419 if (!complete) 420 return (type == AutocompleteInput::URL) ? 700 : 450; 421 if (!supports_replacement || (allow_exact_keyword_match && prefer_keyword)) 422 return 1500; 423 return (allow_exact_keyword_match && (type == AutocompleteInput::QUERY)) ? 424 1450 : 1100; 425 } 426 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, 433 int relevance) { 434 DCHECK(template_url); 435 const bool supports_replacement = 436 template_url->url_ref().SupportsReplacement(); 437 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()); 443 if (relevance < 0) { 444 relevance = 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()); 451 } 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. 465 466 // Create destination URL and popup entry content by substituting user input 467 // into keyword templates. 468 FillInURLAndContents(remaining_input, template_url, &match); 469 470 match.keyword = keyword; 471 match.transition = content::PAGE_TRANSITION_KEYWORD; 472 473 return match; 474 } 475 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 488 // entered. 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)); 498 } else { 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); 505 } 506 } else { 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(), 520 remaining_input, 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); 526 } 527 } 528 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_; 534 535 switch (type) { 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; 542 return; 543 544 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED: { 545 // It's possible to change the default suggestion while not in an editing 546 // session. 547 base::string16 keyword, remaining_input; 548 if (matches_.empty() || current_keyword_extension_id_.empty() || 549 !ExtractKeywordFromInput(input, &keyword, &remaining_input)) 550 return; 551 552 const TemplateURL* template_url( 553 model->GetTemplateURLForKeyword(keyword)); 554 extensions::ApplyDefaultSuggestionForExtensionKeyword( 555 profile_, template_url, 556 remaining_input, 557 &matches_[0]); 558 listener_->OnProviderUpdate(true); 559 return; 560 } 561 562 case chrome::NOTIFICATION_EXTENSION_OMNIBOX_SUGGESTIONS_READY: { 563 const omnibox_api::SendSuggestions::Params& suggestions = 564 *content::Details< 565 omnibox_api::SendSuggestions::Params>(details).ptr(); 566 if (suggestions.request_id != current_input_id_) 567 return; // This is an old result. Just ignore. 568 569 base::string16 keyword, remaining_input; 570 bool result = ExtractKeywordFromInput(input, &keyword, &remaining_input); 571 DCHECK(result); 572 const TemplateURL* template_url = 573 model->GetTemplateURLForKeyword(keyword); 574 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 590 // interaction. 591 extension_suggest_matches_.push_back(CreateAutocompleteMatch( 592 template_url, input, keyword.length(), 593 UTF8ToUTF16(suggestion.content), false, first_relevance - (i + 1))); 594 595 AutocompleteMatch* match = &extension_suggest_matches_.back(); 596 match->contents.assign(UTF8ToUTF16(suggestion.description)); 597 match->contents_class = 598 extensions::StyleTypesToACMatchClassifications(suggestion); 599 match->description.clear(); 600 match->description_class.clear(); 601 } 602 603 done_ = true; 604 matches_.insert(matches_.end(), extension_suggest_matches_.begin(), 605 extension_suggest_matches_.end()); 606 listener_->OnProviderUpdate(!extension_suggest_matches_.empty()); 607 return; 608 } 609 610 default: 611 NOTREACHED(); 612 return; 613 } 614 } 615 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. 621 DCHECK(service); 622 service->Load(); 623 return service; 624 } 625 626 void KeywordProvider::EnterExtensionKeywordMode( 627 const std::string& extension_id) { 628 DCHECK(current_keyword_extension_id_.empty()); 629 current_keyword_extension_id_ = extension_id; 630 631 extensions::ExtensionOmniboxEventRouter::OnInputStarted( 632 profile_, current_keyword_extension_id_); 633 } 634 635 void KeywordProvider::MaybeEndExtensionKeywordMode() { 636 if (!current_keyword_extension_id_.empty()) { 637 extensions::ExtensionOmniboxEventRouter::OnInputCancelled( 638 profile_, current_keyword_extension_id_); 639 640 current_keyword_extension_id_.clear(); 641 } 642 } 643