search_provider.cc revision c407dc5cd9bdc5668497f21b26b09d988ab439de
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/search_provider.h" 6 7#include <algorithm> 8 9#include "app/l10n_util.h" 10#include "base/callback.h" 11#include "base/i18n/icu_string_conversions.h" 12#include "base/message_loop.h" 13#include "base/utf_string_conversions.h" 14#include "chrome/browser/autocomplete/keyword_provider.h" 15#include "chrome/browser/browser_process.h" 16#include "chrome/browser/google_util.h" 17#include "chrome/browser/history/history.h" 18#include "chrome/browser/net/url_fixer_upper.h" 19#include "chrome/browser/pref_service.h" 20#include "chrome/browser/profile.h" 21#include "chrome/browser/search_engines/template_url_model.h" 22#include "chrome/common/json_value_serializer.h" 23#include "chrome/common/pref_names.h" 24#include "chrome/common/url_constants.h" 25#include "googleurl/src/url_util.h" 26#include "grit/generated_resources.h" 27#include "net/base/escape.h" 28#include "net/http/http_response_headers.h" 29#include "net/url_request/url_request_status.h" 30 31using base::Time; 32using base::TimeDelta; 33 34// static 35const int SearchProvider::kDefaultProviderURLFetcherID = 1; 36// static 37const int SearchProvider::kKeywordProviderURLFetcherID = 2; 38 39// static 40bool SearchProvider::query_suggest_immediately_ = false; 41 42void SearchProvider::Providers::Set(const TemplateURL* default_provider, 43 const TemplateURL* keyword_provider) { 44 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy 45 // this. Nor should we need |default_provider_| and |keyword_provider_| 46 // just to know whether the provider changed. 47 default_provider_ = default_provider; 48 if (default_provider) 49 cached_default_provider_ = *default_provider; 50 keyword_provider_ = keyword_provider; 51 if (keyword_provider) 52 cached_keyword_provider_ = *keyword_provider; 53} 54 55void SearchProvider::Start(const AutocompleteInput& input, 56 bool minimal_changes) { 57 matches_.clear(); 58 59 // Can't return search/suggest results for bogus input or without a profile. 60 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) { 61 Stop(); 62 return; 63 } 64 65 keyword_input_text_.clear(); 66 const TemplateURL* keyword_provider = 67 KeywordProvider::GetSubstitutingTemplateURLForInput(profile_, input, 68 &keyword_input_text_); 69 if (!TemplateURL::SupportsReplacement(keyword_provider) || 70 keyword_input_text_.empty()) { 71 keyword_provider = NULL; 72 } 73 74 const TemplateURL* default_provider = 75 profile_->GetTemplateURLModel()->GetDefaultSearchProvider(); 76 if (!TemplateURL::SupportsReplacement(default_provider)) 77 default_provider = NULL; 78 79 if (keyword_provider == default_provider) 80 keyword_provider = NULL; // No use in querying the same provider twice. 81 82 if (!default_provider && !keyword_provider) { 83 // No valid providers. 84 Stop(); 85 return; 86 } 87 88 // If we're still running an old query but have since changed the query text 89 // or the providers, abort the query. 90 if (!done_ && (!minimal_changes || 91 !providers_.equals(default_provider, keyword_provider))) { 92 Stop(); 93 } 94 95 providers_.Set(default_provider, keyword_provider); 96 97 if (input.text().empty()) { 98 // User typed "?" alone. Give them a placeholder result indicating what 99 // this syntax does. 100 if (default_provider) { 101 AutocompleteMatch match; 102 match.provider = this; 103 match.contents.assign(l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE)); 104 match.contents_class.push_back( 105 ACMatchClassification(0, ACMatchClassification::NONE)); 106 match.description.assign(l10n_util::GetStringF( 107 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION, 108 default_provider->AdjustedShortNameForLocaleDirection())); 109 match.description_class.push_back( 110 ACMatchClassification(0, ACMatchClassification::DIM)); 111 matches_.push_back(match); 112 } 113 Stop(); 114 return; 115 } 116 117 input_ = input; 118 119 StartOrStopHistoryQuery(minimal_changes); 120 StartOrStopSuggestQuery(minimal_changes); 121 ConvertResultsToAutocompleteMatches(); 122} 123 124void SearchProvider::Run() { 125 // Start a new request with the current input. 126 DCHECK(!done_); 127 suggest_results_pending_ = 0; 128 if (providers_.valid_suggest_for_keyword_provider()) { 129 suggest_results_pending_++; 130 keyword_fetcher_.reset( 131 CreateSuggestFetcher(kKeywordProviderURLFetcherID, 132 providers_.keyword_provider(), 133 keyword_input_text_)); 134 } 135 if (providers_.valid_suggest_for_default_provider()) { 136 suggest_results_pending_++; 137 default_fetcher_.reset( 138 CreateSuggestFetcher(kDefaultProviderURLFetcherID, 139 providers_.default_provider(), input_.text())); 140 } 141 // We should only get here if we have a suggest url for the keyword or default 142 // providers. 143 DCHECK_GT(suggest_results_pending_, 0); 144} 145 146void SearchProvider::Stop() { 147 StopHistory(); 148 StopSuggest(); 149 done_ = true; 150} 151 152void SearchProvider::OnURLFetchComplete(const URLFetcher* source, 153 const GURL& url, 154 const URLRequestStatus& status, 155 int response_code, 156 const ResponseCookies& cookie, 157 const std::string& data) { 158 DCHECK(!done_); 159 suggest_results_pending_--; 160 DCHECK_GE(suggest_results_pending_, 0); // Should never go negative. 161 const net::HttpResponseHeaders* const response_headers = 162 source->response_headers(); 163 std::string json_data(data); 164 // JSON is supposed to be UTF-8, but some suggest service providers send JSON 165 // files in non-UTF-8 encodings. The actual encoding is usually specified in 166 // the Content-Type header field. 167 if (response_headers) { 168 std::string charset; 169 if (response_headers->GetCharset(&charset)) { 170 std::wstring wide_data; 171 // TODO(jungshik): Switch to CodePageToUTF8 after it's added. 172 if (base::CodepageToWide(data, charset.c_str(), 173 base::OnStringConversionError::FAIL, 174 &wide_data)) 175 json_data = WideToUTF8(wide_data); 176 } 177 } 178 179 bool is_keyword_results = (source == keyword_fetcher_.get()); 180 SuggestResults* suggest_results = is_keyword_results ? 181 &keyword_suggest_results_ : &default_suggest_results_; 182 183 if (status.is_success() && response_code == 200) { 184 JSONStringValueSerializer deserializer(json_data); 185 deserializer.set_allow_trailing_comma(true); 186 scoped_ptr<Value> root_val(deserializer.Deserialize(NULL, NULL)); 187 const std::wstring& input_text = 188 is_keyword_results ? keyword_input_text_ : input_.text(); 189 have_suggest_results_ = 190 root_val.get() && 191 ParseSuggestResults(root_val.get(), is_keyword_results, input_text, 192 suggest_results); 193 } 194 195 ConvertResultsToAutocompleteMatches(); 196 listener_->OnProviderUpdate(!suggest_results->empty()); 197} 198 199void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes) { 200 // For the minimal_changes case, if we finished the previous query and still 201 // have its results, or are allowed to keep running it, just do that, rather 202 // than starting a new query. 203 if (minimal_changes && 204 (have_history_results_ || (!done_ && !input_.synchronous_only()))) 205 return; 206 207 // We can't keep running any previous query, so halt it. 208 StopHistory(); 209 210 // We can't start a new query if we're only allowed synchronous results. 211 if (input_.synchronous_only()) 212 return; 213 214 // Request history for both the keyword and default provider. 215 if (providers_.valid_keyword_provider()) { 216 ScheduleHistoryQuery(providers_.keyword_provider().id(), 217 keyword_input_text_); 218 } 219 if (providers_.valid_default_provider()) { 220 ScheduleHistoryQuery(providers_.default_provider().id(), 221 input_.text()); 222 } 223} 224 225void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) { 226 // Don't send any queries to the server until some time has elapsed after 227 // the last keypress, to avoid flooding the server with requests we are 228 // likely to end up throwing away anyway. 229 static const int kQueryDelayMs = 200; 230 231 if (!IsQuerySuitableForSuggest()) { 232 StopSuggest(); 233 return; 234 } 235 236 // For the minimal_changes case, if we finished the previous query and still 237 // have its results, or are allowed to keep running it, just do that, rather 238 // than starting a new query. 239 if (minimal_changes && 240 (have_suggest_results_ || (!done_ && !input_.synchronous_only()))) 241 return; 242 243 // We can't keep running any previous query, so halt it. 244 StopSuggest(); 245 246 // We can't start a new query if we're only allowed synchronous results. 247 if (input_.synchronous_only()) 248 return; 249 250 // We'll have at least one pending fetch. Set it to 1 now, but the value is 251 // correctly set in Run. As Run isn't invoked immediately we need to set this 252 // now, else we won't think we're waiting on results from the server when we 253 // really are. 254 suggest_results_pending_ = 1; 255 256 // Kick off a timer that will start the URL fetch if it completes before 257 // the user types another character. 258 int delay = query_suggest_immediately_ ? 0 : kQueryDelayMs; 259 timer_.Start(TimeDelta::FromMilliseconds(delay), this, &SearchProvider::Run); 260} 261 262bool SearchProvider::IsQuerySuitableForSuggest() const { 263 // Don't run Suggest when off the record, the engine doesn't support it, or 264 // the user has disabled it. 265 if (profile_->IsOffTheRecord() || 266 (!providers_.valid_suggest_for_keyword_provider() && 267 !providers_.valid_suggest_for_default_provider()) || 268 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled)) 269 return false; 270 271 // If the input type is URL, we take extra care so that private data in URL 272 // isn't sent to the server. 273 if (input_.type() == AutocompleteInput::URL) { 274 // Don't query the server for URLs that aren't http/https/ftp. Sending 275 // things like file: and data: is both a waste of time and a disclosure of 276 // potentially private, local data. 277 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") && 278 (input_.scheme() != L"ftp")) 279 return false; 280 281 // Don't leak private data in URL 282 const url_parse::Parsed& parts = input_.parts(); 283 284 // Don't send URLs with usernames, queries or refs. Some of these are 285 // private, and the Suggest server is unlikely to have any useful results 286 // for any of them. 287 // Password is optional and may be omitted. Checking username is 288 // sufficient. 289 if (parts.username.is_nonempty() || parts.query.is_nonempty() || 290 parts.ref.is_nonempty()) 291 return false; 292 // Don't send anything for https except hostname and port number. 293 // Hostname and port number are OK because they are visible when TCP 294 // connection is established and the Suggest server may provide some 295 // useful completed URL. 296 if (input_.scheme() == L"https" && parts.path.is_nonempty()) 297 return false; 298 } 299 300 return true; 301} 302 303void SearchProvider::StopHistory() { 304 history_request_consumer_.CancelAllRequests(); 305 history_request_pending_ = false; 306 keyword_history_results_.clear(); 307 default_history_results_.clear(); 308 have_history_results_ = false; 309} 310 311void SearchProvider::StopSuggest() { 312 suggest_results_pending_ = 0; 313 timer_.Stop(); 314 // Stop any in-progress URL fetches. 315 keyword_fetcher_.reset(); 316 default_fetcher_.reset(); 317 keyword_suggest_results_.clear(); 318 default_suggest_results_.clear(); 319 keyword_navigation_results_.clear(); 320 default_navigation_results_.clear(); 321 have_suggest_results_ = false; 322} 323 324void SearchProvider::ScheduleHistoryQuery(TemplateURL::IDType search_id, 325 const std::wstring& text) { 326 DCHECK(!text.empty()); 327 HistoryService* const history_service = 328 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); 329 HistoryService::Handle request_handle = 330 history_service->GetMostRecentKeywordSearchTerms( 331 search_id, WideToUTF16(text), static_cast<int>(kMaxMatches), 332 &history_request_consumer_, 333 NewCallback(this, 334 &SearchProvider::OnGotMostRecentKeywordSearchTerms)); 335 history_request_consumer_.SetClientData(history_service, request_handle, 336 search_id); 337 history_request_pending_ = true; 338} 339 340void SearchProvider::OnGotMostRecentKeywordSearchTerms( 341 CancelableRequestProvider::Handle handle, 342 HistoryResults* results) { 343 HistoryService* history_service = 344 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); 345 DCHECK(history_service); 346 if (providers_.valid_keyword_provider() && 347 (providers_.keyword_provider().id() == 348 history_request_consumer_.GetClientData(history_service, handle))) { 349 keyword_history_results_ = *results; 350 } else { 351 default_history_results_ = *results; 352 } 353 354 if (history_request_consumer_.PendingRequestCount() == 1) { 355 // Requests are removed AFTER the callback is invoked. If the count == 1, 356 // it means no more history requests are pending. 357 history_request_pending_ = false; 358 have_history_results_ = true; 359 } 360 361 ConvertResultsToAutocompleteMatches(); 362 listener_->OnProviderUpdate(!results->empty()); 363} 364 365URLFetcher* SearchProvider::CreateSuggestFetcher(int id, 366 const TemplateURL& provider, 367 const std::wstring& text) { 368 const TemplateURLRef* const suggestions_url = provider.suggestions_url(); 369 DCHECK(suggestions_url->SupportsReplacement()); 370 URLFetcher* fetcher = URLFetcher::Create(id, 371 GURL(suggestions_url->ReplaceSearchTerms( 372 provider, text, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, 373 std::wstring())), 374 URLFetcher::GET, this); 375 fetcher->set_request_context(profile_->GetRequestContext()); 376 fetcher->Start(); 377 return fetcher; 378} 379 380bool SearchProvider::ParseSuggestResults(Value* root_val, 381 bool is_keyword, 382 const std::wstring& input_text, 383 SuggestResults* suggest_results) { 384 if (!root_val->IsType(Value::TYPE_LIST)) 385 return false; 386 ListValue* root_list = static_cast<ListValue*>(root_val); 387 388 Value* query_val; 389 std::wstring query_str; 390 Value* result_val; 391 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) || 392 !query_val->GetAsString(&query_str) || (query_str != input_text) || 393 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST)) 394 return false; 395 396 ListValue* description_list = NULL; 397 if (root_list->GetSize() > 2) { 398 // 3rd element: Description list. 399 Value* description_val; 400 if (root_list->Get(2, &description_val) && 401 description_val->IsType(Value::TYPE_LIST)) 402 description_list = static_cast<ListValue*>(description_val); 403 } 404 405 // We don't care about the query URL list (the fourth element in the 406 // response) for now. 407 408 // Parse optional data in the results from the Suggest server if any. 409 ListValue* type_list = NULL; 410 // 5th argument: Optional key-value pairs. 411 // TODO: We may iterate the 5th+ arguments of the root_list if any other 412 // optional data are defined. 413 if (root_list->GetSize() > 4) { 414 Value* optional_val; 415 if (root_list->Get(4, &optional_val) && 416 optional_val->IsType(Value::TYPE_DICTIONARY)) { 417 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val); 418 419 // Parse Google Suggest specific type extension. 420 static const std::wstring kGoogleSuggestType(L"google:suggesttype"); 421 if (dict_val->HasKey(kGoogleSuggestType)) 422 dict_val->GetList(kGoogleSuggestType, &type_list); 423 } 424 } 425 426 ListValue* result_list = static_cast<ListValue*>(result_val); 427 for (size_t i = 0; i < result_list->GetSize(); ++i) { 428 Value* suggestion_val; 429 std::wstring suggestion_str; 430 if (!result_list->Get(i, &suggestion_val) || 431 !suggestion_val->GetAsString(&suggestion_str)) 432 return false; 433 434 Value* type_val; 435 std::wstring type_str; 436 if (type_list && type_list->Get(i, &type_val) && 437 type_val->GetAsString(&type_str) && (type_str == L"NAVIGATION")) { 438 Value* site_val; 439 std::wstring site_name; 440 NavigationResults& navigation_results = 441 is_keyword ? keyword_navigation_results_ : 442 default_navigation_results_; 443 if ((navigation_results.size() < kMaxMatches) && 444 description_list && description_list->Get(i, &site_val) && 445 site_val->IsType(Value::TYPE_STRING) && 446 site_val->GetAsString(&site_name)) { 447 // We can't blindly trust the URL coming from the server to be valid. 448 GURL result_url(URLFixerUpper::FixupURL(WideToUTF8(suggestion_str), 449 std::string())); 450 if (result_url.is_valid()) 451 navigation_results.push_back(NavigationResult(result_url, site_name)); 452 } 453 } else { 454 // TODO(kochi): Currently we treat a calculator result as a query, but it 455 // is better to have better presentation for caluculator results. 456 if (suggest_results->size() < kMaxMatches) 457 suggest_results->push_back(suggestion_str); 458 } 459 } 460 461 return true; 462} 463 464void SearchProvider::ConvertResultsToAutocompleteMatches() { 465 // Convert all the results to matches and add them to a map, so we can keep 466 // the most relevant match for each result. 467 MatchMap map; 468 const Time no_time; 469 int did_not_accept_keyword_suggestion = keyword_suggest_results_.empty() ? 470 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : 471 TemplateURLRef::NO_SUGGESTION_CHOSEN; 472 // Keyword what you typed results are handled by the KeywordProvider. 473 474 int did_not_accept_default_suggestion = default_suggest_results_.empty() ? 475 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : 476 TemplateURLRef::NO_SUGGESTION_CHOSEN; 477 if (providers_.valid_default_provider()) { 478 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(), 479 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED, 480 did_not_accept_default_suggestion, false, &map); 481 } 482 483 AddHistoryResultsToMap(keyword_history_results_, true, 484 did_not_accept_keyword_suggestion, &map); 485 AddHistoryResultsToMap(default_history_results_, false, 486 did_not_accept_default_suggestion, &map); 487 488 AddSuggestResultsToMap(keyword_suggest_results_, true, 489 did_not_accept_keyword_suggestion, &map); 490 AddSuggestResultsToMap(default_suggest_results_, false, 491 did_not_accept_default_suggestion, &map); 492 493 // Now add the most relevant matches from the map to |matches_|. 494 matches_.clear(); 495 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i) 496 matches_.push_back(i->second); 497 498 AddNavigationResultsToMatches(keyword_navigation_results_, true); 499 AddNavigationResultsToMatches(default_navigation_results_, false); 500 501 const size_t max_total_matches = kMaxMatches + 1; // 1 for "what you typed" 502 std::partial_sort(matches_.begin(), 503 matches_.begin() + std::min(max_total_matches, matches_.size()), 504 matches_.end(), &AutocompleteMatch::MoreRelevant); 505 if (matches_.size() > max_total_matches) 506 matches_.erase(matches_.begin() + max_total_matches, matches_.end()); 507 508 UpdateStarredStateOfMatches(); 509 510 // We're done when both asynchronous subcomponents have finished. We can't 511 // use CancelableRequestConsumer.HasPendingRequests() for history requests 512 // here. A pending request is not cleared until after the completion 513 // callback has returned, but we've reached here from inside that callback. 514 // HasPendingRequests() would therefore return true, and if this is the last 515 // thing left to calculate for this query, we'll never mark the query "done". 516 done_ = !history_request_pending_ && !suggest_results_pending_; 517} 518 519void SearchProvider::AddNavigationResultsToMatches( 520 const NavigationResults& navigation_results, 521 bool is_keyword) { 522 if (!navigation_results.empty()) { 523 // TODO(kochi): http://b/1170574 We add only one results for navigational 524 // suggestions. If we can get more useful information about the score, 525 // consider adding more results. 526 const size_t num_results = is_keyword ? 527 keyword_navigation_results_.size() : default_navigation_results_.size(); 528 matches_.push_back(NavigationToMatch(navigation_results.front(), 529 CalculateRelevanceForNavigation(num_results, 0, is_keyword), 530 is_keyword)); 531 } 532} 533 534void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results, 535 bool is_keyword, 536 int did_not_accept_suggestion, 537 MatchMap* map) { 538 for (HistoryResults::const_iterator i(results.begin()); i != results.end(); 539 ++i) { 540 AddMatchToMap(UTF16ToWide(i->term), 541 CalculateRelevanceForHistory(i->time, is_keyword), 542 AutocompleteMatch::SEARCH_HISTORY, did_not_accept_suggestion, 543 is_keyword, map); 544 } 545} 546 547void SearchProvider::AddSuggestResultsToMap( 548 const SuggestResults& suggest_results, 549 bool is_keyword, 550 int did_not_accept_suggestion, 551 MatchMap* map) { 552 for (size_t i = 0; i < suggest_results.size(); ++i) { 553 AddMatchToMap(suggest_results[i], 554 CalculateRelevanceForSuggestion(suggest_results.size(), i, 555 is_keyword), 556 AutocompleteMatch::SEARCH_SUGGEST, 557 static_cast<int>(i), is_keyword, map); 558 } 559} 560 561int SearchProvider::CalculateRelevanceForWhatYouTyped() const { 562 if (providers_.valid_keyword_provider()) 563 return 250; 564 565 switch (input_.type()) { 566 case AutocompleteInput::UNKNOWN: 567 case AutocompleteInput::QUERY: 568 case AutocompleteInput::FORCED_QUERY: 569 return 1300; 570 571 case AutocompleteInput::REQUESTED_URL: 572 return 1150; 573 574 case AutocompleteInput::URL: 575 return 850; 576 577 default: 578 NOTREACHED(); 579 return 0; 580 } 581} 582 583int SearchProvider::CalculateRelevanceForHistory(const Time& time, 584 bool is_keyword) const { 585 // The relevance of past searches falls off over time. This curve is chosen 586 // so that the relevance of a search 15 minutes ago is discounted about 50 587 // points, while the relevance of a search two weeks ago is discounted about 588 // 450 points. 589 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.); 590 const int score_discount = static_cast<int>(6.5 * pow(elapsed_time, 0.3)); 591 592 // Don't let scores go below 0. Negative relevance scores are meaningful in 593 // a different way. 594 int base_score; 595 if (!providers_.is_primary_provider(is_keyword)) 596 base_score = 200; 597 else 598 base_score = (input_.type() == AutocompleteInput::URL) ? 750 : 1050; 599 return std::max(0, base_score - score_discount); 600} 601 602int SearchProvider::CalculateRelevanceForSuggestion(size_t num_results, 603 size_t result_number, 604 bool is_keyword) const { 605 DCHECK(result_number < num_results); 606 int base_score; 607 if (!providers_.is_primary_provider(is_keyword)) 608 base_score = 100; 609 else 610 base_score = (input_.type() == AutocompleteInput::URL) ? 300 : 600; 611 return base_score + 612 static_cast<int>(num_results - 1 - result_number); 613} 614 615int SearchProvider::CalculateRelevanceForNavigation(size_t num_results, 616 size_t result_number, 617 bool is_keyword) const { 618 DCHECK(result_number < num_results); 619 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest 620 // server if possible. 621 return (providers_.is_primary_provider(is_keyword) ? 800 : 150) + 622 static_cast<int>(num_results - 1 - result_number); 623} 624 625void SearchProvider::AddMatchToMap(const std::wstring& query_string, 626 int relevance, 627 AutocompleteMatch::Type type, 628 int accepted_suggestion, 629 bool is_keyword, 630 MatchMap* map) { 631 const std::wstring& input_text = 632 is_keyword ? keyword_input_text_ : input_.text(); 633 AutocompleteMatch match(this, relevance, false, type); 634 std::vector<size_t> content_param_offsets; 635 const TemplateURL& provider = is_keyword ? providers_.keyword_provider() : 636 providers_.default_provider(); 637 // We do intra-string highlighting for suggestions - the suggested segment 638 // will be highlighted, e.g. for input_text = "you" the suggestion may be 639 // "youtube", so we'll bold the "tube" section: you*tube*. 640 if (input_text != query_string) { 641 match.contents.assign(query_string); 642 size_t input_position = match.contents.find(input_text); 643 if (input_position == std::wstring::npos) { 644 // The input text is not a substring of the query string, e.g. input 645 // text is "slasdot" and the query string is "slashdot", so we bold the 646 // whole thing. 647 match.contents_class.push_back( 648 ACMatchClassification(0, ACMatchClassification::MATCH)); 649 } else { 650 // TODO(beng): ACMatchClassification::MATCH now seems to just mean 651 // "bold" this. Consider modifying the terminology. 652 // We don't iterate over the string here annotating all matches because 653 // it looks odd to have every occurrence of a substring that may be as 654 // short as a single character highlighted in a query suggestion result, 655 // e.g. for input text "s" and query string "southwest airlines", it 656 // looks odd if both the first and last s are highlighted. 657 if (input_position != 0) { 658 match.contents_class.push_back( 659 ACMatchClassification(0, ACMatchClassification::NONE)); 660 } 661 match.contents_class.push_back( 662 ACMatchClassification(input_position, ACMatchClassification::DIM)); 663 size_t next_fragment_position = input_position + input_text.length(); 664 if (next_fragment_position < query_string.length()) { 665 match.contents_class.push_back( 666 ACMatchClassification(next_fragment_position, 667 ACMatchClassification::NONE)); 668 } 669 } 670 } else { 671 // Otherwise, we're dealing with the "default search" result which has no 672 // completion, but has the search provider name as the description. 673 match.contents.assign(query_string); 674 match.contents_class.push_back( 675 ACMatchClassification(0, ACMatchClassification::NONE)); 676 match.description.assign(l10n_util::GetStringF( 677 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION, 678 provider.AdjustedShortNameForLocaleDirection())); 679 match.description_class.push_back( 680 ACMatchClassification(0, ACMatchClassification::DIM)); 681 } 682 683 // When the user forced a query, we need to make sure all the fill_into_edit 684 // values preserve that property. Otherwise, if the user starts editing a 685 // suggestion, non-Search results will suddenly appear. 686 size_t search_start = 0; 687 if (input_.type() == AutocompleteInput::FORCED_QUERY) { 688 match.fill_into_edit.assign(L"?"); 689 ++search_start; 690 } 691 if (is_keyword) { 692 match.fill_into_edit.append(providers_.keyword_provider().keyword() + L" "); 693 match.template_url = &providers_.keyword_provider(); 694 } 695 match.fill_into_edit.append(query_string); 696 // Not all suggestions start with the original input. 697 if (!input_.prevent_inline_autocomplete() && 698 !match.fill_into_edit.compare(search_start, input_text.length(), 699 input_text)) 700 match.inline_autocomplete_offset = search_start + input_text.length(); 701 702 const TemplateURLRef* const search_url = provider.url(); 703 DCHECK(search_url->SupportsReplacement()); 704 match.destination_url = 705 GURL(search_url->ReplaceSearchTerms(provider, 706 query_string, 707 accepted_suggestion, 708 input_text)); 709 710 // Search results don't look like URLs. 711 match.transition = 712 is_keyword ? PageTransition::KEYWORD : PageTransition::GENERATED; 713 714 // Try to add |match| to |map|. If a match for |query_string| is already in 715 // |map|, replace it if |match| is more relevant. 716 // NOTE: Keep this ToLower() call in sync with url_database.cc. 717 const std::pair<MatchMap::iterator, bool> i = map->insert( 718 std::pair<std::wstring, AutocompleteMatch>( 719 l10n_util::ToLower(query_string), match)); 720 // NOTE: We purposefully do a direct relevance comparison here instead of 721 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added 722 // first" rather than "items alphabetically first" when the scores are equal. 723 // The only case this matters is when a user has results with the same score 724 // that differ only by capitalization; because the history system returns 725 // results sorted by recency, this means we'll pick the most recent such 726 // result even if the precision of our relevance score is too low to 727 // distinguish the two. 728 if (!i.second && (match.relevance > i.first->second.relevance)) 729 i.first->second = match; 730} 731 732AutocompleteMatch SearchProvider::NavigationToMatch( 733 const NavigationResult& navigation, 734 int relevance, 735 bool is_keyword) { 736 const std::wstring& input_text = 737 is_keyword ? keyword_input_text_ : input_.text(); 738 AutocompleteMatch match(this, relevance, false, 739 AutocompleteMatch::NAVSUGGEST); 740 match.destination_url = navigation.url; 741 match.contents = 742 StringForURLDisplay(navigation.url, true, !HasHTTPScheme(input_text)); 743 AutocompleteMatch::ClassifyMatchInString(input_text, match.contents, 744 ACMatchClassification::URL, 745 &match.contents_class); 746 747 match.description = navigation.site_name; 748 AutocompleteMatch::ClassifyMatchInString(input_text, navigation.site_name, 749 ACMatchClassification::NONE, 750 &match.description_class); 751 752 // When the user forced a query, we need to make sure all the fill_into_edit 753 // values preserve that property. Otherwise, if the user starts editing a 754 // suggestion, non-Search results will suddenly appear. 755 if (input_.type() == AutocompleteInput::FORCED_QUERY) 756 match.fill_into_edit.assign(L"?"); 757 match.fill_into_edit.append( 758 AutocompleteInput::FormattedStringWithEquivalentMeaning(navigation.url, 759 match.contents)); 760 // TODO(pkasting): http://b/1112879 These should perhaps be 761 // inline-autocompletable? 762 763 return match; 764} 765