diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 5e212e21a34..a8f5d3a3e13 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -31,6 +31,7 @@ #include "brave/components/skus/common/features.h" #include "brave/components/speedreader/common/buildflags/buildflags.h" #include "build/build_config.h" +#include "chrome/browser/ui/ui_features.h" #include "components/content_settings/core/common/features.h" #include "components/flags_ui/feature_entry.h" #include "components/flags_ui/feature_entry_macros.h" diff --git a/browser/sources.gni b/browser/sources.gni index 45208aa4337..d42f3d1640b 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -232,6 +232,7 @@ brave_chrome_browser_deps = [ if (!is_android && !is_ios) { brave_chrome_browser_deps += [ + "//brave/components/commander/browser", "//brave/components/commands/browser", "//brave/components/commands/common", ] diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index c898461f923..26d85e40084 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -88,6 +88,10 @@ source_set("ui") { "browser_commands.cc", "browser_commands.h", "browser_dialogs.h", + "commander/commander_service.cc", + "commander/commander_service.h", + "commander/commander_service_factory.cc", + "commander/commander_service_factory.h", "content_settings/brave_autoplay_blocked_image_model.cc", "content_settings/brave_autoplay_blocked_image_model.h", "content_settings/brave_autoplay_content_setting_bubble_model.cc", @@ -535,6 +539,10 @@ source_set("ui") { deps += [ "//brave/components/text_recognition/browser" ] } + if (!is_android && !is_ios) { + deps += [ "//brave/components/commander/browser" ] + } + # This is no longer compiled into Chromium on Android, but we still # need it if (is_android) { diff --git a/browser/ui/commander/BUILD.gn b/browser/ui/commander/BUILD.gn new file mode 100644 index 00000000000..47a3cd02b4f --- /dev/null +++ b/browser/ui/commander/BUILD.gn @@ -0,0 +1,24 @@ +# Copyright (c) 2023 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +source_set("browser_tests") { + testonly = true + defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ] + + sources = [ "commander_service_browsertest.cc" ] + + deps = [ + "//base", + "//brave/components/commander/browser", + "//chrome/browser/profiles", + "//chrome/browser/profiles:profile", + "//chrome/browser/ui", + "//chrome/test:test_support_ui", + "//components/omnibox/browser", + "//content/test:test_support", + "//testing/gmock", + "//testing/gtest", + ] +} diff --git a/browser/ui/commander/commander_service.cc b/browser/ui/commander/commander_service.cc new file mode 100644 index 00000000000..c9fb3578800 --- /dev/null +++ b/browser/ui/commander/commander_service.cc @@ -0,0 +1,255 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/commander/commander_service.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/functional/bind.h" +#include "base/functional/callback_forward.h" +#include "base/functional/callback_helpers.h" +#include "base/location.h" +#include "base/memory/weak_ptr.h" +#include "base/ranges/algorithm.h" +#include "base/strings/strcat.h" +#include "base/strings/string_util.h" +#include "base/task/sequenced_task_runner.h" +#include "brave/components/commander/browser/commander_frontend_delegate.h" +#include "brave/components/commander/browser/commander_item_model.h" +#include "brave/components/commander/common/constants.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_window.h" +#include "chrome/browser/ui/commander/bookmark_command_source.h" +#include "chrome/browser/ui/commander/command_source.h" +#include "chrome/browser/ui/commander/commander.h" +#include "chrome/browser/ui/commander/commander_view_model.h" +#include "chrome/browser/ui/commander/open_url_command_source.h" +#include "chrome/browser/ui/commander/simple_command_source.h" +#include "chrome/browser/ui/commander/tab_command_source.h" +#include "chrome/browser/ui/commander/window_command_source.h" +#include "chrome/browser/ui/location_bar/location_bar.h" +#include "components/omnibox/browser/omnibox_edit_model.h" +#include "components/omnibox/browser/omnibox_view.h" + +namespace commander { +namespace { +constexpr size_t kMaxResults = 8; +CommandItemModel FromCommand(const std::unique_ptr& item) { + return CommandItemModel(item->title, item->matched_ranges, item->annotation); +} +} // namespace + +CommanderService::CommanderService(Profile* profile) : profile_(profile) { + command_sources_.push_back(std::make_unique()); + command_sources_.push_back(std::make_unique()); + command_sources_.push_back(std::make_unique()); + command_sources_.push_back(std::make_unique()); + command_sources_.push_back(std::make_unique()); +} + +CommanderService::~CommanderService() = default; + +void CommanderService::AddObserver(Observer* observer) { + observers_.AddObserver(observer); + observer->OnCommanderUpdated(); +} + +void CommanderService::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void CommanderService::UpdateText(bool force) { + auto* browser = chrome::FindLastActiveWithProfile(profile_); + + // The last active browser can have no tabs, if we're in the process of moving + // the last tab from the current window into another one. + if (!browser || browser->tab_strip_model()->empty()) { + return; + } + + auto* window = browser->window(); + CHECK(window); + + auto text = window->GetLocationBar()->GetOmniboxView()->GetText(); + if (!base::StartsWith(text, kCommandPrefix)) { + return; + } + + std::u16string trimmed_text(base::TrimWhitespace( + text.substr(kCommandPrefix.size()), base::TRIM_LEADING)); + + // If nothing has changed (and we aren't forcing things), don't update the + // commands. + if (trimmed_text == last_searched_ && browser == last_browser_ && !force) { + return; + } + last_searched_ = trimmed_text; + last_browser_ = browser; + + UpdateCommands(); +} + +void CommanderService::SelectCommand(uint32_t command_index, + uint32_t result_set_id) { + if (command_index >= items_.size() || + result_set_id != current_result_set_id_) { + return; + } + + // Increment the current result set id - we don't want any commands from this + // set to be reused after we've selected a command. + // Note: This needs to happen before beginning a composite command, to ensure + // that the generated model uses right right |result_set_id|. + current_result_set_id_++; + + auto* item = items_[command_index].get(); + if (item->GetType() == CommandItem::Type::kOneShot) { + std::move(absl::get(item->command)).Run(); + Hide(); + } else { + auto composite_command = + absl::get(item->command); + std::tie(prompt_, composite_command_provider_) = composite_command; + Show(); + } +} + +std::vector CommanderService::GetItems() { + std::vector result; + base::ranges::transform(items_, std::back_inserter(result), FromCommand); + return result; +} + +int CommanderService::GetResultSetId() { + return current_result_set_id_; +} + +const std::u16string& CommanderService::GetPrompt() { + return prompt_; +} + +void CommanderService::Shutdown() { + weak_ptr_factory_.InvalidateWeakPtrs(); +} + +void CommanderService::Toggle() { + if (IsShowing()) { + Hide(); + } else { + Show(); + } +} + +void CommanderService::Show() { + // Note: This posts a task because we can't change the Omnibox text while + // autocompleting. + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&CommanderService::ShowCommander, + weak_ptr_factory_.GetWeakPtr())); +} + +void CommanderService::Hide() { + // Note: This posts a task because we can't change the Omnibox text while + // autocompleting. + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&CommanderService::HideCommander, + weak_ptr_factory_.GetWeakPtr())); +} + +void CommanderService::Reset() { + current_result_set_id_++; + items_.clear(); + prompt_.clear(); + last_searched_.clear(); + last_browser_ = nullptr; + if (composite_command_provider_) { + composite_command_provider_.Reset(); + } + NotifyObservers(); +} + +OmniboxView* CommanderService::GetOmnibox() const { + auto* browser = chrome::FindLastActiveWithProfile(profile_); + if (!browser) { + return nullptr; + } + + auto* window = browser->window(); + CHECK(window); + return window->GetLocationBar()->GetOmniboxView(); +} + +bool CommanderService::IsShowing() const { + auto* omnibox = GetOmnibox(); + return omnibox && omnibox->GetText().starts_with(kCommandPrefix.data()); +} + +void CommanderService::UpdateCommands() { + std::vector> items; + if (composite_command_provider_) { + items = composite_command_provider_.Run(last_searched_); + } else { + for (auto& source : command_sources_) { + auto commands = source->GetCommands(last_searched_, last_browser_); + items.insert(items.end(), std::make_move_iterator(commands.begin()), + std::make_move_iterator(commands.end())); + } + } + + // Sort at most |kMaxResults| by score and then alphabetically. + auto max_elements = std::min(items.size(), kMaxResults); + std::partial_sort( + std::begin(items), std::begin(items) + max_elements, std::end(items), + [](const std::unique_ptr& left, + const std::unique_ptr& right) { + return (left->score == right->score) ? left->title < right->title + : left->score > right->score; + }); + if (items.size() > kMaxResults) { + items.resize(kMaxResults); + } + items_ = std::move(items); + + // Increment the current result set id, so we don't confuse these results with + // a prior set before notifying observers. + current_result_set_id_++; + NotifyObservers(); +} + +void CommanderService::NotifyObservers() { + for (auto& observer : observers_) { + observer.OnCommanderUpdated(); + } +} + +void CommanderService::ShowCommander() { + if (auto* omnibox = GetOmnibox()) { + omnibox->SetFocus(true); + + auto text = base::StrCat({commander::kCommandPrefix, u" "}); + omnibox->SetUserText(text); + omnibox->SetCaretPos(text.size()); + UpdateText(true); + } +} + +void CommanderService::HideCommander() { + Reset(); + + if (auto* omnibox = GetOmnibox(); omnibox && IsShowing()) { + omnibox->RevertAll(); + } +} + +} // namespace commander diff --git a/browser/ui/commander/commander_service.h b/browser/ui/commander/commander_service.h new file mode 100644 index 00000000000..77a8771574c --- /dev/null +++ b/browser/ui/commander/commander_service.h @@ -0,0 +1,80 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_H_ +#define BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_H_ + +#include +#include +#include + +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "brave/components/commander/browser/commander_frontend_delegate.h" +#include "brave/components/commander/browser/commander_item_model.h" +#include "chrome/browser/ui/commander/command_source.h" +#include "components/keyed_service/core/keyed_service.h" + +class OmniboxView; +class Profile; + +namespace commander { + +class CommanderService : public CommanderFrontendDelegate, public KeyedService { + public: + using CommandSources = std::vector>; + + explicit CommanderService(Profile* profile); + CommanderService(const CommanderService&) = delete; + CommanderService& operator=(const CommanderService&) = delete; + ~CommanderService() override; + + void Show(); + void Reset(); + bool IsShowing() const; + + // CommanderFrontendDelegate: + void Toggle() override; + void Hide() override; + void AddObserver(Observer* observer) override; + void RemoveObserver(Observer* observer) override; + void UpdateText(bool force = false) override; + void SelectCommand(uint32_t command_index, uint32_t result_set_id) override; + std::vector GetItems() override; + int GetResultSetId() override; + const std::u16string& GetPrompt() override; + + // KeyedService: + void Shutdown() override; + + private: + OmniboxView* GetOmnibox() const; + void UpdateCommands(); + void NotifyObservers(); + + void ShowCommander(); + void HideCommander(); + + CommandSources command_sources_; + + std::u16string last_searched_; + std::u16string prompt_; + std::vector> items_; + uint32_t current_result_set_id_ = 0; + raw_ptr last_browser_; + raw_ptr profile_; + + // Some commands have multiple steps (like move tab to window, pick a + // window). This allows commands to specify a command provider for a + // subsequent step (in the window example, this would be a list of all + // available windows). + CommandItem::CompositeCommandProvider composite_command_provider_; + + base::ObserverList observers_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; +} // namespace commander + +#endif // BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_H_ diff --git a/browser/ui/commander/commander_service_browsertest.cc b/browser/ui/commander/commander_service_browsertest.cc new file mode 100644 index 00000000000..501c830d7ce --- /dev/null +++ b/browser/ui/commander/commander_service_browsertest.cc @@ -0,0 +1,189 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include "base/functional/callback_forward.h" +#include "base/location.h" +#include "base/run_loop.h" +#include "base/strings/strcat.h" +#include "base/test/bind.h" +#include "base/test/scoped_feature_list.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "brave/browser/ui/commander/commander_service.h" +#include "brave/browser/ui/commander/commander_service_factory.h" +#include "brave/components/commander/common/constants.h" +#include "brave/components/commander/common/features.h" +#include "build/build_config.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/location_bar/location_bar.h" +#include "chrome/browser/ui/views/frame/browser_view.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "components/omnibox/browser/omnibox_view.h" +#include "content/public/test/browser_test.h" +#include "testing/gmock/include/gmock/gmock.h" + +class CommanderServiceBrowserTest : public InProcessBrowserTest { + public: + CommanderServiceBrowserTest() { + features_.InitAndEnableFeature(features::kBraveCommander); + } + + ~CommanderServiceBrowserTest() override = default; + void SetUp() override { InProcessBrowserTest::SetUp(); } + void TearDownOnMainThread() override { + commander()->Hide(); + WaitUntil(base::BindLambdaForTesting( + [this]() { return !commander()->IsShowing(); })); + } + + protected: + Profile* profile() { return browser()->profile(); } + commander::CommanderService* commander() { + return commander::CommanderServiceFactory::GetForBrowserContext(profile()); + } + + OmniboxView* omnibox() { + return browser()->window()->GetLocationBar()->GetOmniboxView(); + } + + void WaitUntil(base::RepeatingCallback condition) { + if (condition.Run()) { + return; + } + + base::RepeatingTimer scheduler; + scheduler.Start(FROM_HERE, base::Milliseconds(100), + base::BindLambdaForTesting([this, &condition]() { + if (condition.Run()) { + run_loop_->Quit(); + } + })); + Run(); + } + + private: + void Run() { + run_loop_ = std::make_unique(); + run_loop_->Run(); + } + + base::test::ScopedFeatureList features_; + std::unique_ptr run_loop_; +}; + +IN_PROC_BROWSER_TEST_F(CommanderServiceBrowserTest, CanShowCommander) { + EXPECT_FALSE(commander()->IsShowing()); + + commander()->Show(); + WaitUntil( + base::BindLambdaForTesting([&]() { return commander()->IsShowing(); })); +} + +IN_PROC_BROWSER_TEST_F(CommanderServiceBrowserTest, + CanShowCommanderViaOmniboxText) { + EXPECT_FALSE(commander()->IsShowing()); + + omnibox()->SetUserText( + base::StrCat({commander::kCommandPrefix, u" Hello World"})); + WaitUntil( + base::BindLambdaForTesting([&]() { return commander()->IsShowing(); })); +} + +IN_PROC_BROWSER_TEST_F(CommanderServiceBrowserTest, CanHideCommander) { + commander()->Show(); + WaitUntil( + base::BindLambdaForTesting([&]() { return commander()->IsShowing(); })); + + commander()->Hide(); + WaitUntil( + base::BindLambdaForTesting([&]() { return !commander()->IsShowing(); })); +} + +// NOTE: This test will pass in isolation but they depend on focus +// so they'll fail if run with other tests. It'd be a good candidate for an +// interactive UI test. +IN_PROC_BROWSER_TEST_F(CommanderServiceBrowserTest, MANUAL_HideClearsText) { + commander()->Show(); + omnibox()->SetUserText( + base::StrCat({commander::kCommandPrefix, u" Hello World"})); + + commander()->Hide(); + WaitUntil( + base::BindLambdaForTesting([&]() { return !commander()->IsShowing(); })); + EXPECT_EQ(u"about:blank", omnibox()->GetText()); +} + +// NOTE: This test will pass in isolation but they depend on focus +// so they'll fail if run with other tests. It'd be a good candidate for an +// interactive UI test. +IN_PROC_BROWSER_TEST_F(CommanderServiceBrowserTest, + MANUAL_CanHideCommanderViaText) { + omnibox()->SetUserText( + base::StrCat({commander::kCommandPrefix, u" Hello World"})); + WaitUntil( + base::BindLambdaForTesting([&]() { return commander()->IsShowing(); })); + + omnibox()->SetUserText(u"Hello World"); + WaitUntil( + base::BindLambdaForTesting([&]() { return !commander()->IsShowing(); })); +} + +IN_PROC_BROWSER_TEST_F(CommanderServiceBrowserTest, + CommandsAreUpdatedViaOmnibox) { + omnibox()->SetUserText( + base::StrCat({commander::kCommandPrefix, u" Create tab"})); + + EXPECT_LE(1, commander()->GetResultSetId()); + + auto items = commander()->GetItems(); + ASSERT_EQ(1u, items.size()); + EXPECT_EQ(u"Create new tab", items[0].title); +} + +IN_PROC_BROWSER_TEST_F(CommanderServiceBrowserTest, CommandsCanBeSelected) { + omnibox()->SetUserText( + base::StrCat({commander::kCommandPrefix, u" Create tab"})); + + EXPECT_LE(1, commander()->GetResultSetId()); + + auto items = commander()->GetItems(); + ASSERT_EQ(1u, items.size()); + EXPECT_EQ(u"Create new tab", items[0].title); + + EXPECT_EQ(1, browser()->tab_strip_model()->count()); + commander()->SelectCommand(0, commander()->GetResultSetId()); + EXPECT_EQ(2, browser()->tab_strip_model()->count()); +} + +IN_PROC_BROWSER_TEST_F(CommanderServiceBrowserTest, + CompositeCommandsCanBeSelected) { + omnibox()->SetUserText( + base::StrCat({commander::kCommandPrefix, u" Pin tab"})); + + EXPECT_LE(1, commander()->GetResultSetId()); + + auto items = commander()->GetItems(); + ASSERT_EQ(2u, items.size()); + EXPECT_EQ(u"Pin tab...", items[0].title); + EXPECT_EQ(u"Close unpinned tabs", items[1].title); + + commander()->SelectCommand(0, 1); + EXPECT_LE(2, commander()->GetResultSetId()); + + // This is retriggered on a different thread normally, but we want to force it + // here because otherwise the tests get a bit flakey with focus. + omnibox()->SetUserText(commander::kCommandPrefix.data()); + + items = commander()->GetItems(); + ASSERT_EQ(1u, items.size()); + EXPECT_EQ(u"about:blank", items[0].title); + + EXPECT_FALSE(browser()->tab_strip_model()->IsTabPinned(0)); + commander()->SelectCommand(0, commander()->GetResultSetId()); + EXPECT_TRUE(browser()->tab_strip_model()->IsTabPinned(0)); +} diff --git a/browser/ui/commander/commander_service_factory.cc b/browser/ui/commander/commander_service_factory.cc new file mode 100644 index 00000000000..2d311828743 --- /dev/null +++ b/browser/ui/commander/commander_service_factory.cc @@ -0,0 +1,39 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/commander/commander_service_factory.h" + +#include "base/memory/singleton.h" +#include "brave/browser/ui/commander/commander_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_keyed_service_factory.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/browser_context.h" + +namespace commander { + +// static +CommanderServiceFactory* CommanderServiceFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +CommanderService* CommanderServiceFactory::GetForBrowserContext( + content::BrowserContext* context) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +CommanderServiceFactory::CommanderServiceFactory() + : ProfileKeyedServiceFactory("CommanderService", + ProfileSelections::BuildForAllProfiles()) {} +CommanderServiceFactory::~CommanderServiceFactory() = default; + +KeyedService* CommanderServiceFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new CommanderService(Profile::FromBrowserContext(context)); +} + +} // namespace commander diff --git a/browser/ui/commander/commander_service_factory.h b/browser/ui/commander/commander_service_factory.h new file mode 100644 index 00000000000..63d91541077 --- /dev/null +++ b/browser/ui/commander/commander_service_factory.h @@ -0,0 +1,38 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_FACTORY_H_ +#define BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "brave/browser/ui/commander/commander_service.h" +#include "chrome/browser/profiles/profile_keyed_service_factory.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/browser_context.h" + +namespace commander { + +class CommanderServiceFactory : public ProfileKeyedServiceFactory { + public: + CommanderServiceFactory(const CommanderServiceFactory&) = delete; + CommanderServiceFactory& operator=(const CommanderServiceFactory&) = delete; + + static CommanderServiceFactory* GetInstance(); + static CommanderService* GetForBrowserContext( + content::BrowserContext* context); + + private: + CommanderServiceFactory(); + ~CommanderServiceFactory() override; + friend struct base::DefaultSingletonTraits; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; +}; + +} // namespace commander + +#endif // BRAVE_BROWSER_UI_COMMANDER_COMMANDER_SERVICE_FACTORY_H_ diff --git a/browser/ui/views/location_bar/brave_location_bar_view.cc b/browser/ui/views/location_bar/brave_location_bar_view.cc index bc009b69dfb..2f97b8835f0 100644 --- a/browser/ui/views/location_bar/brave_location_bar_view.cc +++ b/browser/ui/views/location_bar/brave_location_bar_view.cc @@ -14,10 +14,13 @@ #include "brave/browser/profiles/profile_util.h" #include "brave/browser/themes/brave_theme_service.h" #include "brave/browser/ui/color/brave_color_id.h" +#include "brave/browser/ui/commander/commander_service_factory.h" #include "brave/browser/ui/views/brave_actions/brave_actions_container.h" #include "brave/browser/ui/views/location_bar/brave_news_location_view.h" #include "brave/browser/ui/views/toolbar/brave_toolbar_view.h" #include "brave/components/brave_news/common/features.h" +#include "brave/components/commander/browser/commander_frontend_delegate.h" +#include "brave/components/commander/common/features.h" #include "brave/components/l10n/common/localization_util.h" #include "brave/grit/brave_theme_resources.h" #include "chrome/browser/profiles/profile.h" @@ -89,8 +92,9 @@ void BraveLocationBarView::Init() { focus_ring->SetPathGenerator( std::make_unique< BraveLocationBarViewFocusRingHighlightPathGenerator>()); - if (const auto color_id = GetFocusRingColor(profile())) + if (const auto color_id = GetFocusRingColor(profile())) { focus_ring->SetColorId(color_id.value()); + } } if (base::FeatureList::IsEnabled(brave_news::features::kBraveNewsFeature) && @@ -119,8 +123,9 @@ void BraveLocationBarView::Init() { Update(nullptr); // Stop slide animation for all content settings views icon. - for (auto* content_setting_view : content_setting_views_) + for (auto* content_setting_view : content_setting_views_) { content_setting_view->disable_animation(); + } } bool BraveLocationBarView::ShouldShowIPFSLocationView() const { @@ -128,8 +133,9 @@ bool BraveLocationBarView::ShouldShowIPFSLocationView() const { const GURL& url = GetLocationBarModel()->GetURL(); if (!ipfs::IpfsServiceFactory::IsIpfsEnabled(profile_) || !ipfs::IsIPFSScheme(url) || - !ipfs::IsLocalGatewayConfigured(profile_->GetPrefs())) + !ipfs::IsLocalGatewayConfigured(profile_->GetPrefs())) { return false; + } return true; #else @@ -146,12 +152,14 @@ void BraveLocationBarView::Update(content::WebContents* contents) { auto show_page_actions = !ShouldHidePageActionIcons(); #if BUILDFLAG(ENABLE_TOR) - if (onion_location_view_) + if (onion_location_view_) { onion_location_view_->Update(contents, show_page_actions); + } #endif #if BUILDFLAG(ENABLE_IPFS) - if (ipfs_location_view_) + if (ipfs_location_view_) { ipfs_location_view_->Update(contents, show_page_actions); + } #endif if (brave_news_location_view_) { @@ -160,12 +168,14 @@ void BraveLocationBarView::Update(content::WebContents* contents) { LocationBarView::Update(contents); - if (!ShouldShowIPFSLocationView()) + if (!ShouldShowIPFSLocationView()) { return; + } // Secure display text for a page was set by chromium. // We do not want to override this. - if (!GetLocationBarModel()->GetSecureDisplayText().empty()) + if (!GetLocationBarModel()->GetSecureDisplayText().empty()) { return; + } auto badge_text = brave_l10n::GetLocalizedResourceUTF16String(IDS_IPFS_BADGE_TITLE); location_icon_view()->SetLabel(badge_text); @@ -174,30 +184,40 @@ void BraveLocationBarView::Update(content::WebContents* contents) { ui::ImageModel BraveLocationBarView::GetLocationIcon( LocationIconView::Delegate::IconFetchedCallback on_icon_fetched) const { if (!ShouldShowIPFSLocationView() || - !omnibox_view_->model()->ShouldShowCurrentPageIcon()) + !omnibox_view_->model()->ShouldShowCurrentPageIcon()) { return LocationBarView::GetLocationIcon(std::move(on_icon_fetched)); + } auto& bundle = ui::ResourceBundle::GetSharedInstance(); const auto& ipfs_logo = *bundle.GetImageSkiaNamed(IDR_BRAVE_IPFS_LOGO); return ui::ImageModel::FromImageSkia(ipfs_logo); } +void BraveLocationBarView::OnOmniboxBlurred() { + if (commander::CommanderEnabled()) { + commander::CommanderServiceFactory::GetForBrowserContext(profile_)->Hide(); + } + LocationBarView::OnOmniboxBlurred(); +} + void BraveLocationBarView::OnChanged() { auto hide_page_actions = ShouldHidePageActionIcons(); if (brave_actions_) { brave_actions_->SetShouldHide(hide_page_actions); } #if BUILDFLAG(ENABLE_TOR) - if (onion_location_view_) + if (onion_location_view_) { onion_location_view_->Update( browser_->tab_strip_model()->GetActiveWebContents(), !hide_page_actions); + } #endif #if BUILDFLAG(ENABLE_IPFS) - if (ipfs_location_view_) + if (ipfs_location_view_) { ipfs_location_view_->Update( browser_->tab_strip_model()->GetActiveWebContents(), !hide_page_actions); + } #endif if (brave_news_location_view_) { @@ -210,19 +230,23 @@ void BraveLocationBarView::OnChanged() { std::vector BraveLocationBarView::GetTrailingViews() { std::vector views; - if (brave_news_location_view_) + if (brave_news_location_view_) { views.push_back(brave_news_location_view_); + } #if BUILDFLAG(ENABLE_TOR) - if (onion_location_view_) + if (onion_location_view_) { views.push_back(onion_location_view_); + } #endif #if BUILDFLAG(ENABLE_IPFS) - if (ipfs_location_view_) + if (ipfs_location_view_) { views.push_back(ipfs_location_view_); + } #endif - if (brave_actions_) + if (brave_actions_) { views.push_back(brave_actions_); + } return views; } @@ -261,8 +285,9 @@ gfx::Size BraveLocationBarView::CalculatePreferredSize() const { void BraveLocationBarView::OnThemeChanged() { LocationBarView::OnThemeChanged(); - if (!IsInitialized()) + if (!IsInitialized()) { return; + } Update(nullptr); RefreshBackground(); diff --git a/browser/ui/views/location_bar/brave_location_bar_view.h b/browser/ui/views/location_bar/brave_location_bar_view.h index cf04aaef82e..13c9a731865 100644 --- a/browser/ui/views/location_bar/brave_location_bar_view.h +++ b/browser/ui/views/location_bar/brave_location_bar_view.h @@ -56,6 +56,7 @@ class BraveLocationBarView : public LocationBarView { ui::ImageModel GetLocationIcon(LocationIconView::Delegate::IconFetchedCallback on_icon_fetched) const override; + void OnOmniboxBlurred() override; // views::View: gfx::Size CalculatePreferredSize() const override; diff --git a/chromium_src/chrome/browser/DEPS b/chromium_src/chrome/browser/DEPS index a8e0d392904..f4c12eb3bb4 100644 --- a/chromium_src/chrome/browser/DEPS +++ b/chromium_src/chrome/browser/DEPS @@ -18,6 +18,7 @@ include_rules = [ "+brave/components/brave_wallet", "+brave/components/brave_webtorrent/browser", "+brave/components/brave_webtorrent/grit", + "+brave/components/commander", "+brave/components/constants", "+brave/components/content_settings/core", "+brave/components/debounce", diff --git a/chromium_src/chrome/browser/about_flags.cc b/chromium_src/chrome/browser/about_flags.cc index 6affb62b5d4..fa97397cde1 100644 --- a/chromium_src/chrome/browser/about_flags.cc +++ b/chromium_src/chrome/browser/about_flags.cc @@ -3,7 +3,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ - #include "brave/browser/about_flags.cc" +#include "brave/components/commander/common/features.h" +// Note: We replace the kQuickCommands feature with the kBraveCommander feature +// so we can use it from //components without DEPS violations. +#define kQuickCommands kBraveCommander #include "src/chrome/browser/about_flags.cc" +#undef kQuickCommands diff --git a/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc b/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc new file mode 100644 index 00000000000..75e14afbdae --- /dev/null +++ b/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc @@ -0,0 +1,16 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc" + +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) +#include "brave/browser/ui/commander/commander_service_factory.h" +#include "brave/components/commander/browser/commander_frontend_delegate.h" + +commander::CommanderFrontendDelegate* +ChromeAutocompleteProviderClient::GetCommanderDelegate() { + return commander::CommanderServiceFactory::GetForBrowserContext(profile_); +} +#endif diff --git a/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h b/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h new file mode 100644 index 00000000000..780a410d4a3 --- /dev/null +++ b/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h @@ -0,0 +1,22 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_CHROME_BROWSER_AUTOCOMPLETE_CHROME_AUTOCOMPLETE_PROVIDER_CLIENT_H_ +#define BRAVE_CHROMIUM_SRC_CHROME_BROWSER_AUTOCOMPLETE_CHROME_AUTOCOMPLETE_PROVIDER_CLIENT_H_ + +#include "components/omnibox/browser/autocomplete_provider_client.h" + +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) +#include "brave/components/commander/browser/commander_frontend_delegate.h" +#define GetBookmarkModel \ + GetBookmarkModel() override; \ + commander::CommanderFrontendDelegate* GetCommanderDelegate +#endif + +#include "src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h" // IWYU pragma: export + +#undef GetBookmarkModel + +#endif // BRAVE_CHROMIUM_SRC_CHROME_BROWSER_AUTOCOMPLETE_CHROME_AUTOCOMPLETE_PROVIDER_CLIENT_H_ diff --git a/chromium_src/chrome/browser/ui/browser_commands.cc b/chromium_src/chrome/browser/ui/browser_commands.cc index a8bd6747364..0eccfd9ffc6 100644 --- a/chromium_src/chrome/browser/ui/browser_commands.cc +++ b/chromium_src/chrome/browser/ui/browser_commands.cc @@ -5,14 +5,18 @@ #include "brave/browser/ui/browser_commands.h" +#include "brave/browser/ui/commander/commander_service_factory.h" +#include "brave/components/commander/browser/commander_frontend_delegate.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser_commands.h" +#define ToggleCommander ToggleCommander_ChromiumImpl #define ReloadBypassingCache ReloadBypassingCache_ChromiumImpl #define GetReadingListModel GetReadingListModel_ChromiumImpl #include "src/chrome/browser/ui/browser_commands.cc" #undef ReloadBypassingCache #undef GetReadingListModel +#undef ToggleCommander namespace chrome { @@ -20,14 +24,20 @@ void ReloadBypassingCache(Browser* browser, WindowOpenDisposition disposition) { Profile* profile = browser->profile(); DCHECK(profile); // NewTorConnectionForSite will do hard reload after obtaining new identity - if (profile->IsTor()) + if (profile->IsTor()) { brave::NewTorConnectionForSite(browser); - else + } else { ReloadBypassingCache_ChromiumImpl(browser, disposition); + } } ReadingListModel* GetReadingListModel(Browser* browser) { return nullptr; } +void ToggleCommander(Browser* browser) { + commander::CommanderServiceFactory::GetForBrowserContext(browser->profile()) + ->Toggle(); +} + } // namespace chrome diff --git a/chromium_src/chrome/browser/ui/commander/commander.cc b/chromium_src/chrome/browser/ui/commander/commander.cc new file mode 100644 index 00000000000..7a25ea6bff2 --- /dev/null +++ b/chromium_src/chrome/browser/ui/commander/commander.cc @@ -0,0 +1,13 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/commander/common/features.h" +#include "chrome/browser/ui/ui_features.h" + +// We replace Chromium's flag with one of our own, because we want it to be +// accessible from //components (which //chrome/ui/browser is not). +#define kQuickCommands kBraveCommander +#include "src/chrome/browser/ui/commander/commander.cc" +#undef kQuickCommands diff --git a/chromium_src/chrome/browser/ui/commander/fuzzy_finder.cc b/chromium_src/chrome/browser/ui/commander/fuzzy_finder.cc new file mode 100644 index 00000000000..35699df5faa --- /dev/null +++ b/chromium_src/chrome/browser/ui/commander/fuzzy_finder.cc @@ -0,0 +1,13 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +// By default the Fuzzy finder doesn't return any results for the empty string. +// However, this isn't ideal because it's not obvious what the input is for. +// It's much better if we can show a bunch of commands. +#define BRAVE_FUZZY_FINDER_NEEDLE_SIZE_CHECK &&false + +#include "src/chrome/browser/ui/commander/fuzzy_finder.cc" + +#undef BRAVE_FUZZY_FINDER_NEEDLE_SIZE_CHECK diff --git a/chromium_src/chrome/browser/ui/views/location_bar/location_bar_view.h b/chromium_src/chrome/browser/ui/views/location_bar/location_bar_view.h index 430cb1e1c26..202c98d8c21 100644 --- a/chromium_src/chrome/browser/ui/views/location_bar/location_bar_view.h +++ b/chromium_src/chrome/browser/ui/views/location_bar/location_bar_view.h @@ -13,9 +13,11 @@ public: \ virtual std::vector GetTrailingViews(); +#define OnOmniboxBlurred virtual OnOmniboxBlurred #define GetBorderRadius virtual GetBorderRadius #include "src/chrome/browser/ui/views/location_bar/location_bar_view.h" // IWYU pragma: export #undef GetBorderRadius +#undef OnOmniboxBlurred #undef BRAVE_LOCATION_BAR_VIEW_H_ #endif // BRAVE_CHROMIUM_SRC_CHROME_BROWSER_UI_VIEWS_LOCATION_BAR_LOCATION_BAR_VIEW_H_ diff --git a/chromium_src/components/omnibox/browser/DEPS b/chromium_src/components/omnibox/browser/DEPS index 3581531d5f9..58ea16430d3 100644 --- a/chromium_src/components/omnibox/browser/DEPS +++ b/chromium_src/components/omnibox/browser/DEPS @@ -1,5 +1,6 @@ include_rules = [ "+brave/components/brave_search_conversion", + "+brave/components/commander", "+brave/components/omnibox/browser", "+content/public/common", ] diff --git a/chromium_src/components/omnibox/browser/actions/omnibox_action.h b/chromium_src/components/omnibox/browser/actions/omnibox_action.h new file mode 100644 index 00000000000..22ee6df0b9d --- /dev/null +++ b/chromium_src/components/omnibox/browser/actions/omnibox_action.h @@ -0,0 +1,20 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_ACTIONS_OMNIBOX_ACTION_H_ +#define BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_ACTIONS_OMNIBOX_ACTION_H_ + +#include "brave/components/commander/browser/commander_frontend_delegate.h" + +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) +#define OpenSharingHub \ + OpenSharingHub() = 0; \ + virtual commander::CommanderFrontendDelegate* GetCommanderDelegate +#endif + +#include "src/components/omnibox/browser/actions/omnibox_action.h" // IWYU pragma: export +#undef OpenSharingHub + +#endif // BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_ACTIONS_OMNIBOX_ACTION_H_ diff --git a/chromium_src/components/omnibox/browser/autocomplete_controller.cc b/chromium_src/components/omnibox/browser/autocomplete_controller.cc index 3298e4d5dc3..1fca6b9fb4b 100644 --- a/chromium_src/components/omnibox/browser/autocomplete_controller.cc +++ b/chromium_src/components/omnibox/browser/autocomplete_controller.cc @@ -3,7 +3,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include +#include + +#include "base/memory/scoped_refptr.h" +#include "base/ranges/algorithm.h" +#include "base/strings/string_util.h" #include "brave/components/brave_search_conversion/utils.h" +#include "brave/components/commander/common/constants.h" +#include "brave/components/commander/common/features.h" #include "brave/components/omnibox/browser/brave_bookmark_provider.h" #include "brave/components/omnibox/browser/brave_history_quick_provider.h" #include "brave/components/omnibox/browser/brave_history_url_provider.h" @@ -13,12 +21,61 @@ #include "brave/components/omnibox/browser/promotion_provider.h" #include "brave/components/omnibox/browser/promotion_utils.h" #include "brave/components/omnibox/browser/topsites_provider.h" +#include "components/omnibox/browser/autocomplete_controller.h" +#include "components/omnibox/browser/autocomplete_input.h" +#include "components/omnibox/browser/autocomplete_match.h" #include "components/omnibox/browser/clipboard_provider.h" #include "components/omnibox/browser/history_cluster_provider.h" #include "components/omnibox/browser/history_fuzzy_provider.h" +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) +#include "brave/components/omnibox/browser/commander_provider.h" +#endif + using brave_search_conversion::IsBraveSearchConversionFetureEnabled; +namespace { +// If this input has triggered the commander then only show commander results. +void MaybeShowCommands(AutocompleteResult* result, + const AutocompleteInput& input) { +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) + // If this input isn't a command, return and don't do any work. + if (!commander::CommanderEnabled() || + !base::StartsWith(input.text(), commander::kCommandPrefix)) { + return; + } + + uint32_t seen_commands = 0; + + // Move all the commands to the top of the Omnibox suggestions. + for (uint32_t i = 0; i < result->size(); ++i) { + auto* match = result->match_at(i); + if (match->provider->type() != AutocompleteProvider::TYPE_BRAVE_COMMANDER) { + continue; + } + + result->ReorderMatch(result->begin() + i, seen_commands++); + } + + // Remove all results after the commands. + for (auto it = result->end() - 1; it >= result->begin() + seen_commands; + --it) { + result->RemoveMatch(it); + } +#endif +} + +void MaybeAddCommanderProvider(AutocompleteController::Providers& providers, + AutocompleteController* controller) { +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) + if (commander::CommanderEnabled()) { + providers.push_back(base::MakeRefCounted( + controller->autocomplete_provider_client(), controller)); + } +#endif +} +} // namespace + #define SearchProvider BraveSearchProvider #define HistoryQuickProvider BraveHistoryQuickProvider #define HistoryURLProvider BraveHistoryURLProvider @@ -26,6 +83,7 @@ using brave_search_conversion::IsBraveSearchConversionFetureEnabled; #define BookmarkProvider BraveBookmarkProvider #define ShortcutsProvider BraveShortcutsProvider #define BRAVE_AUTOCOMPLETE_CONTROLLER_AUTOCOMPLETE_CONTROLLER \ + MaybeAddCommanderProvider(providers_, this); \ providers_.push_back(new TopSitesProvider(provider_client_.get())); \ if (IsBraveSearchConversionFetureEnabled() && \ !provider_client_->IsOffTheRecord()) \ @@ -35,7 +93,8 @@ using brave_search_conversion::IsBraveSearchConversionFetureEnabled; // AutocompleteController::UpdateResult() because result should be updated // before notifying at the last of UpdateResult(). #define BRAVE_AUTOCOMPLETE_CONTROLLER_UPDATE_RESULT \ - SortBraveSearchPromotionMatch(&result_); + SortBraveSearchPromotionMatch(&result_); \ + MaybeShowCommands(&result_, input_); #include "src/components/omnibox/browser/autocomplete_controller.cc" diff --git a/chromium_src/components/omnibox/browser/autocomplete_provider.cc b/chromium_src/components/omnibox/browser/autocomplete_provider.cc new file mode 100644 index 00000000000..3f79bbfc24f --- /dev/null +++ b/chromium_src/components/omnibox/browser/autocomplete_provider.cc @@ -0,0 +1,15 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/chromium_src/components/omnibox/browser/autocomplete_provider.h" + +// This is a bit of a hack to just make COMMANDER use the same metrics/friendly +// name as BOOKMARK. It's easier than patching things and we don't use the +// metrics and the name is mostly useful for debugging. +#define TYPE_BOOKMARK \ + TYPE_BRAVE_COMMANDER: \ + case TYPE_BOOKMARK +#include "src/components/omnibox/browser/autocomplete_provider.cc" +#undef TYPE_BOOKMARK diff --git a/chromium_src/components/omnibox/browser/autocomplete_provider.h b/chromium_src/components/omnibox/browser/autocomplete_provider.h new file mode 100644 index 00000000000..ce6a3ee9ad1 --- /dev/null +++ b/chromium_src/components/omnibox/browser/autocomplete_provider.h @@ -0,0 +1,15 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_AUTOCOMPLETE_PROVIDER_H_ +#define BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_AUTOCOMPLETE_PROVIDER_H_ + +// Note: We go negative with the BraveAutoCompleteTypes, so we don't conflict if +// Chromium adds something new. +#define TYPE_BOOKMARK TYPE_BRAVE_COMMANDER = -1 << 0, TYPE_BOOKMARK +#include "src/components/omnibox/browser/autocomplete_provider.h" // IWYU pragma: export +#undef TYPE_BOOKMARK + +#endif // BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_AUTOCOMPLETE_PROVIDER_H_ diff --git a/chromium_src/components/omnibox/browser/mock_autocomplete_provider_client.h b/chromium_src/components/omnibox/browser/mock_autocomplete_provider_client.h new file mode 100644 index 00000000000..347cad4a9d1 --- /dev/null +++ b/chromium_src/components/omnibox/browser/mock_autocomplete_provider_client.h @@ -0,0 +1,35 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_MOCK_AUTOCOMPLETE_PROVIDER_CLIENT_H_ +#define BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_MOCK_AUTOCOMPLETE_PROVIDER_CLIENT_H_ + +#include "components/omnibox/browser/autocomplete_provider_client.h" + +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) +#include "brave/components/commander/browser/commander_frontend_delegate.h" +#define GetTopSites \ + GetTopSites_Unused() { \ + return nullptr; \ + } \ + commander::CommanderFrontendDelegate* GetCommanderDelegate() { \ + return commander_delegate_.get(); \ + } \ + void set_commander_delegate( \ + std::unique_ptr delegate) { \ + commander_delegate_ = std::move(delegate); \ + } \ + \ + private: \ + std::unique_ptr commander_delegate_; \ + \ + public: \ + scoped_refptr GetTopSites +#endif + +#include "src/components/omnibox/browser/mock_autocomplete_provider_client.h" // IWYU pragma: export +#undef GetTopSites + +#endif // BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_MOCK_AUTOCOMPLETE_PROVIDER_CLIENT_H_ diff --git a/chromium_src/components/omnibox/browser/omnibox_edit_model.h b/chromium_src/components/omnibox/browser/omnibox_edit_model.h new file mode 100644 index 00000000000..feb7f3b8d08 --- /dev/null +++ b/chromium_src/components/omnibox/browser/omnibox_edit_model.h @@ -0,0 +1,15 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_OMNIBOX_EDIT_MODEL_H_ +#define BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_OMNIBOX_EDIT_MODEL_H_ + +#include "components/omnibox/browser/omnibox_view.h" + +#define CanPasteAndGo virtual CanPasteAndGo +#include "src/components/omnibox/browser/omnibox_edit_model.h" // IWYU pragma: export +#undef CanPasteAndGo + +#endif // BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_OMNIBOX_EDIT_MODEL_H_ diff --git a/chromium_src/components/omnibox/browser/omnibox_view.cc b/chromium_src/components/omnibox/browser/omnibox_view.cc new file mode 100644 index 00000000000..d4fbe813461 --- /dev/null +++ b/chromium_src/components/omnibox/browser/omnibox_view.cc @@ -0,0 +1,10 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/omnibox/browser/brave_omnibox_edit_model.h" + +#define OmniboxEditModel BraveOmniboxEditModel +#include "src/components/omnibox/browser/omnibox_view.cc" +#undef OmniboxEditModel diff --git a/components/commander/browser/BUILD.gn b/components/commander/browser/BUILD.gn new file mode 100644 index 00000000000..9d863f52939 --- /dev/null +++ b/components/commander/browser/BUILD.gn @@ -0,0 +1,25 @@ +# Copyright (c) 2023 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +assert(!is_android && !is_ios) + +component("browser") { + output_name = "commander_browser" + defines = [ "IS_COMMANDER_BROWSER_IMPL" ] + + sources = [ + "commander_frontend_delegate.h", + "commander_item_model.cc", + "commander_item_model.h", + ] + + deps = [ + "//base", + "//ui/gfx", + "//url", + ] + + public_deps = [ "//brave/components/commander/common" ] +} diff --git a/components/commander/browser/DEPS b/components/commander/browser/DEPS new file mode 100644 index 00000000000..672366269dc --- /dev/null +++ b/components/commander/browser/DEPS @@ -0,0 +1,8 @@ +# Copyright (c) 2023 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +include_rules = [ + "+ui/gfx" +] diff --git a/components/commander/browser/commander_frontend_delegate.h b/components/commander/browser/commander_frontend_delegate.h new file mode 100644 index 00000000000..0d7db8a44b2 --- /dev/null +++ b/components/commander/browser/commander_frontend_delegate.h @@ -0,0 +1,39 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_COMMANDER_BROWSER_COMMANDER_FRONTEND_DELEGATE_H_ +#define BRAVE_COMPONENTS_COMMANDER_BROWSER_COMMANDER_FRONTEND_DELEGATE_H_ + +#include +#include + +#include "base/component_export.h" +#include "base/observer_list_types.h" +#include "brave/components/commander/browser/commander_item_model.h" + +namespace commander { +class COMPONENT_EXPORT(COMMANDER_BROWSER) CommanderFrontendDelegate { + public: + class Observer : public base::CheckedObserver { + public: + virtual void OnCommanderUpdated() = 0; + }; + + virtual ~CommanderFrontendDelegate() {} + + virtual void Toggle() = 0; + virtual void Hide() = 0; + virtual void AddObserver(Observer* observer) = 0; + virtual void RemoveObserver(Observer* observer) = 0; + virtual void SelectCommand(uint32_t command_index, + uint32_t result_set_id) = 0; + virtual void UpdateText(bool force = false) = 0; + virtual std::vector GetItems() = 0; + virtual int GetResultSetId() = 0; + virtual const std::u16string& GetPrompt() = 0; +}; +} // namespace commander + +#endif // BRAVE_COMPONENTS_COMMANDER_BROWSER_COMMANDER_FRONTEND_DELEGATE_H_ diff --git a/components/commander/browser/commander_item_model.cc b/components/commander/browser/commander_item_model.cc new file mode 100644 index 00000000000..904ab9e4a83 --- /dev/null +++ b/components/commander/browser/commander_item_model.cc @@ -0,0 +1,21 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/commander/browser/commander_item_model.h" + +namespace commander { + +CommandItemModel::CommandItemModel( + const std::u16string& title, + const std::vector& matched_ranges, + const std::u16string& annotation) + : title(title), matched_ranges(matched_ranges), annotation(annotation) {} + +CommandItemModel::CommandItemModel(const CommandItemModel& other) = default; +CommandItemModel::CommandItemModel(CommandItemModel&& other) = default; + +CommandItemModel::~CommandItemModel() = default; + +} // namespace commander diff --git a/components/commander/browser/commander_item_model.h b/components/commander/browser/commander_item_model.h new file mode 100644 index 00000000000..7eb719c3f4c --- /dev/null +++ b/components/commander/browser/commander_item_model.h @@ -0,0 +1,37 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_COMMANDER_BROWSER_COMMANDER_ITEM_MODEL_H_ +#define BRAVE_COMPONENTS_COMMANDER_BROWSER_COMMANDER_ITEM_MODEL_H_ + +#include +#include + +#include "base/component_export.h" +#include "ui/gfx/range/range.h" + +namespace commander { + +// See chrome/browser/ui/commander/commander_view_model.h for details on these +// structs. They exist to get around some deps violations. +struct COMPONENT_EXPORT(COMMANDER_BROWSER) CommandItemModel { + public: + CommandItemModel(const std::u16string& title, + const std::vector& matched_ranges, + const std::u16string& annotation); + ~CommandItemModel(); + CommandItemModel(const CommandItemModel& other); + CommandItemModel(CommandItemModel&& other); + CommandItemModel& operator=(const CommandItemModel& other) = default; + CommandItemModel& operator=(CommandItemModel&& other) = default; + + std::u16string title; + std::vector matched_ranges; + std::u16string annotation; +}; + +} // namespace commander + +#endif // BRAVE_COMPONENTS_COMMANDER_BROWSER_COMMANDER_ITEM_MODEL_H_ diff --git a/components/commander/common/BUILD.gn b/components/commander/common/BUILD.gn new file mode 100644 index 00000000000..49f4016eee2 --- /dev/null +++ b/components/commander/common/BUILD.gn @@ -0,0 +1,20 @@ +# Copyright (c) 2023 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +assert(!is_android && !is_ios) + +component("common") { + output_name = "commander_common" + defines = [ "IS_COMMANDER_COMMON_IMPL" ] + + sources = [ + "constants.cc", + "constants.h", + "features.cc", + "features.h", + ] + + deps = [ "//base" ] +} diff --git a/components/commander/common/constants.cc b/components/commander/common/constants.cc new file mode 100644 index 00000000000..009938b3183 --- /dev/null +++ b/components/commander/common/constants.cc @@ -0,0 +1,14 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/commander/common/constants.h" + +#include "base/strings/string_piece.h" + +namespace commander { + +const base::StringPiece16 kCommandPrefix(u":>"); + +} diff --git a/components/commander/common/constants.h b/components/commander/common/constants.h new file mode 100644 index 00000000000..007b3bfde54 --- /dev/null +++ b/components/commander/common/constants.h @@ -0,0 +1,21 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_COMMANDER_COMMON_CONSTANTS_H_ +#define BRAVE_COMPONENTS_COMMANDER_COMMON_CONSTANTS_H_ + +#include + +#include "base/component_export.h" +#include "base/strings/string_piece.h" + +namespace commander { + +COMPONENT_EXPORT(COMMANDER_COMMON) +extern const base::StringPiece16 kCommandPrefix; + +} // namespace commander + +#endif // BRAVE_COMPONENTS_COMMANDER_COMMON_CONSTANTS_H_ diff --git a/components/commander/common/features.cc b/components/commander/common/features.cc new file mode 100644 index 00000000000..f87bc664bf1 --- /dev/null +++ b/components/commander/common/features.cc @@ -0,0 +1,23 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/commander/common/features.h" + +#include "base/feature_list.h" + +namespace features { + +BASE_FEATURE(kBraveCommander, + "BraveCommander", + base::FEATURE_DISABLED_BY_DEFAULT); +} + +namespace commander { + +bool CommanderEnabled() { + return base::FeatureList::IsEnabled(features::kBraveCommander); +} + +} // namespace commander diff --git a/components/commander/common/features.h b/components/commander/common/features.h new file mode 100644 index 00000000000..93d3b0eb48d --- /dev/null +++ b/components/commander/common/features.h @@ -0,0 +1,26 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_COMMANDER_COMMON_FEATURES_H_ +#define BRAVE_COMPONENTS_COMMANDER_COMMON_FEATURES_H_ + +#include "base/component_export.h" +#include "base/feature_list.h" + +namespace features { + +// Note: This flag is declared in features rather than commander::features so we +// can replace the upstream flag with it more easily. +COMPONENT_EXPORT(COMMANDER_COMMON) BASE_DECLARE_FEATURE(kBraveCommander); + +} // namespace features + +namespace commander { + +COMPONENT_EXPORT(COMMANDER_COMMON) bool CommanderEnabled(); + +} // namespace commander + +#endif // BRAVE_COMPONENTS_COMMANDER_COMMON_FEATURES_H_ diff --git a/components/omnibox/browser/BUILD.gn b/components/omnibox/browser/BUILD.gn index 4adb7c52461..e4312a5c0d2 100644 --- a/components/omnibox/browser/BUILD.gn +++ b/components/omnibox/browser/BUILD.gn @@ -41,4 +41,11 @@ source_set("unit_tests") { "//testing/gmock", "//testing/gtest", ] + + if (!is_android && !is_ios) { + sources += + [ "//brave/components/omnibox/browser/commander_provider_unittest.cc" ] + + deps += [ "//brave/components/commander/browser" ] + } } diff --git a/components/omnibox/browser/DEPS b/components/omnibox/browser/DEPS index e69de29bb2d..672366269dc 100644 --- a/components/omnibox/browser/DEPS +++ b/components/omnibox/browser/DEPS @@ -0,0 +1,8 @@ +# Copyright (c) 2023 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +include_rules = [ + "+ui/gfx" +] diff --git a/components/omnibox/browser/brave_omnibox_edit_model.cc b/components/omnibox/browser/brave_omnibox_edit_model.cc new file mode 100644 index 00000000000..ecabc54f462 --- /dev/null +++ b/components/omnibox/browser/brave_omnibox_edit_model.cc @@ -0,0 +1,26 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/omnibox/browser/brave_omnibox_edit_model.h" + +#include "base/strings/string_util.h" + +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) +#include "brave/components/commander/common/constants.h" +#include "brave/components/commander/common/features.h" +#endif + +BraveOmniboxEditModel::~BraveOmniboxEditModel() = default; + +bool BraveOmniboxEditModel::CanPasteAndGo(const std::u16string& text) const { +#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) + if (commander::CommanderEnabled() && + base::StartsWith(text, commander::kCommandPrefix)) { + return false; + } +#endif + + return OmniboxEditModel::CanPasteAndGo(text); +} diff --git a/components/omnibox/browser/brave_omnibox_edit_model.h b/components/omnibox/browser/brave_omnibox_edit_model.h new file mode 100644 index 00000000000..0b92da5e014 --- /dev/null +++ b/components/omnibox/browser/brave_omnibox_edit_model.h @@ -0,0 +1,21 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_OMNIBOX_BROWSER_BRAVE_OMNIBOX_EDIT_MODEL_H_ +#define BRAVE_COMPONENTS_OMNIBOX_BROWSER_BRAVE_OMNIBOX_EDIT_MODEL_H_ + +#include + +#include "components/omnibox/browser/omnibox_edit_model.h" + +class BraveOmniboxEditModel : public OmniboxEditModel { + public: + using OmniboxEditModel::OmniboxEditModel; + ~BraveOmniboxEditModel() override; + + bool CanPasteAndGo(const std::u16string& text) const override; +}; + +#endif // BRAVE_COMPONENTS_OMNIBOX_BROWSER_BRAVE_OMNIBOX_EDIT_MODEL_H_ diff --git a/components/omnibox/browser/commander_action.cc b/components/omnibox/browser/commander_action.cc new file mode 100644 index 00000000000..08c3c8d530a --- /dev/null +++ b/components/omnibox/browser/commander_action.cc @@ -0,0 +1,21 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/omnibox/browser/commander_action.h" + +namespace commander { + +CommanderAction::CommanderAction(uint32_t command_index, uint32_t result_set_id) + : OmniboxAction({}, GURL(), true), + command_index_(command_index), + result_set_id_(result_set_id) {} +CommanderAction::~CommanderAction() = default; + +void CommanderAction::Execute(ExecutionContext& context) const { + context.client_->GetCommanderDelegate()->SelectCommand(command_index_, + result_set_id_); +} + +} // namespace commander diff --git a/components/omnibox/browser/commander_action.h b/components/omnibox/browser/commander_action.h new file mode 100644 index 00000000000..0c2547eb7a9 --- /dev/null +++ b/components/omnibox/browser/commander_action.h @@ -0,0 +1,31 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_OMNIBOX_BROWSER_COMMANDER_ACTION_H_ +#define BRAVE_COMPONENTS_OMNIBOX_BROWSER_COMMANDER_ACTION_H_ + +#include "components/omnibox/browser/actions/omnibox_action.h" + +namespace commander { + +class CommanderAction : public OmniboxAction { + public: + CommanderAction(uint32_t command_index, uint32_t result_set_id); + CommanderAction(const CommanderAction&) = delete; + CommanderAction& operator=(const CommanderAction&) = delete; + + void Execute(ExecutionContext& context) const override; + + protected: + ~CommanderAction() override; + + private: + uint32_t command_index_; + uint32_t result_set_id_; +}; + +} // namespace commander + +#endif // BRAVE_COMPONENTS_OMNIBOX_BROWSER_COMMANDER_ACTION_H_ diff --git a/components/omnibox/browser/commander_provider.cc b/components/omnibox/browser/commander_provider.cc new file mode 100644 index 00000000000..3d181a8aef6 --- /dev/null +++ b/components/omnibox/browser/commander_provider.cc @@ -0,0 +1,116 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/omnibox/browser/commander_provider.h" + +#include +#include + +#include "base/functional/bind.h" +#include "base/functional/callback_helpers.h" +#include "base/memory/scoped_refptr.h" +#include "base/strings/strcat.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "brave/components/commander/browser/commander_frontend_delegate.h" +#include "brave/components/commander/browser/commander_item_model.h" +#include "brave/components/commander/common/constants.h" +#include "brave/components/omnibox/browser/commander_action.h" +#include "components/omnibox/browser/autocomplete_match.h" +#include "components/omnibox/browser/autocomplete_match_type.h" +#include "components/omnibox/browser/autocomplete_provider_client.h" +#include "components/omnibox/browser/autocomplete_provider_listener.h" + +namespace commander { +CommanderProvider::CommanderProvider(AutocompleteProviderClient* client, + AutocompleteProviderListener* listener) + : AutocompleteProvider(AutocompleteProvider::TYPE_BRAVE_COMMANDER), + client_(client) { + if (listener) { + AddListener(listener); + } + + observation_.Observe(client_->GetCommanderDelegate()); +} + +CommanderProvider::~CommanderProvider() = default; + +void CommanderProvider::Start(const AutocompleteInput& input, + bool minimal_changes) { + if (minimal_changes) { + return; + } + + matches_.clear(); + last_input_ = input.text(); + client_->GetCommanderDelegate()->UpdateText(); +} + +void CommanderProvider::OnCommanderUpdated() { + auto* delegate = client_->GetCommanderDelegate(); + matches_.clear(); + + if (!last_input_.starts_with(commander::kCommandPrefix.data())) { + return; + } + + int rank = 10000; + const auto& items = delegate->GetItems(); + for (uint32_t i = 0; i < items.size(); ++i) { + const auto& option = items[i]; + AutocompleteMatch match(this, rank--, false, + AutocompleteMatchType::BOOKMARK_TITLE); + match.actions.push_back( + base::MakeRefCounted(i, delegate->GetResultSetId())); + + // This is neat but it would be nice if we could always show it instead of + // only when we have a result selected. + match.contents = option.annotation; + match.additional_text = delegate->GetPrompt(); + if (!option.annotation.empty()) { + match.contents_class = { + ACMatchClassification(0, ACMatchClassification::DIM)}; + } + match.description = + base::StrCat({commander::kCommandPrefix, u" ", option.title}); + match.allowed_to_be_default_match = true; + match.swap_contents_and_description = true; + // We don't want to change the prompt at all while the user is going through + // their options. + match.fill_into_edit = last_input_; + match.description_class = { + ACMatchClassification(0, ACMatchClassification::DIM)}; + + // All commands have a ":> " prefix added to them, so make sure we take it + // into account when mapping over the matched ranges. + const int offset = commander::kCommandPrefix.size() + 1; + for (size_t j = 0; j < option.matched_ranges.size(); ++j) { + auto range = option.matched_ranges[j]; + // If the match has no length (as in the case of the empty string match) + // don't highlight anything - zero length highlights trigger a DCHECK. + if (range.start() == range.end()) { + continue; + } + + // Start the match classification at the start of the matching range. + match.description_class.push_back(ACMatchClassification( + range.start() + offset, ACMatchClassification::MATCH)); + + // If the end of the range isn't the last character in the string, and + // this range doesn't intersect with the next one, change the + // classification back to DIM from the end of this range. + if (range.end() + offset < match.description.size() && + (j + 1 >= option.matched_ranges.size() || + option.matched_ranges[j + 1].start() > range.end())) { + match.description_class.push_back(ACMatchClassification( + range.end() + offset, ACMatchClassification::DIM)); + } + } + matches_.push_back(match); + } + + NotifyListeners(/* updated_matches= */ true); +} +} // namespace commander diff --git a/components/omnibox/browser/commander_provider.h b/components/omnibox/browser/commander_provider.h new file mode 100644 index 00000000000..4bf55389d58 --- /dev/null +++ b/components/omnibox/browser/commander_provider.h @@ -0,0 +1,51 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_OMNIBOX_BROWSER_COMMANDER_PROVIDER_H_ +#define BRAVE_COMPONENTS_OMNIBOX_BROWSER_COMMANDER_PROVIDER_H_ + +#include + +#include "base/memory/weak_ptr.h" +#include "base/scoped_observation.h" +#include "brave/components/commander/browser/commander_frontend_delegate.h" +#include "brave/components/commander/browser/commander_item_model.h" +#include "components/omnibox/browser/autocomplete_provider.h" +#include "components/omnibox/browser/autocomplete_provider_client.h" +#include "components/omnibox/browser/autocomplete_provider_listener.h" + +namespace commander { +class CommanderProvider + : public AutocompleteProvider, + public commander::CommanderFrontendDelegate::Observer { + public: + CommanderProvider(AutocompleteProviderClient* client, + AutocompleteProviderListener* listener); + CommanderProvider(const CommanderProvider&) = delete; + CommanderProvider& operator=(const CommanderProvider&) = delete; + + // AutocompleteProvider: + void Start(const AutocompleteInput& input, bool minimal_changes) override; + + private: + // Destructor for AutocompleteProvider must be private or protected as it + // extends |base::RefCountedThreadSafe|. + ~CommanderProvider() override; + + // commander::CommanderFrontendDelegate::Observer: + void OnCommanderUpdated() override; + + raw_ptr client_; + + base::ScopedObservation + observation_{this}; + + std::u16string last_input_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; +} // namespace commander + +#endif // BRAVE_COMPONENTS_OMNIBOX_BROWSER_COMMANDER_PROVIDER_H_ diff --git a/components/omnibox/browser/commander_provider_unittest.cc b/components/omnibox/browser/commander_provider_unittest.cc new file mode 100644 index 00000000000..02aab855add --- /dev/null +++ b/components/omnibox/browser/commander_provider_unittest.cc @@ -0,0 +1,349 @@ +// Copyright (c) 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/omnibox/browser/commander_provider.h" + +#include +#include +#include +#include +#include + +#include "base/memory/scoped_refptr.h" +#include "base/notreached.h" +#include "base/observer_list.h" +#include "base/strings/strcat.h" +#include "base/test/scoped_feature_list.h" +#include "brave/components/commander/browser/commander_frontend_delegate.h" +#include "brave/components/commander/browser/commander_item_model.h" +#include "brave/components/commander/common/constants.h" +#include "brave/components/commander/common/features.h" +#include "brave/components/omnibox/browser/brave_fake_autocomplete_provider_client.h" +#include "components/omnibox/browser/autocomplete_input.h" +#include "components/omnibox/browser/autocomplete_match.h" +#include "components/omnibox/browser/test_scheme_classifier.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/googletest/src/googletest/include/gtest/gtest.h" +#include "ui/gfx/range/range.h" + +namespace { +const unsigned int kClassificationOffset = + 1u + commander::kCommandPrefix.size(); + +class FakeCommanderDelegate : public commander::CommanderFrontendDelegate { + public: + FakeCommanderDelegate() = default; + ~FakeCommanderDelegate() override {} + + void AddObserver(Observer* observer) override { + Notify(items_); + observers_.AddObserver(observer); + } + void RemoveObserver(Observer* observer) override { + observers_.RemoveObserver(observer); + } + + void Toggle() override { showing_ = !showing_; } + + void Hide() override { showing_ = false; } + + std::vector GetItems() override { + return items_; + } + + int GetResultSetId() override { return notifies_; } + + const std::u16string& GetPrompt() override { return prompt_; } + + void Notify(const std::vector& items, + std::u16string prompt = std::u16string()) { + items_ = items; + notifies_++; + prompt_ = std::move(prompt); + + for (auto& observer : observers_) { + observer.OnCommanderUpdated(); + } + } + + void SelectCommand(uint32_t command_index, uint32_t result_set_id) override { + NOTIMPLEMENTED(); + } + + void UpdateText(bool force = false) override { NOTIMPLEMENTED(); } + + private: + base::ObserverList observers_; + std::u16string prompt_; + std::vector items_; + int notifies_ = 0; + bool showing_ = false; +}; + +AutocompleteInput CreateInput(std::u16string text) { + return AutocompleteInput(text, + metrics::OmniboxEventProto::PageClassification:: + OmniboxEventProto_PageClassification_NTP, + TestSchemeClassifier()); +} +} // namespace + +class CommanderProviderTest : public testing::Test { + public: + CommanderProviderTest() { + features_.InitAndEnableFeature(features::kBraveCommander); + } + + void SetUp() override { + client_.set_commander_delegate(std::make_unique()); + provider_ = + base::MakeRefCounted(&client_, nullptr); + } + + FakeCommanderDelegate* delegate() { + return static_cast(client_.GetCommanderDelegate()); + } + commander::CommanderProvider* provider() { return provider_.get(); } + + private: + base::test::ScopedFeatureList features_; + + BraveFakeAutocompleteProviderClient client_; + scoped_refptr provider_; +}; + +TEST_F(CommanderProviderTest, EmptyTextDoesNotTriggerProvider) { + delegate()->Notify({commander::CommandItemModel(u"First", {}, u"Ctrl+F")}); + + provider()->Start(CreateInput(u""), false); + EXPECT_EQ(0u, provider()->matches().size()); +} + +TEST_F(CommanderProviderTest, NonPrefixedTextDoesNotTriggerProvider) { + provider()->Start(CreateInput(u"Hello"), false); + EXPECT_EQ(0u, provider()->matches().size()); +} + +TEST_F(CommanderProviderTest, PrefixTriggersProvider) { + provider()->Start(CreateInput(u""), false); + EXPECT_EQ(0u, provider()->matches().size()); + + provider()->Start(CreateInput(commander::kCommandPrefix.data()), false); + EXPECT_EQ(0u, provider()->matches().size()); +} + +TEST_F(CommanderProviderTest, PrefixedCommandTriggersProvider) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u"Hello"})), false); + EXPECT_EQ(0u, provider()->matches().size()); +} + +TEST_F(CommanderProviderTest, PrefixWhiteSpaceIsStripped) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u" Hello"})), + false); + EXPECT_EQ(0u, provider()->matches().size()); +} + +TEST_F(CommanderProviderTest, ItemsAreConvertedToMatches) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u" Hello World"})), + false); + + delegate()->Notify({commander::CommandItemModel(u"First", {}, u"Ctrl+F"), + commander::CommandItemModel(u"Second", {}, u"Ctrl+S")}); + + EXPECT_EQ(2u, provider()->matches().size()); + + EXPECT_EQ(u":> First", provider()->matches()[0].description); + EXPECT_EQ(u"Ctrl+F", provider()->matches()[0].contents); + + EXPECT_EQ(u":> Second", provider()->matches()[1].description); + EXPECT_EQ(u"Ctrl+S", provider()->matches()[1].contents); + + for (const auto& match : provider()->matches()) { + // As we haven't specified a prompt, none of the matches should have + // additional_text. + EXPECT_EQ(u"", match.additional_text); + // All matches swap the contents & description, so that the command displays + // on the left and the shortcut (if any) on the right. + EXPECT_TRUE(match.swap_contents_and_description); + // All matches should be allowed to be the default, so when the user presses + // enter, the top command is executed. + EXPECT_TRUE(match.allowed_to_be_default_match); + // fill_into_edit should be the same as whatever the last input was, so + // scrolling through the commands doesn't affect what the user typed. + EXPECT_EQ(u":> Hello World", match.fill_into_edit); + // Check the matches have actions. + EXPECT_NE(0u, match.actions.size()); + } +} + +TEST_F(CommanderProviderTest, RemovingPrefixClearsMatches) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u" Hello World"})), + false); + + delegate()->Notify({commander::CommandItemModel(u"First", {}, u"Ctrl+F"), + commander::CommandItemModel(u"Second", {}, u"Ctrl+S")}); + EXPECT_EQ(2u, provider()->matches().size()); + + provider()->Start(CreateInput(u"no prefix!"), false); + EXPECT_EQ(0u, provider()->matches().size()); +} + +TEST_F(CommanderProviderTest, PromptingForMoreInputSetsAnnotation) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u" Hello World"})), + false); + + delegate()->Notify({commander::CommandItemModel(u"Foo", {}, u"")}, + u"What thing?"); + + EXPECT_EQ(1u, provider()->matches().size()); + EXPECT_EQ(u"What thing?", provider()->matches()[0].additional_text); + EXPECT_EQ(u":> Foo", provider()->matches()[0].description); +} + +TEST_F(CommanderProviderTest, NoMatchRangeAllDimStyle) { + provider()->Start(CreateInput(u":> Hello World"), false); + + delegate()->Notify({commander::CommandItemModel(u"Foo", {}, u"")}, + u"What thing?"); + + EXPECT_EQ(1u, provider()->matches().size()); + const auto& c = provider()->matches()[0].description_class; + ASSERT_EQ(1u, c.size()); + EXPECT_EQ(0u, c[0].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[0].style); +} + +TEST_F(CommanderProviderTest, ZeroCharMatchIsIgnored) { + provider()->Start(CreateInput(u":> Hello World"), false); + + delegate()->Notify({commander::CommandItemModel(u"Foo", {gfx::Range()}, u"")}, + u"What thing?"); + + EXPECT_EQ(1u, provider()->matches().size()); + const auto& c = provider()->matches()[0].description_class; + ASSERT_EQ(1u, c.size()); + EXPECT_EQ(0u, c[0].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[0].style); +} + +TEST_F(CommanderProviderTest, OneCharMatchIsHighlighted) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u" Hello World"})), + false); + + delegate()->Notify( + {commander::CommandItemModel(u"Foo", {gfx::Range(0, 1)}, u"")}, + u"What thing?"); + + EXPECT_EQ(1u, provider()->matches().size()); + const auto& c = provider()->matches()[0].description_class; + ASSERT_EQ(3u, c.size()); + + // :> is DIM + EXPECT_EQ(0u, c[0].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[0].style); + + // F is MATCH + EXPECT_EQ(kClassificationOffset, c[1].offset); + EXPECT_EQ(ACMatchClassification::MATCH, c[1].style); + + // oo should be DIM, as it didn't match + EXPECT_EQ(1u + kClassificationOffset, c[2].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[2].style); +} + +// Note: The AutocompleteClassifier gets unhappy if the style switches back and +// forth on the same character (i.e one match finishes where another one +// starts). +TEST_F(CommanderProviderTest, AdjacentMatchesDontSwitchBackAndForth) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u" Hello World"})), + false); + + delegate()->Notify({commander::CommandItemModel( + u"Foo", {gfx::Range(0, 1), gfx::Range(1, 2)}, u"")}, + u"What thing?"); + + const auto& c = provider()->matches()[0].description_class; + EXPECT_EQ(1u, provider()->matches().size()); + ASSERT_EQ(4u, c.size()); + + // :> Foo + // :> is DIM + EXPECT_EQ(0u, c[0].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[0].style); + + // F is MATCH, but shouldn't add a closing DIM + EXPECT_EQ(kClassificationOffset, c[1].offset); + EXPECT_EQ(ACMatchClassification::MATCH, c[1].style); + + // first o is also MATCH + EXPECT_EQ(1u + kClassificationOffset, c[2].offset); + EXPECT_EQ(ACMatchClassification::MATCH, c[2].style); + + // second o should be DIM, as it didn't match + EXPECT_EQ(2u + kClassificationOffset, c[3].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[3].style); +} + +TEST_F(CommanderProviderTest, FullLengthMatchIsApplied) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u" Hello World"})), + false); + + delegate()->Notify( + {commander::CommandItemModel(u"Foo", {gfx::Range(0, 3)}, u"")}, + u"What thing?"); + + const auto& c = provider()->matches()[0].description_class; + EXPECT_EQ(1u, provider()->matches().size()); + ASSERT_EQ(2u, c.size()); + + // :> Foo + // :> is DIM + EXPECT_EQ(0u, c[0].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[0].style); + + // Foo is MATCH + EXPECT_EQ(kClassificationOffset, c[1].offset); + EXPECT_EQ(ACMatchClassification::MATCH, c[1].style); +} + +TEST_F(CommanderProviderTest, MatchesCanHaveGaps) { + provider()->Start( + CreateInput(base::StrCat({commander::kCommandPrefix, u"FoBa"})), false); + + delegate()->Notify( + {commander::CommandItemModel(u"Foo Bar", + {gfx::Range(0, 2), gfx::Range(4, 6)}, u"")}, + u"What thing?"); + + const auto& c = provider()->matches()[0].description_class; + EXPECT_EQ(1u, provider()->matches().size()); + ASSERT_EQ(5u, c.size()); + + // |:> |Foo Bar is DIM + EXPECT_EQ(0u, c[0].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[0].style); + + // :> |Fo|o Bar is MATCH + EXPECT_EQ(kClassificationOffset, c[1].offset); + EXPECT_EQ(ACMatchClassification::MATCH, c[1].style); + + EXPECT_EQ(2u + kClassificationOffset, c[2].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[2].style); + + // :> Foo |Ba|r is MATCH + EXPECT_EQ(4u + kClassificationOffset, c[3].offset); + EXPECT_EQ(ACMatchClassification::MATCH, c[3].style); + + EXPECT_EQ(6u + kClassificationOffset, c[4].offset); + EXPECT_EQ(ACMatchClassification::DIM, c[4].style); +} diff --git a/components/omnibox/browser/sources.gni b/components/omnibox/browser/sources.gni index 6f3dad5648d..f1ef2e060f2 100644 --- a/components/omnibox/browser/sources.gni +++ b/components/omnibox/browser/sources.gni @@ -14,6 +14,8 @@ brave_components_omnibox_browser_sources = [ "//brave/components/omnibox/browser/brave_local_history_zero_suggest_provider.h", "//brave/components/omnibox/browser/brave_omnibox_client.cc", "//brave/components/omnibox/browser/brave_omnibox_client.h", + "//brave/components/omnibox/browser/brave_omnibox_edit_model.cc", + "//brave/components/omnibox/browser/brave_omnibox_edit_model.h", "//brave/components/omnibox/browser/brave_omnibox_prefs.cc", "//brave/components/omnibox/browser/brave_omnibox_prefs.h", "//brave/components/omnibox/browser/brave_search_provider.cc", @@ -36,3 +38,15 @@ brave_components_omnibox_browser_deps = [ "//components/prefs", "//url", ] + +if (!is_android && !is_ios) { + brave_components_omnibox_browser_sources += [ + "//brave/components/omnibox/browser/commander_action.cc", + "//brave/components/omnibox/browser/commander_action.h", + "//brave/components/omnibox/browser/commander_provider.cc", + "//brave/components/omnibox/browser/commander_provider.h", + ] + + brave_components_omnibox_browser_deps += + [ "//brave/components/commander/browser" ] +} diff --git a/patches/chrome-browser-ui-commander-fuzzy_finder.cc.patch b/patches/chrome-browser-ui-commander-fuzzy_finder.cc.patch new file mode 100644 index 00000000000..8ba6e8a3c30 --- /dev/null +++ b/patches/chrome-browser-ui-commander-fuzzy_finder.cc.patch @@ -0,0 +1,13 @@ +diff --git a/chrome/browser/ui/commander/fuzzy_finder.cc b/chrome/browser/ui/commander/fuzzy_finder.cc +index 5166bb5d04388..0149fbaea6b93 100644 +--- a/chrome/browser/ui/commander/fuzzy_finder.cc ++++ b/chrome/browser/ui/commander/fuzzy_finder.cc +@@ -236,7 +236,7 @@ FuzzyFinder::~FuzzyFinder() = default; + double FuzzyFinder::Find(const std::u16string& haystack, + std::vector* matched_ranges) { + matched_ranges->clear(); +- if (needle_.size() == 0) ++ if (needle_.size() == 0 BRAVE_FUZZY_FINDER_NEEDLE_SIZE_CHECK) + return 0; + const std::u16string& folded = base::i18n::FoldCase(haystack); + size_t m = needle_.size(); diff --git a/test/BUILD.gn b/test/BUILD.gn index ac1dee8a40e..458b54de1fe 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -1085,6 +1085,7 @@ test("brave_browser_tests") { sources += [ "//brave/browser/ui/views/toolbar/wallet_button_notification_source_browsertest.cc" ] deps += [ "//brave/browser/sharing_hub:browser_tests", + "//brave/browser/ui/commander:browser_tests", "//brave/browser/ui/whats_new:browser_test", "//chrome/browser/apps/app_service:app_service", "//chrome/browser/apps/app_service:constants", diff --git a/test/filters/unit_tests.filter b/test/filters/unit_tests.filter index 7c76210c053..2445ed0194f 100644 --- a/test/filters/unit_tests.filter +++ b/test/filters/unit_tests.filter @@ -634,3 +634,7 @@ # This test crashes because AutocompleteClassifier called when TabStripModel # was not initialized in test. -RenderViewContextMenuPrefsTest.ShowAllPasswordsIncognito + +# This test fails because we changed the behavior of the fuzzy finder to match +# the empty string. +-CommanderFuzzyFinder.EmptyStringDoesNotMatch