blob: 0d0a348bbed6987d9461c7742c96b24a77ddc383 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "calculator_provider.h"
#include <algorithm>
#include <limits>
#include <vector>
#include "base/check.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/provider_state_service.h"
#include "components/omnibox/browser/search_provider.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
CalculatorProvider::CalculatorProvider(AutocompleteProviderClient* client,
AutocompleteProviderListener* listener,
SearchProvider* search_provider)
: AutocompleteProvider(AutocompleteProvider::TYPE_CALCULATOR),
client_(client),
search_provider_(search_provider) {
CHECK(search_provider_);
AddListener(listener);
search_provider->AddListener(this);
}
CalculatorProvider::~CalculatorProvider() = default;
void CalculatorProvider::Start(const AutocompleteInput& input,
bool minimal_changes) {
if (!omnibox_feature_configs::CalcProvider::Get().enabled)
return;
matches_.clear();
done_ = input.omit_asynchronous_matches();
if (done_)
return;
input_ = input.text();
grew_input_ = base::StartsWith(input.text(), last_calc_input_,
base::CompareCase::SENSITIVE);
shrunk_input_ = base::StartsWith(last_calc_input_, input.text(),
base::CompareCase::SENSITIVE);
if (since_last_calculator_suggestion_ < SIZE_MAX)
since_last_calculator_suggestion_++;
if (search_provider_->done()) {
// If the search provider finished before this started (unlikely), read the
// search suggestions and finalize the calc suggestions immediately.
UpdateFromSearch();
} else if (Show()) {
// Otherwise, show the cached matches now. Then `OnProviderUpdate()` will
// update the matches when the search provider finishes.
AddMatches();
}
}
void CalculatorProvider::DeleteMatch(const AutocompleteMatch& match) {
auto it = std::ranges::find_if(Cache(), [&](const auto& cached) {
return cached.match.destination_url == match.destination_url;
});
if (it != Cache().end()) {
Cache().erase(it);
AddMatches();
}
}
void CalculatorProvider::OnProviderUpdate(
bool updated_matches,
const AutocompleteProvider* provider) {
CHECK_EQ(provider, search_provider_);
if (search_provider_->done() && !done_)
UpdateFromSearch();
}
void CalculatorProvider::UpdateFromSearch() {
CHECK(!done_);
// Find and cache any search calculator matches.
for (const auto& match : search_provider_->matches()) {
if (match.type == AutocompleteMatchType::CALCULATOR)
AddMatchToCache(match);
}
done_ = true;
bool show = Show();
if (show)
AddMatches();
NotifyListeners(show);
}
void CalculatorProvider::AddMatchToCache(AutocompleteMatch match) {
// Set provider to null so the cache doesn't contain dangling pointers if this
// provider is deleted (i.e. the window it belongs to is closed).
match.provider = nullptr;
match.deletable = true;
match.allowed_to_be_default_match = false;
match.additional_info.clear();
match.RecordAdditionalInfo("original relevance", match.relevance);
match.RecordAdditionalInfo("input", input_);
// As the user types out an input, e.g. '1+22+33', replace the intermediate
// matches to avoid showing all of: '1+2=3', '1+22=23', '1+22+3=26', &
// '1+22+33=56'.
if (!Cache().empty() && grew_input_ && !last_calc_input_.empty())
Cache().pop_back();
// Remove duplicates to avoid a repeated match reducing cache capacity.
auto duplicate = std::ranges::find_if(Cache(), [&](const auto& cached) {
return cached.match.contents == match.contents;
});
if (duplicate != Cache().end())
Cache().erase(duplicate);
if (Cache().size() >
omnibox_feature_configs::CalcProvider::Get().max_matches) {
Cache().erase(Cache().begin());
}
Cache().push_back({std::move(match), base::TimeTicks::Now()});
last_calc_input_ = input_;
since_last_calculator_suggestion_ = 0;
}
void CalculatorProvider::AddMatches() {
// Expire old cached matches.
const auto now = base::TimeTicks::Now();
std::erase_if(Cache(), [&](const auto& cached) {
return now - cached.time > base::Hours(1);
});
matches_.clear();
// Score sequentially so they're ranked sequentially.
// TODO(manukh) Consider enforcing hard grouping (e.g. search v URL).
int relevance = omnibox_feature_configs::CalcProvider::Get().score;
// Use copies instead of references to avoid dangling pointers. This provider
// might be deleted before the cache (i.e. the window this provider belongs to
// might be closed).
for (auto [match, _] : Cache()) {
match.relevance = relevance++;
match.provider = this;
matches_.push_back(match);
}
}
bool CalculatorProvider::Show() {
// Show matches if either:
// a) the current input is has a calc search suggestion; e.g. '3+2'.
if (since_last_calculator_suggestion_ == 0)
return true;
// b) OR one of the recent inputs had a calc search suggestion and this input
// is similar to it. E.g. '3+2-' won't have a calc search suggestion as it's
// incomplete. This helps keep the suggestions stable.
return (grew_input_ || shrunk_input_) &&
since_last_calculator_suggestion_ <=
omnibox_feature_configs::CalcProvider::Get().num_non_calc_inputs;
}
std::vector<ProviderStateService::CachedAutocompleteMatch>&
CalculatorProvider::Cache() {
return client_->GetProviderStateService()->calculator_provider_cache;
}