| 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 |