keyword_provider.cc revision 201ade2fbba22bfb27ae029f4d23fca6ded109a0
1// Copyright (c) 2010 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 "app/l10n_util.h" 11#include "base/string16.h" 12#include "base/utf_string_conversions.h" 13#include "chrome/browser/autocomplete/autocomplete_match.h" 14#include "chrome/browser/extensions/extension_omnibox_api.h" 15#include "chrome/browser/extensions/extensions_service.h" 16#include "chrome/browser/profile.h" 17#include "chrome/browser/search_engines/template_url.h" 18#include "chrome/browser/search_engines/template_url_model.h" 19#include "chrome/common/notification_service.h" 20#include "grit/generated_resources.h" 21#include "net/base/escape.h" 22#include "net/base/net_util.h" 23 24// Helper functor for Start(), for ending keyword mode unless explicitly told 25// otherwise. 26class KeywordProvider::ScopedEndExtensionKeywordMode { 27 public: 28 explicit ScopedEndExtensionKeywordMode(KeywordProvider* provider) 29 : provider_(provider) { } 30 ~ScopedEndExtensionKeywordMode() { 31 if (provider_) 32 provider_->MaybeEndExtensionKeywordMode(); 33 } 34 35 void StayInKeywordMode() { 36 provider_ = NULL; 37 } 38 private: 39 KeywordProvider* provider_; 40}; 41 42// static 43std::wstring KeywordProvider::SplitReplacementStringFromInput( 44 const std::wstring& input) { 45 // The input may contain leading whitespace, strip it. 46 std::wstring trimmed_input; 47 TrimWhitespace(input, TRIM_LEADING, &trimmed_input); 48 49 // And extract the replacement string. 50 std::wstring remaining_input; 51 SplitKeywordFromInput(trimmed_input, &remaining_input); 52 return remaining_input; 53} 54 55KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile) 56 : AutocompleteProvider(listener, profile, "Keyword"), 57 model_(NULL), 58 current_input_id_(0) { 59 // Extension suggestions always come from the original profile, since that's 60 // where extensions run. We use the input ID to distinguish whether the 61 // suggestions are meant for us. 62 registrar_.Add(this, NotificationType::EXTENSION_OMNIBOX_SUGGESTIONS_READY, 63 Source<Profile>(profile->GetOriginalProfile())); 64 registrar_.Add(this, 65 NotificationType::EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED, 66 Source<Profile>(profile->GetOriginalProfile())); 67 registrar_.Add(this, NotificationType::EXTENSION_OMNIBOX_INPUT_ENTERED, 68 Source<Profile>(profile)); 69} 70 71KeywordProvider::KeywordProvider(ACProviderListener* listener, 72 TemplateURLModel* model) 73 : AutocompleteProvider(listener, NULL, "Keyword"), 74 model_(model), 75 current_input_id_(0) { 76} 77 78 79namespace { 80 81// Helper functor for Start(), for sorting keyword matches by quality. 82class CompareQuality { 83 public: 84 // A keyword is of higher quality when a greater fraction of it has been 85 // typed, that is, when it is shorter. 86 // 87 // TODO(pkasting): http://b/740691 Most recent and most frequent keywords are 88 // probably better rankings than the fraction of the keyword typed. We should 89 // always put any exact matches first no matter what, since the code in 90 // Start() assumes this (and it makes sense). 91 bool operator()(const std::wstring& keyword1, 92 const std::wstring& keyword2) const { 93 return keyword1.length() < keyword2.length(); 94 } 95}; 96 97// We need our input IDs to be unique across all profiles, so we keep a global 98// UID that each provider uses. 99static int global_input_uid_; 100 101} // namespace 102 103// static 104const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput( 105 Profile* profile, 106 const AutocompleteInput& input, 107 std::wstring* remaining_input) { 108 if (!input.allow_exact_keyword_match()) 109 return NULL; 110 111 std::wstring keyword; 112 if (!ExtractKeywordFromInput(input, &keyword, remaining_input)) 113 return NULL; 114 115 // Make sure the model is loaded. This is cheap and quickly bails out if 116 // the model is already loaded. 117 TemplateURLModel* model = profile->GetTemplateURLModel(); 118 DCHECK(model); 119 model->Load(); 120 121 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword); 122 return TemplateURL::SupportsReplacement(template_url) ? template_url : NULL; 123} 124 125void KeywordProvider::Start(const AutocompleteInput& input, 126 bool minimal_changes) { 127 // This object ensures we end keyword mode if we exit the function without 128 // toggling keyword mode to on. 129 ScopedEndExtensionKeywordMode keyword_mode_toggle(this); 130 131 matches_.clear(); 132 133 if (!minimal_changes) { 134 done_ = true; 135 136 // Input has changed. Increment the input ID so that we can discard any 137 // stale extension suggestions that may be incoming. 138 current_input_id_ = ++global_input_uid_; 139 } 140 141 // Split user input into a keyword and some query input. 142 // 143 // We want to suggest keywords even when users have started typing URLs, on 144 // the assumption that they might not realize they no longer need to go to a 145 // site to be able to search it. So we call CleanUserInputKeyword() to strip 146 // any initial scheme and/or "www.". NOTE: Any heuristics or UI used to 147 // automatically/manually create keywords will need to be in sync with 148 // whatever we do here! 149 // 150 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for 151 // keywords, we might suggest keywords that haven't even been partially typed, 152 // if the user uses them enough and isn't obviously typing something else. In 153 // this case we'd consider all input here to be query input. 154 std::wstring keyword, remaining_input; 155 if (!ExtractKeywordFromInput(input, &keyword, &remaining_input)) 156 return; 157 158 // Make sure the model is loaded. This is cheap and quickly bails out if 159 // the model is already loaded. 160 TemplateURLModel* model = profile_ ? profile_->GetTemplateURLModel() : model_; 161 DCHECK(model); 162 model->Load(); 163 164 // Get the best matches for this keyword. 165 // 166 // NOTE: We could cache the previous keywords and reuse them here in the 167 // |minimal_changes| case, but since we'd still have to recalculate their 168 // relevances and we can just recreate the results synchronously anyway, we 169 // don't bother. 170 // 171 // TODO(pkasting): http://b/893701 We should remember the user's use of a 172 // search query both from the autocomplete popup and from web pages 173 // themselves. 174 std::vector<std::wstring> keyword_matches; 175 model->FindMatchingKeywords(keyword, !remaining_input.empty(), 176 &keyword_matches); 177 178 // Prune any extension keywords that are disallowed in incognito mode (if 179 // we're incognito), or disabled. 180 for (std::vector<std::wstring>::iterator i(keyword_matches.begin()); 181 i != keyword_matches.end(); ) { 182 const TemplateURL* template_url(model->GetTemplateURLForKeyword(*i)); 183 if (profile_ && 184 !input.synchronous_only() && template_url->IsExtensionKeyword()) { 185 ExtensionsService* service = profile_->GetExtensionsService(); 186 const Extension* extension = service->GetExtensionById( 187 template_url->GetExtensionId(), false); 188 bool enabled = extension && (!profile_->IsOffTheRecord() || 189 service->IsIncognitoEnabled(extension)); 190 if (!enabled) { 191 i = keyword_matches.erase(i); 192 continue; 193 } 194 } 195 ++i; 196 } 197 if (keyword_matches.empty()) 198 return; 199 std::sort(keyword_matches.begin(), keyword_matches.end(), CompareQuality()); 200 201 // Limit to one exact or three inexact matches, and mark them up for display 202 // in the autocomplete popup. 203 // Any exact match is going to be the highest quality match, and thus at the 204 // front of our vector. 205 if (keyword_matches.front() == keyword) { 206 const TemplateURL* template_url(model->GetTemplateURLForKeyword(keyword)); 207 // TODO(pkasting): We should probably check that if the user explicitly 208 // typed a scheme, that scheme matches the one in |template_url|. 209 matches_.push_back(CreateAutocompleteMatch(model, keyword, input, 210 keyword.length(), 211 remaining_input, -1)); 212 213 if (profile_ && 214 !input.synchronous_only() && template_url->IsExtensionKeyword()) { 215 if (template_url->GetExtensionId() != current_keyword_extension_id_) 216 MaybeEndExtensionKeywordMode(); 217 if (current_keyword_extension_id_.empty()) 218 EnterExtensionKeywordMode(template_url->GetExtensionId()); 219 keyword_mode_toggle.StayInKeywordMode(); 220 221 ApplyDefaultSuggestionForExtensionKeyword(profile_, template_url, 222 WideToUTF16(remaining_input), 223 &matches_[0]); 224 225 if (minimal_changes) { 226 // If the input hasn't significantly changed, we can just use the 227 // suggestions from last time. We need to readjust the relevance to 228 // ensure it is less than the main match's relevance. 229 for (size_t i = 0; i < extension_suggest_matches_.size(); ++i) { 230 matches_.push_back(extension_suggest_matches_[i]); 231 matches_.back().relevance = matches_[0].relevance - (i + 1); 232 } 233 } else { 234 extension_suggest_last_input_ = input; 235 extension_suggest_matches_.clear(); 236 237 bool have_listeners = ExtensionOmniboxEventRouter::OnInputChanged( 238 profile_, template_url->GetExtensionId(), 239 WideToUTF8(remaining_input), current_input_id_); 240 241 // We only have to wait for suggest results if there are actually 242 // extensions listening for input changes. 243 if (have_listeners) 244 done_ = false; 245 } 246 } 247 } else { 248 if (keyword_matches.size() > kMaxMatches) { 249 keyword_matches.erase(keyword_matches.begin() + kMaxMatches, 250 keyword_matches.end()); 251 } 252 for (std::vector<std::wstring>::const_iterator i(keyword_matches.begin()); 253 i != keyword_matches.end(); ++i) { 254 matches_.push_back(CreateAutocompleteMatch(model, *i, input, 255 keyword.length(), 256 remaining_input, -1)); 257 } 258 } 259} 260 261void KeywordProvider::Stop() { 262 done_ = true; 263 MaybeEndExtensionKeywordMode(); 264} 265 266KeywordProvider::~KeywordProvider() {} 267 268// static 269bool KeywordProvider::ExtractKeywordFromInput(const AutocompleteInput& input, 270 std::wstring* keyword, 271 std::wstring* remaining_input) { 272 if ((input.type() == AutocompleteInput::INVALID) || 273 (input.type() == AutocompleteInput::FORCED_QUERY)) 274 return false; 275 276 *keyword = TemplateURLModel::CleanUserInputKeyword( 277 SplitKeywordFromInput(input.text(), remaining_input)); 278 return !keyword->empty(); 279} 280 281// static 282std::wstring KeywordProvider::SplitKeywordFromInput( 283 const std::wstring& input, 284 std::wstring* remaining_input) { 285 // Find end of first token. The AutocompleteController has trimmed leading 286 // whitespace, so we need not skip over that. 287 const size_t first_white(input.find_first_of(kWhitespaceWide)); 288 DCHECK_NE(0U, first_white); 289 if (first_white == std::wstring::npos) 290 return input; // Only one token provided. 291 292 // Set |remaining_input| to everything after the first token. 293 DCHECK(remaining_input != NULL); 294 const size_t first_nonwhite(input.find_first_not_of(kWhitespaceWide, 295 first_white)); 296 if (first_nonwhite != std::wstring::npos) 297 remaining_input->assign(input.begin() + first_nonwhite, input.end()); 298 299 // Return first token as keyword. 300 return input.substr(0, first_white); 301} 302 303// static 304void KeywordProvider::FillInURLAndContents( 305 const std::wstring& remaining_input, 306 const TemplateURL* element, 307 AutocompleteMatch* match) { 308 DCHECK(!element->short_name().empty()); 309 DCHECK(element->url()); 310 DCHECK(element->url()->IsValid()); 311 int message_id = element->IsExtensionKeyword() ? 312 IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH; 313 if (remaining_input.empty()) { 314 // Allow extension keyword providers to accept empty string input. This is 315 // useful to allow extensions to do something in the case where no input is 316 // entered. 317 if (element->url()->SupportsReplacement() && 318 !element->IsExtensionKeyword()) { 319 // No query input; return a generic, no-destination placeholder. 320 match->contents.assign(l10n_util::GetStringF(message_id, 321 element->AdjustedShortNameForLocaleDirection(), 322 l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE))); 323 match->contents_class.push_back( 324 ACMatchClassification(0, ACMatchClassification::DIM)); 325 } else { 326 // Keyword that has no replacement text (aka a shorthand for a URL). 327 match->destination_url = GURL(element->url()->url()); 328 match->contents.assign(element->short_name()); 329 AutocompleteMatch::ClassifyLocationInString(0, match->contents.length(), 330 match->contents.length(), ACMatchClassification::NONE, 331 &match->contents_class); 332 } 333 } else { 334 // Create destination URL by escaping user input and substituting into 335 // keyword template URL. The escaping here handles whitespace in user 336 // input, but we rely on later canonicalization functions to do more 337 // fixup to make the URL valid if necessary. 338 DCHECK(element->url()->SupportsReplacement()); 339 match->destination_url = GURL(element->url()->ReplaceSearchTerms( 340 *element, remaining_input, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, 341 std::wstring())); 342 std::vector<size_t> content_param_offsets; 343 match->contents.assign(l10n_util::GetStringF(message_id, 344 element->short_name(), 345 remaining_input, 346 &content_param_offsets)); 347 if (content_param_offsets.size() == 2) { 348 AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1], 349 remaining_input.length(), match->contents.length(), 350 ACMatchClassification::NONE, &match->contents_class); 351 } else { 352 // See comments on an identical NOTREACHED() in search_provider.cc. 353 NOTREACHED(); 354 } 355 } 356} 357 358// static 359int KeywordProvider::CalculateRelevance(AutocompleteInput::Type type, 360 bool complete, 361 bool no_query_text_needed, 362 bool allow_exact_keyword_match) { 363 if (!complete) 364 return (type == AutocompleteInput::URL) ? 700 : 450; 365 if (!allow_exact_keyword_match) 366 return 1100; 367 if (no_query_text_needed) 368 return 1500; 369 return (type == AutocompleteInput::QUERY) ? 1450 : 1100; 370} 371 372AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( 373 TemplateURLModel* model, 374 const std::wstring& keyword, 375 const AutocompleteInput& input, 376 size_t prefix_length, 377 const std::wstring& remaining_input, 378 int relevance) { 379 DCHECK(model); 380 // Get keyword data from data store. 381 const TemplateURL* element(model->GetTemplateURLForKeyword(keyword)); 382 DCHECK(element && element->url()); 383 const bool supports_replacement = element->url()->SupportsReplacement(); 384 385 // Create an edit entry of "[keyword] [remaining input]". This is helpful 386 // even when [remaining input] is empty, as the user can select the popup 387 // choice and immediately begin typing in query input. 388 const bool keyword_complete = (prefix_length == keyword.length()); 389 if (relevance < 0) { 390 relevance = 391 CalculateRelevance(input.type(), keyword_complete, 392 // When the user wants keyword matches to take 393 // preference, score them highly regardless of 394 // whether the input provides query text. 395 input.prefer_keyword() || !supports_replacement, 396 input.allow_exact_keyword_match()); 397 } 398 AutocompleteMatch result(this, relevance, false, 399 supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE : 400 AutocompleteMatch::HISTORY_KEYWORD); 401 result.fill_into_edit.assign(keyword); 402 if (!remaining_input.empty() || !keyword_complete || supports_replacement) 403 result.fill_into_edit.push_back(L' '); 404 result.fill_into_edit.append(remaining_input); 405 // If we wanted to set |result.inline_autocomplete_offset| correctly, we'd 406 // need CleanUserInputKeyword() to return the amount of adjustment it's made 407 // to the user's input. Because right now inexact keyword matches can't score 408 // more highly than a "what you typed" match from one of the other providers, 409 // we just don't bother to do this, and leave inline autocompletion off. 410 result.inline_autocomplete_offset = std::wstring::npos; 411 412 // Create destination URL and popup entry content by substituting user input 413 // into keyword templates. 414 FillInURLAndContents(remaining_input, element, &result); 415 416 if (supports_replacement) 417 result.template_url = element; 418 result.transition = PageTransition::KEYWORD; 419 420 // Create popup entry description based on the keyword name. 421 if (!element->IsExtensionKeyword()) { 422 result.description.assign(l10n_util::GetStringF( 423 IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION, keyword)); 424 static const std::wstring kKeywordDesc( 425 l10n_util::GetString(IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION)); 426 AutocompleteMatch::ClassifyLocationInString(kKeywordDesc.find(L"%s"), 427 prefix_length, 428 result.description.length(), 429 ACMatchClassification::DIM, 430 &result.description_class); 431 } 432 433 return result; 434} 435 436void KeywordProvider::Observe(NotificationType type, 437 const NotificationSource& source, 438 const NotificationDetails& details) { 439 TemplateURLModel* model = profile_ ? profile_->GetTemplateURLModel() : model_; 440 const AutocompleteInput& input = extension_suggest_last_input_; 441 442 switch (type.value) { 443 case NotificationType::EXTENSION_OMNIBOX_INPUT_ENTERED: 444 // Input has been accepted, so we're done with this input session. Ensure 445 // we don't send the OnInputCancelled event. 446 current_keyword_extension_id_.clear(); 447 return; 448 449 case NotificationType::EXTENSION_OMNIBOX_DEFAULT_SUGGESTION_CHANGED: { 450 // It's possible to change the default suggestion while not in an editing 451 // session. 452 std::wstring keyword, remaining_input; 453 if (matches_.empty() || current_keyword_extension_id_.empty() || 454 !ExtractKeywordFromInput(input, &keyword, &remaining_input)) 455 return; 456 457 const TemplateURL* template_url(model->GetTemplateURLForKeyword(keyword)); 458 ApplyDefaultSuggestionForExtensionKeyword(profile_, template_url, 459 WideToUTF16(remaining_input), 460 &matches_[0]); 461 listener_->OnProviderUpdate(true); 462 return; 463 } 464 465 case NotificationType::EXTENSION_OMNIBOX_SUGGESTIONS_READY: { 466 const ExtensionOmniboxSuggestions& suggestions = 467 *Details<ExtensionOmniboxSuggestions>(details).ptr(); 468 if (suggestions.request_id != current_input_id_) 469 return; // This is an old result. Just ignore. 470 471 std::wstring keyword, remaining_input; 472 if (!ExtractKeywordFromInput(input, &keyword, &remaining_input)) { 473 NOTREACHED(); 474 return; 475 } 476 477 // TODO(mpcomplete): consider clamping the number of suggestions to 478 // AutocompleteProvider::kMaxMatches. 479 for (size_t i = 0; i < suggestions.suggestions.size(); ++i) { 480 const ExtensionOmniboxSuggestion& suggestion = 481 suggestions.suggestions[i]; 482 // We want to order these suggestions in descending order, so start with 483 // the relevance of the first result (added synchronously in Start()), 484 // and subtract 1 for each subsequent suggestion from the extension. 485 // We know that |complete| is true, because we wouldn't get results from 486 // the extension unless the full keyword had been typed. 487 int first_relevance = CalculateRelevance(input.type(), true, 488 input.prefer_keyword(), input.allow_exact_keyword_match()); 489 extension_suggest_matches_.push_back(CreateAutocompleteMatch( 490 model, keyword, input, keyword.length(), 491 UTF16ToWide(suggestion.content), first_relevance - (i + 1))); 492 493 AutocompleteMatch* match = &extension_suggest_matches_.back(); 494 match->contents.assign(UTF16ToWide(suggestion.description)); 495 match->contents_class = suggestion.description_styles; 496 match->description.clear(); 497 match->description_class.clear(); 498 } 499 500 done_ = true; 501 matches_.insert(matches_.end(), extension_suggest_matches_.begin(), 502 extension_suggest_matches_.end()); 503 listener_->OnProviderUpdate(!extension_suggest_matches_.empty()); 504 return; 505 } 506 507 default: 508 NOTREACHED(); 509 return; 510 } 511} 512 513void KeywordProvider::EnterExtensionKeywordMode( 514 const std::string& extension_id) { 515 DCHECK(current_keyword_extension_id_.empty()); 516 current_keyword_extension_id_ = extension_id; 517 518 ExtensionOmniboxEventRouter::OnInputStarted( 519 profile_, current_keyword_extension_id_); 520} 521 522void KeywordProvider::MaybeEndExtensionKeywordMode() { 523 if (!current_keyword_extension_id_.empty()) { 524 ExtensionOmniboxEventRouter::OnInputCancelled( 525 profile_, current_keyword_extension_id_); 526 527 current_keyword_extension_id_.clear(); 528 } 529} 530