diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index 0186110f6cf..eef5fac17cb 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -8,6 +8,14 @@ using namespace Microsoft::Console::Types; +bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData) +{ + return ResetIfStale(renderData, + _needle, + _step == -1, // this is the opposite of the initializer below + _caseInsensitive); +} + bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive) { const auto& textBuffer = renderData.GetTextBuffer(); @@ -71,8 +79,10 @@ void Search::MovePastPoint(const til::point anchor) noexcept void Search::FindNext() noexcept { - const auto count = gsl::narrow_cast(_results.size()); - _index = (_index + _step + count) % count; + if (const auto count{ gsl::narrow_cast(_results.size()) }) + { + _index = (_index + _step + count) % count; + } } const til::point_span* Search::GetCurrent() const noexcept @@ -87,6 +97,7 @@ const til::point_span* Search::GetCurrent() const noexcept // Routine Description: // - Takes the found word and selects it in the screen buffer + bool Search::SelectCurrent() const { if (const auto s = GetCurrent()) @@ -102,3 +113,13 @@ bool Search::SelectCurrent() const return false; } + +const std::vector& Search::Results() const noexcept +{ + return _results; +} + +size_t Search::CurrentMatch() const noexcept +{ + return _index; +} diff --git a/src/buffer/out/search.h b/src/buffer/out/search.h index a337552d59a..2c6bc80a724 100644 --- a/src/buffer/out/search.h +++ b/src/buffer/out/search.h @@ -25,6 +25,7 @@ class Search final public: Search() = default; + bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData); bool ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, const std::wstring_view& needle, bool reverse, bool caseInsensitive); void MovePastCurrentSelection(); @@ -34,10 +35,14 @@ class Search final const til::point_span* GetCurrent() const noexcept; bool SelectCurrent() const; + const std::vector& Results() const noexcept; + size_t CurrentMatch() const noexcept; + bool CurrentDirection() const noexcept; + private: // _renderData is a pointer so that Search() is constexpr default constructable. Microsoft::Console::Render::IRenderData* _renderData = nullptr; - std::wstring_view _needle; + std::wstring _needle; bool _reverse = false; bool _caseInsensitive = false; uint64_t _lastMutationId = 0; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index c487a81960f..7530909aaab 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -39,6 +39,9 @@ constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); // The minimum delay between updating the locations of regex patterns constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); +// The delay before performing the search after change of search criteria +constexpr const auto SearchAfterChangeDelay = std::chrono::milliseconds(200); + namespace winrt::Microsoft::Terminal::Control::implementation { static winrt::Microsoft::Terminal::Core::OptionalColor OptionalFromColor(const til::color& c) @@ -1346,6 +1349,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation nullptr; } + til::color ControlCore::ForegroundColor() const + { + return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultForeground); + } + til::color ControlCore::BackgroundColor() const { return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultBackground); @@ -1552,6 +1560,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } const auto foundMatch = _searcher.SelectCurrent(); + auto foundResults = winrt::make_self(foundMatch); if (foundMatch) { // this is used for search, @@ -1560,15 +1569,44 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetBlockSelection(false); _renderer->TriggerSelection(); _UpdateSelectionMarkersHandlers(*this, winrt::make(true)); + + foundResults->TotalMatches(gsl::narrow(_searcher.Results().size())); + foundResults->CurrentMatch(gsl::narrow(_searcher.CurrentMatch())); + + _terminal->AlwaysNotifyOnBufferRotation(true); } // Raise a FoundMatch event, which the control will use to notify // narrator if there was any results in the buffer - _FoundMatchHandlers(*this, winrt::make(foundMatch)); + _FoundMatchHandlers(*this, *foundResults); + } + + Windows::Foundation::Collections::IVector ControlCore::SearchResultRows() + { + auto lock = _terminal->LockForWriting(); + if (_searcher.ResetIfStale(*GetRenderData())) + { + auto results = std::vector(); + + auto lastRow = til::CoordTypeMin; + for (const auto& match : _searcher.Results()) + { + const auto row{ match.start.y }; + if (row != lastRow) + { + results.push_back(row); + lastRow = row; + } + } + _cachedSearchResultRows = winrt::single_threaded_vector(std::move(results)); + } + + return _cachedSearchResultRows; } void ControlCore::ClearSearch() { + _terminal->AlwaysNotifyOnBufferRotation(false); _searcher = {}; } diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 37058ebfbd3..c21730f4789 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -106,6 +106,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::hstring FontFaceName() const noexcept; uint16_t FontWeight() const noexcept; + til::color ForegroundColor() const; til::color BackgroundColor() const; void SendInput(const winrt::hstring& wstr); @@ -208,6 +209,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); void ClearSearch(); + Windows::Foundation::Collections::IVector SearchResultRows(); + void LeftClickOnTerminal(const til::point terminalPosition, const int numberOfClicks, const bool altEnabled, @@ -338,6 +341,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation til::point _contextMenuBufferPosition{ 0, 0 }; + Windows::Foundation::Collections::IVector _cachedSearchResultRows{ nullptr }; + void _setupDispatcherAndCallbacks(); bool _setFontSizeUnderLock(float fontSize); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 1b9f67d8d9c..01223fb1a14 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -127,8 +127,12 @@ namespace Microsoft.Terminal.Control Microsoft.Terminal.Core.Point CursorPosition { get; }; void ResumeRendering(); void BlinkAttributeTick(); + void Search(String text, Boolean goForward, Boolean caseSensitive); void ClearSearch(); + IVector SearchResultRows { get; }; + + Microsoft.Terminal.Core.Color ForegroundColor { get; }; Microsoft.Terminal.Core.Color BackgroundColor { get; }; SelectionData SelectionInfo { get; }; diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 9d3f3e2a3f6..12632871fdb 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -176,6 +176,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation } WINRT_PROPERTY(bool, FoundMatch); + WINRT_PROPERTY(int32_t, TotalMatches); + WINRT_PROPERTY(int32_t, CurrentMatch); }; struct ShowWindowArgs : public ShowWindowArgsT diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 75e1a20211c..3caea5a0f38 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -82,6 +82,8 @@ namespace Microsoft.Terminal.Control runtimeclass FoundResultsArgs { Boolean FoundMatch { get; }; + Int32 TotalMatches { get; }; + Int32 CurrentMatch { get; }; } runtimeclass ShowWindowArgs diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index c94ab15d368..a604fc912e4 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -178,6 +178,18 @@ Ctrl+Click to follow link + + No results + Will be presented near the search box when Find operation returned no results. + + + Searching... + Will be presented near the search box when Find operation is running. + + + {0}/{1} + Will be displayed to indicate what result the user has selected, of how many total results. {0} will be replaced with the index of the current result. {1} will be replaced with the total number of results. + Invalid URI Whenever we encounter an invalid URI or URL we show this string as a warning. @@ -276,4 +288,4 @@ Please either install the missing font or choose another one. Select output The tooltip for a button for selecting all of a command's output - + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index a841f11a7d8..bd5aeae3433 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "SearchBoxControl.h" #include "SearchBoxControl.g.cpp" +#include using namespace winrt; using namespace winrt::Windows::UI::Xaml; @@ -18,12 +19,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation this->CharacterReceived({ this, &SearchBoxControl::_CharacterHandler }); this->KeyDown({ this, &SearchBoxControl::_KeyDownHandler }); + this->RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + // Once the control is visible again we trigger SearchChanged event. + // We do this since we probably have a value from the previous search, + // and in such case logically the search changes from "nothing" to this value. + // A good example for SearchChanged event consumer is Terminal Control. + // Once the Search Box is open we want the Terminal Control + // to immediately perform the search with the value appearing in the box. + if (Visibility() == Visibility::Visible) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + }); _focusableElements.insert(TextBox()); _focusableElements.insert(CloseButton()); _focusableElements.insert(CaseSensitivityButton()); _focusableElements.insert(GoForwardButton()); _focusableElements.insert(GoBackwardButton()); + + StatusBox().Width(_GetStatusMaxWidth()); } // Method Description: @@ -62,6 +77,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (e.OriginalKey() == winrt::Windows::System::VirtualKey::Enter) { + // If the buttons are disabled, then don't allow enter to search either. + if (!GoForwardButton().IsEnabled() || !GoBackwardButton().IsEnabled()) + { + return; + } + const auto state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift); if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down)) { @@ -209,4 +230,141 @@ namespace winrt::Microsoft::Terminal::Control::implementation { e.Handled(true); } + + // Method Description: + // - Handler for changing the text. Triggers SearchChanged event + // Arguments: + // - sender: not used + // - e: event data + // Return Value: + // - + void SearchBoxControl::TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + + // Method Description: + // - Handler for clicking the case sensitivity toggle. Triggers SearchChanged event + // Arguments: + // - sender: not used + // - e: not used + // Return Value: + // - + void SearchBoxControl::CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) + { + _SearchChangedHandlers(TextBox().Text(), _GoForward(), _CaseSensitive()); + } + + // Method Description: + // - Formats a status message representing the search state: + // * "Searching" - if totalMatches is negative + // * "No results" - if totalMatches is 0 + // * "?/n" - if totalMatches=n matches and we didn't start the iteration over matches + // (usually we will get this after buffer update) + // * "m/n" - if we are currently at match m out of n. + // * "m/max+" - if n > max results to show + // * "?/max+" - if m > max results to show + // Arguments: + // - totalMatches - total number of matches (search results) + // - currentMatch - the index of the current match (0-based) + // Return Value: + // - status message + winrt::hstring SearchBoxControl::_FormatStatus(int32_t totalMatches, int32_t currentMatch) + { + if (totalMatches < 0) + { + return RS_(L"TermControl_Searching"); + } + + if (totalMatches == 0) + { + return RS_(L"TermControl_NoMatch"); + } + + std::wstring currentString; + std::wstring totalString; + + if (currentMatch < 0 || currentMatch > (MaximumTotalResultsToShowInStatus - 1)) + { + currentString = CurrentIndexTooHighStatus; + } + else + { + currentString = fmt::format(L"{}", currentMatch + 1); + } + + if (totalMatches > MaximumTotalResultsToShowInStatus) + { + totalString = TotalResultsTooHighStatus; + } + else + { + totalString = fmt::format(L"{}", totalMatches); + } + + return winrt::hstring{ fmt::format(RS_(L"TermControl_NumResults").c_str(), currentString, totalString) }; + } + + // Method Description: + // - Helper method to measure the width of the text block given the text and the font size + // Arguments: + // - text: the text to measure + // - fontSize: the size of the font to measure + // Return Value: + // - the size in pixels + double SearchBoxControl::_TextWidth(winrt::hstring text, double fontSize) + { + winrt::Windows::UI::Xaml::Controls::TextBlock t; + t.FontSize(fontSize); + t.Text(text); + t.Measure({ FLT_MAX, FLT_MAX }); + return t.ActualWidth(); + } + + // Method Description: + // - This method tries to predict the maximal size of the status box + // by measuring different possible statuses + // Return Value: + // - the size in pixels + double SearchBoxControl::_GetStatusMaxWidth() + { + const auto fontSize = StatusBox().FontSize(); + const auto maxLength = std::max({ _TextWidth(_FormatStatus(-1, -1), fontSize), + _TextWidth(_FormatStatus(0, -1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus, MaximumTotalResultsToShowInStatus - 1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus + 1, MaximumTotalResultsToShowInStatus - 1), fontSize), + _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus + 1, MaximumTotalResultsToShowInStatus), fontSize) }); + + return maxLength; + } + + // Method Description: + // - Formats and sets the status message in the status box. + // Increases the size of the box if required. + // Arguments: + // - totalMatches - total number of matches (search results) + // - currentMatch - the index of the current match (0-based) + // Return Value: + // - + void SearchBoxControl::SetStatus(int32_t totalMatches, int32_t currentMatch) + { + const auto status = _FormatStatus(totalMatches, currentMatch); + StatusBox().Text(status); + } + + // Method Description: + // - Enables / disables results navigation buttons + // Arguments: + // - enable: if true, the buttons should be enabled + // Return Value: + // - + void SearchBoxControl::NavigationEnabled(bool enabled) + { + GoBackwardButton().IsEnabled(enabled); + GoForwardButton().IsEnabled(enabled); + } + bool SearchBoxControl::NavigationEnabled() + { + return GoBackwardButton().IsEnabled() || GoForwardButton().IsEnabled(); + } } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 4a680edcb68..5ab9ee291bc 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -21,6 +21,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation { struct SearchBoxControl : SearchBoxControlT { + static constexpr int32_t MaximumTotalResultsToShowInStatus = 999; + static constexpr std::wstring_view TotalResultsTooHighStatus = L"999+"; + static constexpr std::wstring_view CurrentIndexTooHighStatus = L"?"; + static constexpr std::wstring_view StatusDelimiter = L"/"; + SearchBoxControl(); void TextBoxKeyDown(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); @@ -28,17 +33,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation void SetFocusOnTextbox(); void PopulateTextbox(const winrt::hstring& text); bool ContainsFocus(); + void SetStatus(int32_t totalMatches, int32_t currentMatch); + bool NavigationEnabled(); + void NavigationEnabled(bool enabled); void GoBackwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); void GoForwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); void CloseClick(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& e); + void TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + WINRT_CALLBACK(Search, SearchHandler); + WINRT_CALLBACK(SearchChanged, SearchHandler); TYPED_EVENT(Closed, Control::SearchBoxControl, Windows::UI::Xaml::RoutedEventArgs); private: std::unordered_set _focusableElements; + static winrt::hstring _FormatStatus(int32_t totalMatches, int32_t currentMatch); + static double _TextWidth(winrt::hstring text, double fontSize); + double _GetStatusMaxWidth(); + bool _GoForward(); bool _CaseSensitive(); void _KeyDownHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); diff --git a/src/cascadia/TerminalControl/SearchBoxControl.idl b/src/cascadia/TerminalControl/SearchBoxControl.idl index a6bd0a486d7..de909157376 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.idl +++ b/src/cascadia/TerminalControl/SearchBoxControl.idl @@ -11,8 +11,12 @@ namespace Microsoft.Terminal.Control void SetFocusOnTextbox(); void PopulateTextbox(String text); Boolean ContainsFocus(); + void SetStatus(Int32 totalMatches, Int32 currentMatch); + + Boolean NavigationEnabled; event SearchHandler Search; + event SearchHandler SearchChanged; event Windows.Foundation.TypedEventHandler Closed; } } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.xaml b/src/cascadia/TerminalControl/SearchBoxControl.xaml index c0716e39d44..4956d255662 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.xaml +++ b/src/cascadia/TerminalControl/SearchBoxControl.xaml @@ -161,7 +161,15 @@ HorizontalAlignment="Left" VerticalAlignment="Center" IsSpellCheckEnabled="False" - KeyDown="TextBoxKeyDown" /> + KeyDown="TextBoxKeyDown" + TextChanged="TextBoxTextChanged" /> + + + BackgroundSizing="OuterBorderEdge" + Click="CaseSensitivityButtonClicked"> diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 88a8db66b00..fedd24f8e14 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -302,6 +302,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto fullHeight{ ScrollBarCanvas().ActualHeight() }; const auto totalBufferRows{ update.newMaximum + update.newViewportSize }; + auto drawPip = [&](const auto row, const auto rightAlign, const auto& brush) { + Windows::UI::Xaml::Shapes::Rectangle r; + r.Fill(brush); + r.Width(16.0f / 3.0f); // pip width - 1/3rd of the scrollbar width. + r.Height(2); + const auto fractionalHeight = row / totalBufferRows; + const auto relativePos = fractionalHeight * fullHeight; + ScrollBarCanvas().Children().Append(r); + Windows::UI::Xaml::Controls::Canvas::SetTop(r, relativePos); + if (rightAlign) + { + Windows::UI::Xaml::Controls::Canvas::SetLeft(r, 16.0f * .66f); + } + }; + for (const auto m : marks) { Windows::UI::Xaml::Shapes::Rectangle r; @@ -312,14 +327,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation // pre-evaluate that for us, and shove the real value into the // Color member, regardless if the mark has a literal value set. brush.Color(static_cast(m.Color.Color)); - r.Fill(brush); - r.Width(16.0f / 3.0f); // pip width - 1/3rd of the scrollbar width. - r.Height(2); - const auto markRow = m.Start.Y; - const auto fractionalHeight = markRow / totalBufferRows; - const auto relativePos = fractionalHeight * fullHeight; - ScrollBarCanvas().Children().Append(r); - Windows::UI::Xaml::Controls::Canvas::SetTop(r, relativePos); + drawPip(m.Start.Y, false, brush); + } + + if (_searchBox) + { + const auto searchMatches{ _core.SearchResultRows() }; + if (searchMatches && + searchMatches.Size() > 0 && + _searchBox->Visibility() == Visibility::Visible) + { + const til::color fgColor{ _core.ForegroundColor() }; + Media::SolidColorBrush searchMarkBrush{}; + searchMarkBrush.Color(fgColor); + for (const auto m : searchMatches) + { + drawPip(m, true, searchMarkBrush); + } + } } } } @@ -388,8 +413,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // Method Description: - // - Search text in text buffer. This is triggered if the user click - // search button or press enter. + // - Search text in text buffer. This is triggered if the user clicks the + // search button, presses enter, or changes the search criteria. // Arguments: // - text: the text to search // - goForward: boolean that represents if the current search direction is forward @@ -403,6 +428,24 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.Search(text, goForward, caseSensitive); } + // Method Description: + // - The handler for the "search criteria changed" event. Clears selection and initiates a new search. + // Arguments: + // - text: the text to search + // - goForward: indicates whether the search should be performed forward (if set to true) or backward + // - caseSensitive: boolean that represents if the current search is case sensitive + // Return Value: + // - + void TermControl::_SearchChanged(const winrt::hstring& text, + const bool goForward, + const bool caseSensitive) + { + if (_searchBox && _searchBox->Visibility() == Visibility::Visible) + { + _core.Search(text, goForward, caseSensitive); + } + } + // Method Description: // - The handler for the close button or pressing "Esc" when focusing on the // search dialog. @@ -3420,8 +3463,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - args: contains information about the results that were or were not found. // Return Value: // - - void TermControl::_coreFoundMatch(const IInspectable& /*sender*/, const Control::FoundResultsArgs& args) + winrt::fire_and_forget TermControl::_coreFoundMatch(const IInspectable& /*sender*/, Control::FoundResultsArgs args) { + co_await wil::resume_foreground(Dispatcher()); if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this) }) { automationPeer.RaiseNotificationEvent( @@ -3430,6 +3474,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation args.FoundMatch() ? RS_(L"SearchBox_MatchesAvailable") : RS_(L"SearchBox_NoMatches"), // what to announce if results were found L"SearchBoxResultAnnouncement" /* unique name for this group of notifications */); } + + // Manually send a scrollbar update, now, on the UI thread. We're + // already UI-driven, so that's okay. We're not really changing the + // scrollbar, but we do want to update the position of any marks. The + // Core might send a scrollbar updated event too, but if the first + // search hit is in the visible viewport, then the pips won't display + // until the user first scrolls. + auto scrollBar = ScrollBar(); + ScrollBarUpdate update{ + .newValue = scrollBar.Value(), + .newMaximum = scrollBar.Maximum(), + .newMinimum = scrollBar.Minimum(), + .newViewportSize = scrollBar.ViewportSize(), + }; + _throttledUpdateScrollbar(update); + + if (_searchBox) + { + _searchBox->SetStatus(args.TotalMatches(), args.CurrentMatch()); + _searchBox->NavigationEnabled(true); + } } void TermControl::OwningHwnd(uint64_t owner) diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 895f46128e4..f2f76971ade 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -274,6 +274,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _UpdateSettingsFromUIThread(); void _UpdateAppearanceFromUIThread(Control::IControlAppearance newAppearance); + void _ApplyUISettings(); winrt::fire_and_forget UpdateAppearance(Control::IControlAppearance newAppearance); void _SetBackgroundImage(const IControlAppearance& newAppearance); @@ -343,6 +344,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation double _GetAutoScrollSpeed(double cursorDistanceFromBorder) const; void _Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); + + void _SearchChanged(const winrt::hstring& text, const bool goForward, const bool caseSensitive); void _CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& args); // TSFInputControl Handlers @@ -357,7 +360,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::fire_and_forget _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); - void _coreFoundMatch(const IInspectable& sender, const Control::FoundResultsArgs& args); + winrt::fire_and_forget _coreFoundMatch(const IInspectable& sender, Control::FoundResultsArgs args); til::point _toPosInDips(const Core::Point terminalCellPos); void _throttledUpdateScrollbar(const ScrollBarUpdate& update); diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index eff29b6b694..9f1c9dc83a5 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -1302,6 +1302,7 @@ x:Load="False" Closed="_CloseSearchBoxControl" Search="_Search" + SearchChanged="_SearchChanged" Visibility="Collapsed" /> diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index ac5a18ec91f..9c83710c3b5 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -16,6 +16,7 @@ #include "../../cascadia/terminalcore/ITerminalInput.hpp" #include +#include inline constexpr size_t TaskbarMinProgress{ 10 }; @@ -118,6 +119,8 @@ class Microsoft::Terminal::Core::Terminal final : const til::point& end, const bool fromUi); + til::property AlwaysNotifyOnBufferRotation; + std::wstring_view CurrentCommand() const; #pragma region ITerminalApi diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 4c31cbc4bbc..6815876b270 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -472,7 +472,7 @@ void Terminal::NotifyBufferRotation(const int delta) const auto oldScrollOffset = _scrollOffset; _PreserveUserScrollOffset(delta); - if (_scrollOffset != oldScrollOffset || hasScrollMarks) + if (_scrollOffset != oldScrollOffset || hasScrollMarks || AlwaysNotifyOnBufferRotation()) { _NotifyScrollEvent(); } diff --git a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp index 7a30714f42e..12223e94a63 100644 --- a/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ScrollTest.cpp @@ -154,6 +154,13 @@ void ScrollTest::TestNotifyScrolling() // SHRT_MAX // - Have a selection + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:notifyOnCircling", L"{false, true}") + END_TEST_METHOD_PROPERTIES(); + INIT_TEST_PROPERTY(bool, notifyOnCircling, L"Controls whether we should always request scroll notifications"); + + _term->AlwaysNotifyOnBufferRotation(notifyOnCircling); + Log::Comment(L"Watch out - this test takes a while to run, and won't " L"output anything unless in encounters an error. This is expected."); @@ -180,10 +187,12 @@ void ScrollTest::TestNotifyScrolling() // causes the first scroll event auto scrolled = currentRow >= TerminalViewHeight - 1; - // When we circle the buffer, the scroll bar's position does not - // change. + // When we circle the buffer, the scroll bar's position does not change. + // However, as of GH#14045, we will send a notification IF the control + // requested on (by setting AlwaysNotifyOnBufferRotation) auto circledBuffer = currentRow >= totalBufferSize - 1; - auto expectScrollBarNotification = scrolled && !circledBuffer; + auto expectScrollBarNotification = (scrolled && !circledBuffer) || // If we scrolled, but didn't circle the buffer OR + (circledBuffer && notifyOnCircling); // we circled AND we asked for notifications. if (expectScrollBarNotification) { diff --git a/src/inc/til/winrt.h b/src/inc/til/winrt.h index ed815d17671..e5faece3185 100644 --- a/src/inc/til/winrt.h +++ b/src/inc/til/winrt.h @@ -13,7 +13,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" property& operator=(const property& other) = default; - T operator()() const + T operator()() const noexcept { return _value; } @@ -23,11 +23,13 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } explicit operator bool() const noexcept { +#ifdef WINRT_Windows_Foundation_H if constexpr (std::is_same_v) { return !_value.empty(); } else +#endif { return _value; }