OLD | NEW |
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/autocomplete/keyword_provider.h" | 5 #include "chrome/browser/autocomplete/keyword_provider.h" |
6 | 6 |
7 #include <algorithm> | 7 #include <algorithm> |
8 #include <vector> | 8 #include <vector> |
9 | 9 |
10 #include "app/l10n_util.h" | 10 #include "app/l10n_util.h" |
| 11 #include "base/string16.h" |
11 #include "base/utf_string_conversions.h" | 12 #include "base/utf_string_conversions.h" |
| 13 #include "chrome/browser/extensions/extension_omnibox_api.h" |
12 #include "chrome/browser/profile.h" | 14 #include "chrome/browser/profile.h" |
13 #include "chrome/browser/search_engines/template_url.h" | 15 #include "chrome/browser/search_engines/template_url.h" |
14 #include "chrome/browser/search_engines/template_url_model.h" | 16 #include "chrome/browser/search_engines/template_url_model.h" |
| 17 #include "chrome/common/notification_service.h" |
15 #include "grit/generated_resources.h" | 18 #include "grit/generated_resources.h" |
16 #include "net/base/escape.h" | 19 #include "net/base/escape.h" |
17 #include "net/base/net_util.h" | 20 #include "net/base/net_util.h" |
18 | 21 |
19 // static | 22 // static |
20 std::wstring KeywordProvider::SplitReplacementStringFromInput( | 23 std::wstring KeywordProvider::SplitReplacementStringFromInput( |
21 const std::wstring& input) { | 24 const std::wstring& input) { |
22 // The input may contain leading whitespace, strip it. | 25 // The input may contain leading whitespace, strip it. |
23 std::wstring trimmed_input; | 26 std::wstring trimmed_input; |
24 TrimWhitespace(input, TRIM_LEADING, &trimmed_input); | 27 TrimWhitespace(input, TRIM_LEADING, &trimmed_input); |
25 | 28 |
26 // And extract the replacement string. | 29 // And extract the replacement string. |
27 std::wstring remaining_input; | 30 std::wstring remaining_input; |
28 SplitKeywordFromInput(trimmed_input, &remaining_input); | 31 SplitKeywordFromInput(trimmed_input, &remaining_input); |
29 return remaining_input; | 32 return remaining_input; |
30 } | 33 } |
31 | 34 |
32 KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile) | 35 KeywordProvider::KeywordProvider(ACProviderListener* listener, Profile* profile) |
33 : AutocompleteProvider(listener, profile, "Keyword"), | 36 : AutocompleteProvider(listener, profile, "Keyword"), |
34 model_(NULL) { | 37 model_(NULL), |
| 38 current_input_id_(0) { |
| 39 registrar_.Add(this, NotificationType::EXTENSION_OMNIBOX_SUGGESTIONS_READY, |
| 40 Source<Profile>(profile)); |
35 } | 41 } |
36 | 42 |
37 KeywordProvider::KeywordProvider(ACProviderListener* listener, | 43 KeywordProvider::KeywordProvider(ACProviderListener* listener, |
38 TemplateURLModel* model) | 44 TemplateURLModel* model) |
39 : AutocompleteProvider(listener, NULL, "Keyword"), | 45 : AutocompleteProvider(listener, NULL, "Keyword"), |
40 model_(model) { | 46 model_(model), |
| 47 current_input_id_(0) { |
41 } | 48 } |
42 | 49 |
43 | 50 |
44 namespace { | 51 namespace { |
45 | 52 |
46 // Helper functor for Start(), for sorting keyword matches by quality. | 53 // Helper functor for Start(), for sorting keyword matches by quality. |
47 class CompareQuality { | 54 class CompareQuality { |
48 public: | 55 public: |
49 // A keyword is of higher quality when a greater fraction of it has been | 56 // A keyword is of higher quality when a greater fraction of it has been |
50 // typed, that is, when it is shorter. | 57 // typed, that is, when it is shorter. |
(...skipping 26 matching lines...) Expand all Loading... |
77 model->Load(); | 84 model->Load(); |
78 | 85 |
79 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword); | 86 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword); |
80 return TemplateURL::SupportsReplacement(template_url) ? template_url : NULL; | 87 return TemplateURL::SupportsReplacement(template_url) ? template_url : NULL; |
81 } | 88 } |
82 | 89 |
83 void KeywordProvider::Start(const AutocompleteInput& input, | 90 void KeywordProvider::Start(const AutocompleteInput& input, |
84 bool minimal_changes) { | 91 bool minimal_changes) { |
85 matches_.clear(); | 92 matches_.clear(); |
86 | 93 |
| 94 if (!minimal_changes) { |
| 95 done_ = true; |
| 96 |
| 97 // Input has changed. Increment the input ID so that we can discard any |
| 98 // stale extension suggestions that may be incoming. |
| 99 ++current_input_id_; |
| 100 } |
| 101 |
87 // Split user input into a keyword and some query input. | 102 // Split user input into a keyword and some query input. |
88 // | 103 // |
89 // We want to suggest keywords even when users have started typing URLs, on | 104 // We want to suggest keywords even when users have started typing URLs, on |
90 // the assumption that they might not realize they no longer need to go to a | 105 // the assumption that they might not realize they no longer need to go to a |
91 // site to be able to search it. So we call CleanUserInputKeyword() to strip | 106 // site to be able to search it. So we call CleanUserInputKeyword() to strip |
92 // any initial scheme and/or "www.". NOTE: Any heuristics or UI used to | 107 // any initial scheme and/or "www.". NOTE: Any heuristics or UI used to |
93 // automatically/manually create keywords will need to be in sync with | 108 // automatically/manually create keywords will need to be in sync with |
94 // whatever we do here! | 109 // whatever we do here! |
95 // | 110 // |
96 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for | 111 // TODO(pkasting): http://b/1112681 If someday we remember usage frequency for |
(...skipping 27 matching lines...) Expand all Loading... |
124 return; | 139 return; |
125 std::sort(keyword_matches.begin(), keyword_matches.end(), CompareQuality()); | 140 std::sort(keyword_matches.begin(), keyword_matches.end(), CompareQuality()); |
126 | 141 |
127 // Limit to one exact or three inexact matches, and mark them up for display | 142 // Limit to one exact or three inexact matches, and mark them up for display |
128 // in the autocomplete popup. | 143 // in the autocomplete popup. |
129 // Any exact match is going to be the highest quality match, and thus at the | 144 // Any exact match is going to be the highest quality match, and thus at the |
130 // front of our vector. | 145 // front of our vector. |
131 if (keyword_matches.front() == keyword) { | 146 if (keyword_matches.front() == keyword) { |
132 matches_.push_back(CreateAutocompleteMatch(model, keyword, input, | 147 matches_.push_back(CreateAutocompleteMatch(model, keyword, input, |
133 keyword.length(), | 148 keyword.length(), |
134 remaining_input)); | 149 remaining_input, -1)); |
| 150 |
| 151 const TemplateURL* template_url(model->GetTemplateURLForKeyword(keyword)); |
| 152 if (profile_ && |
| 153 !input.synchronous_only() && template_url->IsExtensionKeyword()) { |
| 154 if (minimal_changes) { |
| 155 // If the input hasn't significantly changed, we can just use the |
| 156 // suggestions from last time. We need to readjust the relevance to |
| 157 // ensure it is less than the main match's relevance. |
| 158 for (size_t i = 0; i < extension_suggest_matches_.size(); ++i) { |
| 159 matches_.push_back(extension_suggest_matches_[i]); |
| 160 matches_.back().relevance = matches_[0].relevance - (i + 1); |
| 161 } |
| 162 } else { |
| 163 extension_suggest_last_input_ = input; |
| 164 extension_suggest_matches_.clear(); |
| 165 |
| 166 bool have_listeners = ExtensionOmniboxEventRouter::OnInputChanged( |
| 167 profile_, template_url->GetExtensionId(), |
| 168 WideToUTF8(remaining_input), current_input_id_); |
| 169 |
| 170 // We only have to wait for suggest results if there are actually |
| 171 // extensions listening for input changes. |
| 172 if (have_listeners) |
| 173 done_ = false; |
| 174 } |
| 175 } |
135 } else { | 176 } else { |
136 if (keyword_matches.size() > kMaxMatches) { | 177 if (keyword_matches.size() > kMaxMatches) { |
137 keyword_matches.erase(keyword_matches.begin() + kMaxMatches, | 178 keyword_matches.erase(keyword_matches.begin() + kMaxMatches, |
138 keyword_matches.end()); | 179 keyword_matches.end()); |
139 } | 180 } |
140 for (std::vector<std::wstring>::const_iterator i(keyword_matches.begin()); | 181 for (std::vector<std::wstring>::const_iterator i(keyword_matches.begin()); |
141 i != keyword_matches.end(); ++i) { | 182 i != keyword_matches.end(); ++i) { |
142 matches_.push_back(CreateAutocompleteMatch(model, *i, input, | 183 matches_.push_back(CreateAutocompleteMatch(model, *i, input, |
143 keyword.length(), | 184 keyword.length(), |
144 remaining_input)); | 185 remaining_input, -1)); |
145 } | 186 } |
146 } | 187 } |
147 } | 188 } |
148 | 189 |
149 // static | 190 // static |
150 bool KeywordProvider::ExtractKeywordFromInput(const AutocompleteInput& input, | 191 bool KeywordProvider::ExtractKeywordFromInput(const AutocompleteInput& input, |
151 std::wstring* keyword, | 192 std::wstring* keyword, |
152 std::wstring* remaining_input) { | 193 std::wstring* remaining_input) { |
153 if ((input.type() == AutocompleteInput::INVALID) || | 194 if ((input.type() == AutocompleteInput::INVALID) || |
154 (input.type() == AutocompleteInput::FORCED_QUERY)) | 195 (input.type() == AutocompleteInput::FORCED_QUERY)) |
(...skipping 27 matching lines...) Expand all Loading... |
182 } | 223 } |
183 | 224 |
184 // static | 225 // static |
185 void KeywordProvider::FillInURLAndContents( | 226 void KeywordProvider::FillInURLAndContents( |
186 const std::wstring& remaining_input, | 227 const std::wstring& remaining_input, |
187 const TemplateURL* element, | 228 const TemplateURL* element, |
188 AutocompleteMatch* match) { | 229 AutocompleteMatch* match) { |
189 DCHECK(!element->short_name().empty()); | 230 DCHECK(!element->short_name().empty()); |
190 DCHECK(element->url()); | 231 DCHECK(element->url()); |
191 DCHECK(element->url()->IsValid()); | 232 DCHECK(element->url()->IsValid()); |
| 233 int message_id = element->IsExtensionKeyword() ? |
| 234 IDS_EXTENSION_KEYWORD_COMMAND : IDS_KEYWORD_SEARCH; |
192 if (remaining_input.empty()) { | 235 if (remaining_input.empty()) { |
193 if (element->url()->SupportsReplacement()) { | 236 if (element->url()->SupportsReplacement()) { |
194 // No query input; return a generic, no-destination placeholder. | 237 // No query input; return a generic, no-destination placeholder. |
195 match->contents.assign(l10n_util::GetStringF(IDS_KEYWORD_SEARCH, | 238 match->contents.assign(l10n_util::GetStringF(message_id, |
196 element->AdjustedShortNameForLocaleDirection(), | 239 element->AdjustedShortNameForLocaleDirection(), |
197 l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE))); | 240 l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE))); |
198 match->contents_class.push_back( | 241 match->contents_class.push_back( |
199 ACMatchClassification(0, ACMatchClassification::DIM)); | 242 ACMatchClassification(0, ACMatchClassification::DIM)); |
200 } else { | 243 } else { |
201 // Keyword that has no replacement text (aka a shorthand for a URL). | 244 // Keyword that has no replacement text (aka a shorthand for a URL). |
202 match->destination_url = GURL(WideToUTF8(element->url()->url())); | 245 match->destination_url = GURL(WideToUTF8(element->url()->url())); |
203 match->contents.assign(element->short_name()); | 246 match->contents.assign(element->short_name()); |
204 AutocompleteMatch::ClassifyLocationInString(0, match->contents.length(), | 247 AutocompleteMatch::ClassifyLocationInString(0, match->contents.length(), |
205 match->contents.length(), ACMatchClassification::NONE, | 248 match->contents.length(), ACMatchClassification::NONE, |
206 &match->contents_class); | 249 &match->contents_class); |
207 } | 250 } |
208 } else { | 251 } else { |
209 // Create destination URL by escaping user input and substituting into | 252 // Create destination URL by escaping user input and substituting into |
210 // keyword template URL. The escaping here handles whitespace in user | 253 // keyword template URL. The escaping here handles whitespace in user |
211 // input, but we rely on later canonicalization functions to do more | 254 // input, but we rely on later canonicalization functions to do more |
212 // fixup to make the URL valid if necessary. | 255 // fixup to make the URL valid if necessary. |
213 DCHECK(element->url()->SupportsReplacement()); | 256 DCHECK(element->url()->SupportsReplacement()); |
214 match->destination_url = GURL(WideToUTF8(element->url()->ReplaceSearchTerms( | 257 match->destination_url = GURL(WideToUTF8(element->url()->ReplaceSearchTerms( |
215 *element, remaining_input, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, | 258 *element, remaining_input, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, |
216 std::wstring()))); | 259 std::wstring()))); |
217 std::vector<size_t> content_param_offsets; | 260 std::vector<size_t> content_param_offsets; |
218 match->contents.assign(l10n_util::GetStringF(IDS_KEYWORD_SEARCH, | 261 match->contents.assign(l10n_util::GetStringF(message_id, |
219 element->short_name(), | 262 element->short_name(), |
220 remaining_input, | 263 remaining_input, |
221 &content_param_offsets)); | 264 &content_param_offsets)); |
222 if (content_param_offsets.size() == 2) { | 265 if (content_param_offsets.size() == 2) { |
223 AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1], | 266 AutocompleteMatch::ClassifyLocationInString(content_param_offsets[1], |
224 remaining_input.length(), match->contents.length(), | 267 remaining_input.length(), match->contents.length(), |
225 ACMatchClassification::NONE, &match->contents_class); | 268 ACMatchClassification::NONE, &match->contents_class); |
226 } else { | 269 } else { |
227 // See comments on an identical NOTREACHED() in search_provider.cc. | 270 // See comments on an identical NOTREACHED() in search_provider.cc. |
228 NOTREACHED(); | 271 NOTREACHED(); |
(...skipping 10 matching lines...) Expand all Loading... |
239 if (no_query_text_needed) | 282 if (no_query_text_needed) |
240 return 1500; | 283 return 1500; |
241 return (type == AutocompleteInput::QUERY) ? 1450 : 1100; | 284 return (type == AutocompleteInput::QUERY) ? 1450 : 1100; |
242 } | 285 } |
243 | 286 |
244 AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( | 287 AutocompleteMatch KeywordProvider::CreateAutocompleteMatch( |
245 TemplateURLModel* model, | 288 TemplateURLModel* model, |
246 const std::wstring keyword, | 289 const std::wstring keyword, |
247 const AutocompleteInput& input, | 290 const AutocompleteInput& input, |
248 size_t prefix_length, | 291 size_t prefix_length, |
249 const std::wstring& remaining_input) { | 292 const std::wstring& remaining_input, |
| 293 int relevance) { |
250 DCHECK(model); | 294 DCHECK(model); |
251 // Get keyword data from data store. | 295 // Get keyword data from data store. |
252 const TemplateURL* element(model->GetTemplateURLForKeyword(keyword)); | 296 const TemplateURL* element(model->GetTemplateURLForKeyword(keyword)); |
253 DCHECK(element && element->url()); | 297 DCHECK(element && element->url()); |
254 const bool supports_replacement = element->url()->SupportsReplacement(); | 298 const bool supports_replacement = element->url()->SupportsReplacement(); |
255 | 299 |
256 // Create an edit entry of "[keyword] [remaining input]". This is helpful | 300 // Create an edit entry of "[keyword] [remaining input]". This is helpful |
257 // even when [remaining input] is empty, as the user can select the popup | 301 // even when [remaining input] is empty, as the user can select the popup |
258 // choice and immediately begin typing in query input. | 302 // choice and immediately begin typing in query input. |
259 const bool keyword_complete = (prefix_length == keyword.length()); | 303 const bool keyword_complete = (prefix_length == keyword.length()); |
260 AutocompleteMatch result(this, | 304 if (relevance < 0) { |
261 CalculateRelevance(input.type(), keyword_complete, | 305 relevance = |
262 // When the user wants keyword matches to take | 306 CalculateRelevance(input.type(), keyword_complete, |
263 // preference, score them highly regardless of whether | 307 // When the user wants keyword matches to take |
264 // the input provides query text. | 308 // preference, score them highly regardless of |
265 input.prefer_keyword() || !supports_replacement), | 309 // whether the input provides query text. |
266 false, supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE : | 310 input.prefer_keyword() || !supports_replacement); |
267 AutocompleteMatch::HISTORY_KEYWORD); | 311 } |
| 312 AutocompleteMatch result(this, relevance, false, |
| 313 supports_replacement ? AutocompleteMatch::SEARCH_OTHER_ENGINE : |
| 314 AutocompleteMatch::HISTORY_KEYWORD); |
268 result.fill_into_edit.assign(keyword); | 315 result.fill_into_edit.assign(keyword); |
269 if (!remaining_input.empty() || !keyword_complete || supports_replacement) | 316 if (!remaining_input.empty() || !keyword_complete || supports_replacement) |
270 result.fill_into_edit.push_back(L' '); | 317 result.fill_into_edit.push_back(L' '); |
271 result.fill_into_edit.append(remaining_input); | 318 result.fill_into_edit.append(remaining_input); |
272 if (!input.prevent_inline_autocomplete() && | 319 if (!input.prevent_inline_autocomplete() && |
273 (keyword_complete || remaining_input.empty())) | 320 (keyword_complete || remaining_input.empty())) |
274 result.inline_autocomplete_offset = input.text().length(); | 321 result.inline_autocomplete_offset = input.text().length(); |
275 | 322 |
276 // Create destination URL and popup entry content by substituting user input | 323 // Create destination URL and popup entry content by substituting user input |
277 // into keyword templates. | 324 // into keyword templates. |
278 FillInURLAndContents(remaining_input, element, &result); | 325 FillInURLAndContents(remaining_input, element, &result); |
279 | 326 |
280 // Create popup entry description based on the keyword name. | 327 // Create popup entry description based on the keyword name. |
281 result.description.assign(l10n_util::GetStringF( | 328 int message_id = element->IsExtensionKeyword() ? |
282 IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION, keyword)); | 329 IDS_AUTOCOMPLETE_EXTENSION_KEYWORD_DESCRIPTION : |
| 330 IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION; |
| 331 result.description.assign(l10n_util::GetStringF(message_id, keyword)); |
283 if (supports_replacement) | 332 if (supports_replacement) |
284 result.template_url = element; | 333 result.template_url = element; |
285 static const std::wstring kKeywordDesc(l10n_util::GetString( | 334 static const std::wstring kKeywordDesc(l10n_util::GetString(message_id)); |
286 IDS_AUTOCOMPLETE_KEYWORD_DESCRIPTION)); | |
287 AutocompleteMatch::ClassifyLocationInString(kKeywordDesc.find(L"%s"), | 335 AutocompleteMatch::ClassifyLocationInString(kKeywordDesc.find(L"%s"), |
288 prefix_length, | 336 prefix_length, |
289 result.description.length(), | 337 result.description.length(), |
290 ACMatchClassification::DIM, | 338 ACMatchClassification::DIM, |
291 &result.description_class); | 339 &result.description_class); |
292 | 340 |
293 result.transition = PageTransition::KEYWORD; | 341 result.transition = PageTransition::KEYWORD; |
294 | 342 |
295 return result; | 343 return result; |
296 } | 344 } |
| 345 |
| 346 void KeywordProvider::Observe(NotificationType type, |
| 347 const NotificationSource& source, |
| 348 const NotificationDetails& details) { |
| 349 // TODO(mpcomplete): consider clamping the number of suggestions to |
| 350 // AutocompleteProvider::kMaxMatches. |
| 351 DCHECK(type == NotificationType::EXTENSION_OMNIBOX_SUGGESTIONS_READY); |
| 352 |
| 353 int suggest_id = Details<ExtensionOmniboxSuggestions>(details).ptr()->first; |
| 354 if (suggest_id != current_input_id_) |
| 355 return; // This is an old result. Just ignore. |
| 356 |
| 357 const AutocompleteInput& input = extension_suggest_last_input_; |
| 358 std::wstring keyword, remaining_input; |
| 359 if (!ExtractKeywordFromInput(input, &keyword, &remaining_input)) { |
| 360 NOTREACHED(); |
| 361 return; |
| 362 } |
| 363 |
| 364 TemplateURLModel* model = |
| 365 profile_ ? profile_->GetTemplateURLModel() : model_; |
| 366 |
| 367 ListValue* suggestions = |
| 368 Details<ExtensionOmniboxSuggestions>(details).ptr()->second; |
| 369 for (size_t i = 0; i < suggestions->GetSize(); ++i) { |
| 370 DictionaryValue* suggestion; |
| 371 string16 content, description; |
| 372 if (!suggestions->GetDictionary(i, &suggestion) || |
| 373 !suggestion->GetString("content", &content) || |
| 374 !suggestion->GetString("description", &description)) |
| 375 break; |
| 376 |
| 377 // We want to order these suggestions in descending order, so start with |
| 378 // the relevance of the first result (added synchronously in Start()), |
| 379 // and subtract 1 for each subsequent suggestion from the extension. |
| 380 // We know that |complete| is true, because we wouldn't get results from |
| 381 // the extension unless the full keyword had been typed. |
| 382 int first_relevance = |
| 383 CalculateRelevance(input.type(), true, input.prefer_keyword()); |
| 384 extension_suggest_matches_.push_back(CreateAutocompleteMatch( |
| 385 model, keyword, input, keyword.length(), UTF16ToWide(content), |
| 386 first_relevance - (i + 1))); |
| 387 |
| 388 if (!description.empty()) { |
| 389 AutocompleteMatch* match = &extension_suggest_matches_.back(); |
| 390 std::vector<size_t> offsets; |
| 391 match->contents.assign(l10n_util::GetStringF( |
| 392 IDS_AUTOCOMPLETE_EXTENSION_KEYWORD_CONTENT, |
| 393 match->contents, UTF16ToWide(description), &offsets)); |
| 394 CHECK_EQ(2U, offsets.size()) << |
| 395 "Expected 2 params for IDS_AUTOCOMPLETE_EXTENSION_KEYWORD_CONTENT"; |
| 396 match->contents_class.push_back( |
| 397 ACMatchClassification(offsets[1], ACMatchClassification::NONE)); |
| 398 } |
| 399 } |
| 400 |
| 401 done_ = true; |
| 402 matches_.insert(matches_.end(), extension_suggest_matches_.begin(), |
| 403 extension_suggest_matches_.end()); |
| 404 listener_->OnProviderUpdate(!extension_suggest_matches_.empty()); |
| 405 } |
OLD | NEW |