Repository: microsoft/terminal (language: cpp)

Issue:
Search should display the number of results it finds
when the search term is not found, pressing enter on the find dialog announces nothing. It would be ideal if some message was announced to the effect that it was not found.

When it is found, NVDA reads the search term. I find that this behaviour is not that intuitive. 

What does work well is that when escaping out of the find dialog, NVDA's review cursor is positioned at the starting position of the match for you to review around it.

_Originally posted by @Neurrone in https://github.com/microsoft/terminal/issues/5421#issuecomment-632829133_

## Additional Details
With regards to accessibility, UIA should at least somehow notify the user that no results were found.

With regards to general UX, this kind of notification should be revealed in/near the search box. It would be cool if we could say something like "4 of 42 Results" and change it to "No Results" if none are found. But at the very least, somehow displaying "No Results" would be very useful.

Though we _could_ just implement the accessibility approach, if we managed to implement the second approach instead, we would basically get accessibility for free.

(another approach) I think some apps would do some kind of noise when no result was found (though I think that's a bit of an older approach, correct me if I'm wrong). That would be another approach that would "bridge the gap" and solve this problem too.


Discussion / hints:
> (another approach) I think some apps would do some kind of noise when no result was found (though I think that's a bit of an older approach, correct me if I'm wrong).

Lets avoid that if possible. Some apps like notepad do that for example, but also pop up a dialog saying that nothing is found.
@DHowett, @carlos-zamora - I have a PoC for the number of results + interactive behavior (search upon every box modification).

I am not sure how severe are the performance implications on a very large buffer - to count the number of results I need to iterate all over all matches to count them (actually I also store them to prevent additional searches). If we are going to productize this, probably we can introduce some threshold on the size of buffer / number of matches beyond which we will show question marks, something like "2000 of ?".

![SearchStatus](https://user-images.githubusercontent.com/4639110/102137569-b4be8000-3e63-11eb-91cc-c92715324967.gif)

I lost several hours trying to implement live highlighting of all matches (with another color).. eventually I dieded.. (I guess changing renderer interface is still beyond my level).

1. Please let me know - if you want this productized
2. What do you believe is the correct approach to implement highlighting?

Fix patch:
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<ptrdiff_t>(_results.size());
-    _index = (_index + _step + count) % count;
+    if (const auto count{ gsl::narrow_cast<ptrdiff_t>(_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<til::point_span>& 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<til::point_span>& 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<implementation::FoundResultsArgs>(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<implementation::UpdateSelectionMarkersEventArgs>(true));
+
+            foundResults->TotalMatches(gsl::narrow<int32_t>(_searcher.Results().size()));
+            foundResults->CurrentMatch(gsl::narrow<int32_t>(_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<implementation::FoundResultsArgs>(foundMatch));
+        _FoundMatchHandlers(*this, *foundResults);
+    }
+
+    Windows::Foundation::Collections::IVector<int32_t> ControlCore::SearchResultRows()
+    {
+        auto lock = _terminal->LockForWriting();
+        if (_searcher.ResetIfStale(*GetRenderData()))
+        {
+            auto results = std::vector<int32_t>();
+
+            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<int32_t>(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<int32_t> 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<int32_t> _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<Int32> 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<ShowWindowArgs>
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 @@
   <data name="HowToOpenRun.Text" xml:space="preserve">
     <value>Ctrl+Click to follow link</value>
   </data>
+  <data name="TermControl_NoMatch" xml:space="preserve">
+    <value>No results</value>
+    <comment>Will be presented near the search box when Find operation returned no results.</comment>
+  </data>
+  <data name="TermControl_Searching" xml:space="preserve">
+    <value>Searching...</value>
+    <comment>Will be presented near the search box when Find operation is running.</comment>
+  </data>
+  <data name="TermControl_NumResults" xml:space="preserve">
+    <value>{0}/{1}</value>
+    <comment>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.</comment>
+  </data>
   <data name="InvalidUri" xml:space="preserve">
     <value>Invalid URI</value>
     <comment>Whenever we encounter an invalid URI or URL we show this string as a warning.</comment>
@@ -276,4 +288,4 @@ Please either install the missing font or choose another one.</value>
     <value>Select output</value>
     <comment>The tooltip for a button for selecting all of a command's output</comment>
   </data>
-</root>
+</root>
\ 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 <LibraryResources.h>
 
 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:
+    // - <none>
+    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:
+    // - <none>
+    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:
+    // - <none>
+    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:
+    // - <none>
+    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<SearchBoxControl>
     {
+        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<winrt::Windows::Foundation::IInspectable> _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<SearchBoxControl, Windows.UI.Xaml.RoutedEventArgs> 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" />
+
+        <TextBlock x:Name="StatusBox"
+                   x:Uid="SearchBox_StatusBox"
+                   Margin="5"
+                   HorizontalAlignment="Left"
+                   VerticalAlignment="Center"
+                   FontSize="15" />
 
         <ToggleButton x:Name="GoBackwardButton"
                       x:Uid="SearchBox_SearchBackwards"
@@ -195,7 +203,8 @@
                       Height="32"
                       Margin="4,0"
                       Padding="0"
-                      BackgroundSizing="OuterBorderEdge">
+                      BackgroundSizing="OuterBorderEdge"
+                      Click="CaseSensitivityButtonClicked">
             <PathIcon Data="M8.87305 10H7.60156L6.5625 7.25195H2.40625L1.42871 10H0.150391L3.91016 0.197266H5.09961L8.87305 10ZM6.18652 6.21973L4.64844 2.04297C4.59831 1.90625 4.54818 1.6875 4.49805 1.38672H4.4707C4.42513 1.66471 4.37272 1.88346 4.31348 2.04297L2.78906 6.21973H6.18652ZM15.1826 10H14.0615V8.90625H14.0342C13.5465 9.74479 12.8288 10.1641 11.8809 10.1641C11.1836 10.1641 10.6367 9.97949 10.2402 9.61035C9.84831 9.24121 9.65234 8.7513 9.65234 8.14062C9.65234 6.83268 10.4225 6.07161 11.9629 5.85742L14.0615 5.56348C14.0615 4.37402 13.5807 3.7793 12.6191 3.7793C11.776 3.7793 11.015 4.06641 10.3359 4.64062V3.49219C11.0241 3.05469 11.8171 2.83594 12.7148 2.83594C14.36 2.83594 15.1826 3.70638 15.1826 5.44727V10ZM14.0615 6.45898L12.373 6.69141C11.8535 6.76432 11.4616 6.89421 11.1973 7.08105C10.9329 7.26335 10.8008 7.58919 10.8008 8.05859C10.8008 8.40039 10.9215 8.68066 11.1631 8.89941C11.4092 9.11361 11.735 9.2207 12.1406 9.2207C12.6966 9.2207 13.1546 9.02702 13.5146 8.63965C13.8792 8.24772 14.0615 7.75326 14.0615 7.15625V6.45898Z" />
         </ToggleButton>
 
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<til::color>(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:
+    // - <none>
+    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:
     // - <none>
-    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" />
             </Grid>
 
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 <til/ticket_lock.h>
+#include <til/winrt.h>
 
 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<bool> 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<T, winrt::hstring>)
             {
                 return !_value.empty();
             }
             else
+#endif
             {
                 return _value;
             }


Test patch: