Repository: microsoft/terminal (language: cpp)

Issue:
[1.21.1091 Canary] Terminal crashes on launch
Methinks it's an explosion in the merge between #16611 and #17067 

Useless stack:

```
[0x3]   Microsoft_Terminal_Control!std::coroutine_traits<winrt::fire_and_forget,winrt::Microsoft::Terminal::Control::implementation::ControlCore &,std::basic_string_view<wchar_t,std::char_traits<wchar_t> >,unsigned int>::promise_type::unhandled_exception+0x9   0x61c3efc640   0x7fffe5b3a496   
[0x4]   Microsoft_Terminal_Control!`winrt::Microsoft::Terminal::Control::implementation::TermControl::_coreUpdateSearchResults$_ResumeCoro$1'::`1'::catch$57+0x26   0x61c3efc670   0x7fffe5b2fcd0   
[0x5]   Microsoft_Terminal_Control!_CallSettingFrame_LookupContinuationIndex+0x20   0x61c3efc6b0   0x7fffe5b2edf5   
[0x6]   Microsoft_Terminal_Control!__FrameHandler4::CxxCallCatchBlock+0x115   0x61c3efc6e0   0x7ff812f61d16   
[0x7]   ntdll!RtlCaptureContext2+0x4a6   0x61c3efc7c0   0x7fffe5b39fc8   
[0x8]   Microsoft_Terminal_Control!winrt::Microsoft::Terminal::Control::implementation::TermControl::_coreUpdateSearchResults$_ResumeCoro$1+0x278   0x61c3efee90   0x7fffe5b02170   
[0x9]   Microsoft_Terminal_Control!std::coroutine_handle<void>::resume+0xc   0x61c3efefd0   0x7ffff41d08f3   
[0xa]   Microsoft_Terminal_Control!wil::details::dispatcher_handler::Complete+0x1c   0x61c3efefd0   0x7ffff41d08f3   
[0xb]   Microsoft_Terminal_Control!wil::details::dispatcher_handler::operator()+0x1c   0x61c3efefd0   0x7ffff41d08f3   
[0xc]   Microsoft_Terminal_Control!winrt::impl::delegate<winrt::Windows::UI::Core::DispatchedHandler,wil::details::dispatcher_handler>::Invoke+0x20   0x61c3efefd0   0x7ffff41d08f3  
```

https://github.com/microsoft/terminal/blob/4e7b63c664bc09da3ba2f08287cf03d3729ce3a2/src/cascadia/TerminalControl/TermControl.cpp#L3569-L3595


Discussion / hints:
Okay first off: The `automationPeer.RaiseNotificationEvent` gets called... a lot, on startup. Before I've even opened the window. Shouldn't that get moved into `if (_searchbox)`? 


We've discussed this over here: https://github.com/microsoft/terminal/pull/16611#discussion_r1569158640
I've just started making the changes necessary to fix that.
Mmk, lemme know when you've got a fix ready. 

My half assed fix was:

```diff
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index acab82931..1692a981a 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -3569,19 +3569,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     winrt::fire_and_forget TermControl::_coreUpdateSearchResults(const IInspectable& /*sender*/, Control::UpdateSearchResultsEventArgs args)
     {
         co_await wil::resume_foreground(Dispatcher());
-        if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this) })
-        {
-            automationPeer.RaiseNotificationEvent(
-                Automation::Peers::AutomationNotificationKind::ActionCompleted,
-                Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent,
-                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 */);
-        }

-        _UpdateSearchScrollMarks();

         if (_searchBox)
         {
+            if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this) })
+            {
+                automationPeer.RaiseNotificationEvent(
+                    Automation::Peers::AutomationNotificationKind::ActionCompleted,
+                    Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent,
+                    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 */);
+            }
+
+            _UpdateSearchScrollMarks();
+
             _searchBox->NavigationEnabled(true);
             if (args.State() == Control::SearchState::Inactive)
             {
```

but I've got a consistent repro of the crash, so just lemme know

Fix patch:
diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp
index 336fa2957fb..08ed8ad2ef7 100644
--- a/src/buffer/out/search.cpp
+++ b/src/buffer/out/search.cpp
@@ -13,7 +13,8 @@ bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, c
     const auto& textBuffer = renderData.GetTextBuffer();
     const auto lastMutationId = textBuffer.GetLastMutationId();
 
-    if (_needle == needle &&
+    if (_renderData == &renderData &&
+        _needle == needle &&
         _caseInsensitive == caseInsensitive &&
         _lastMutationId == lastMutationId)
     {
@@ -21,15 +22,16 @@ bool Search::ResetIfStale(Microsoft::Console::Render::IRenderData& renderData, c
         return false;
     }
 
+    if (prevResults)
+    {
+        *prevResults = std::move(_results);
+    }
+
     _renderData = &renderData;
     _needle = needle;
     _caseInsensitive = caseInsensitive;
     _lastMutationId = lastMutationId;
 
-    if (prevResults)
-    {
-        *prevResults = std::move(_results);
-    }
     _results = textBuffer.SearchText(needle, caseInsensitive);
     _index = reverse ? gsl::narrow_cast<ptrdiff_t>(_results.size()) - 1 : 0;
     _step = reverse ? -1 : 1;
@@ -142,4 +144,4 @@ const std::vector<til::point_span>& Search::Results() const noexcept
 ptrdiff_t Search::CurrentMatch() const noexcept
 {
     return _index;
-}
\ No newline at end of file
+}
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index cfba0a1d46a..1e82b825eb0 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -29,19 +29,6 @@ using namespace winrt::Windows::Graphics::Display;
 using namespace winrt::Windows::System;
 using namespace winrt::Windows::ApplicationModel::DataTransfer;
 
-// The minimum delay between updates to the scroll bar's values.
-// The updates are throttled to limit power usage.
-constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8);
-
-// The minimum delay between updating the TSF input control.
-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) noexcept
@@ -117,9 +104,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         auto pfnShowWindowChanged = std::bind(&ControlCore::_terminalShowWindowChanged, this, std::placeholders::_1);
         _terminal->SetShowWindowCallback(pfnShowWindowChanged);
 
-        auto pfnTextLayoutUpdated = std::bind(&ControlCore::_terminalTextLayoutUpdated, this);
-        _terminal->SetTextLayoutUpdatedCallback(pfnTextLayoutUpdated);
-
         auto pfnPlayMidiNote = std::bind(&ControlCore::_terminalPlayMidiNote, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
         _terminal->SetPlayMidiNoteCallback(pfnPlayMidiNote);
 
@@ -167,21 +151,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             _dispatcher = controller.DispatcherQueue();
         }
 
-        // A few different events should be throttled, so they don't fire absolutely all the time:
-        // * _updatePatternLocations: When there's new output, or we scroll the
-        //   viewport, we should re-check if there are any visible hyperlinks.
-        //   But we don't really need to do this every single time text is
-        //   output, we can limit this update to once every 500ms.
-        // * _updateScrollBar: Same idea as the TSF update - we don't _really_
-        //   need to hop across the process boundary every time text is output.
-        //   We can throttle this to once every 8ms, which will get us out of
-        //   the way of the main output & rendering threads.
         const auto shared = _shared.lock();
+        // Raises an OutputIdle event once there hasn't been any output for at least 100ms.
+        // It also updates all regex patterns in the viewport.
+        //
         // NOTE: Calling UpdatePatternLocations from a background
         // thread is a workaround for us to hit GH#12607 less often.
-        shared->updatePatternLocations = std::make_unique<til::throttled_func_trailing<>>(
-            UpdatePatternLocationsInterval,
-            [weakTerminal = std::weak_ptr{ _terminal }]() {
+        shared->outputIdle = std::make_unique<til::debounced_func_trailing<>>(
+            std::chrono::milliseconds{ 100 },
+            [weakTerminal = std::weak_ptr{ _terminal }, weakThis = get_weak(), dispatcher = _dispatcher]() {
+                dispatcher.TryEnqueue(DispatcherQueuePriority::Normal, [weakThis]() {
+                    if (const auto self = weakThis.get(); !self->_IsClosing())
+                    {
+                        self->OutputIdle.raise(*self, nullptr);
+                    }
+                });
+
                 if (const auto t = weakTerminal.lock())
                 {
                     const auto lock = t->LockForWriting();
@@ -189,9 +174,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                 }
             });
 
+        // Scrollbar updates are also expensive (XAML), so we'll throttle them as well.
         shared->updateScrollBar = std::make_shared<ThrottledFuncTrailing<Control::ScrollPositionChangedArgs>>(
             _dispatcher,
-            ScrollBarUpdateInterval,
+            std::chrono::milliseconds{ 8 },
             [weakThis = get_weak()](const auto& update) {
                 if (auto core{ weakThis.get() }; !core->_IsClosing())
                 {
@@ -218,7 +204,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         // thread. These will be recreated in _setupDispatcherAndCallbacks, when
         // we're re-attached to a new control (on a possibly new UI thread).
         const auto shared = _shared.lock();
-        shared->updatePatternLocations.reset();
+        shared->outputIdle.reset();
         shared->updateScrollBar.reset();
     }
 
@@ -671,9 +657,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         }
 
         const auto shared = _shared.lock_shared();
-        if (shared->updatePatternLocations)
+        if (shared->outputIdle)
         {
-            (*shared->updatePatternLocations)();
+            (*shared->outputIdle)();
         }
     }
 
@@ -1100,12 +1086,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         // If this function succeeds with S_FALSE, then the terminal didn't
         // actually change size. No need to notify the connection of this no-op.
         const auto hr = _terminal->UserResize({ vp.Width(), vp.Height() });
-        if (SUCCEEDED(hr) && hr != S_FALSE)
+        if (FAILED(hr) || hr == S_FALSE)
         {
-            _connection.Resize(vp.Height(), vp.Width());
+            return;
+        }
+
+        _connection.Resize(vp.Height(), vp.Width());
 
-            // let the UI know that the text layout has been updated
-            _terminal->NotifyTextLayoutUpdated();
+        // TermControl will call Search() once the OutputIdle even fires after 100ms.
+        // Until then we need to hide the now-stale search results from the renderer.
+        ClearSearch();
+        const auto shared = _shared.lock_shared();
+        if (shared->outputIdle)
+        {
+            (*shared->outputIdle)();
         }
     }
 
@@ -1603,16 +1597,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         ShowWindowChanged.raise(*this, *showWindow);
     }
 
-    void ControlCore::_terminalTextLayoutUpdated()
-    {
-        ClearSearch();
-
-        // send an UpdateSearchResults event to the UI to put the Search UI into inactive state.
-        auto evArgs = winrt::make_self<implementation::UpdateSearchResultsEventArgs>();
-        evArgs->State(SearchState::Inactive);
-        UpdateSearchResults.raise(*this, *evArgs);
-    }
-
     // Method Description:
     // - Plays a single MIDI note, blocking for the duration.
     // Arguments:
@@ -1672,13 +1656,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // - caseSensitive: boolean that represents if the current search is case sensitive
     // Return Value:
     // - <none>
-    void ControlCore::Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive)
+    SearchResults ControlCore::Search(const std::wstring_view& text, const bool goForward, const bool caseSensitive, const bool reset)
     {
         const auto lock = _terminal->LockForWriting();
 
+        bool searchInvalidated = false;
         std::vector<til::point_span> oldResults;
         if (_searcher.ResetIfStale(*GetRenderData(), text, !goForward, !caseSensitive, &oldResults))
         {
+            searchInvalidated = true;
+
             _cachedSearchResultRows = {};
             if (SnapSearchResultToSelection())
             {
@@ -1687,30 +1674,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             }
 
             _terminal->SetSearchHighlights(_searcher.Results());
-            _terminal->SetSearchHighlightFocused(_searcher.CurrentMatch());
         }
-        else
+        else if (!reset)
         {
             _searcher.FindNext();
-            _terminal->SetSearchHighlightFocused(_searcher.CurrentMatch());
         }
-        _renderer->TriggerSearchHighlight(oldResults);
 
-        auto evArgs = winrt::make_self<implementation::UpdateSearchResultsEventArgs>();
-        if (!text.empty())
+        int32_t totalMatches = 0;
+        int32_t currentMatch = 0;
+        if (const auto idx = _searcher.CurrentMatch(); idx >= 0)
         {
-            evArgs->State(SearchState::Active);
-            if (_searcher.GetCurrent())
-            {
-                evArgs->FoundMatch(true);
-                evArgs->TotalMatches(gsl::narrow<int32_t>(_searcher.Results().size()));
-                evArgs->CurrentMatch(gsl::narrow<int32_t>(_searcher.CurrentMatch()));
-            }
+            totalMatches = gsl::narrow<int32_t>(_searcher.Results().size());
+            currentMatch = gsl::narrow<int32_t>(idx);
+            _terminal->SetSearchHighlightFocused(gsl::narrow<size_t>(idx));
         }
 
-        // Raise an UpdateSearchResults event, which the control will use to update the
-        // UI and notify the narrator about the updated search results in the buffer
-        UpdateSearchResults.raise(*this, *evArgs);
+        _renderer->TriggerSearchHighlight(oldResults);
+
+        return {
+            .TotalMatches = totalMatches,
+            .CurrentMatch = currentMatch,
+            .SearchInvalidated = searchInvalidated,
+        };
     }
 
     Windows::Foundation::Collections::IVector<int32_t> ControlCore::SearchResultRows()
@@ -1738,16 +1723,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
     void ControlCore::ClearSearch()
     {
-        // nothing to clear if there's no results
-        if (_searcher.GetCurrent())
-        {
-            const auto lock = _terminal->LockForWriting();
-            _terminal->SetSearchHighlights({});
-            _terminal->SetSearchHighlightFocused({});
-            _renderer->TriggerSearchHighlight(_searcher.Results());
-            _searcher = {};
-            _cachedSearchResultRows = {};
-        }
+        const auto lock = _terminal->LockForWriting();
+        _terminal->SetSearchHighlights({});
+        _terminal->SetSearchHighlightFocused({});
+        _renderer->TriggerSearchHighlight(_searcher.Results());
+        _searcher = {};
+        _cachedSearchResultRows = {};
     }
 
     // Method Description:
@@ -2130,9 +2111,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
             // Start the throttled update of where our hyperlinks are.
             const auto shared = _shared.lock_shared();
-            if (shared->updatePatternLocations)
+            if (shared->outputIdle)
             {
-                (*shared->updatePatternLocations)();
+                (*shared->outputIdle)();
             }
         }
         catch (...)
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index ee2e1e5a841..adcaf821024 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -219,7 +219,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         void SetSelectionAnchor(const til::point position);
         void SetEndSelectionPoint(const til::point position);
 
-        void Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive);
+        SearchResults Search(const std::wstring_view& text, bool goForward, bool caseSensitive, bool reset);
         void ClearSearch();
         void SnapSearchResultToSelection(bool snap) noexcept;
         bool SnapSearchResultToSelection() const noexcept;
@@ -279,8 +279,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         til::typed_event<IInspectable, Control::RendererWarningArgs> RendererWarning;
         til::typed_event<IInspectable, Control::NoticeEventArgs> RaiseNotice;
         til::typed_event<IInspectable, Control::TransparencyChangedEventArgs> TransparencyChanged;
-        til::typed_event<> ReceivedOutput;
-        til::typed_event<IInspectable, Control::UpdateSearchResultsEventArgs> UpdateSearchResults;
+        til::typed_event<> OutputIdle;
         til::typed_event<IInspectable, Control::ShowWindowArgs> ShowWindowChanged;
         til::typed_event<IInspectable, Control::UpdateSelectionMarkersEventArgs> UpdateSelectionMarkers;
         til::typed_event<IInspectable, Control::OpenHyperlinkEventArgs> OpenHyperlink;
@@ -295,7 +294,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     private:
         struct SharedState
         {
-            std::unique_ptr<til::throttled_func_trailing<>> updatePatternLocations;
+            std::unique_ptr<til::debounced_func_trailing<>> outputIdle;
             std::shared_ptr<ThrottledFuncTrailing<Control::ScrollPositionChangedArgs>> updateScrollBar;
         };
 
@@ -376,7 +375,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                             const int bufferSize);
         void _terminalTaskbarProgressChanged();
         void _terminalShowWindowChanged(bool showOrHide);
-        void _terminalTextLayoutUpdated();
         void _terminalPlayMidiNote(const int noteNumber,
                                    const int velocity,
                                    const std::chrono::microseconds duration);
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index 038f21c0f41..ac5eed73452 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -49,6 +49,13 @@ namespace Microsoft.Terminal.Control
         Boolean EndAtRightBoundary;
     };
 
+    struct SearchResults
+    {
+        Int32 TotalMatches;
+        Int32 CurrentMatch;
+        Boolean SearchInvalidated;
+    };
+
     [default_interface] runtimeclass SelectionColor
     {
         SelectionColor();
@@ -127,7 +134,7 @@ namespace Microsoft.Terminal.Control
         void ResumeRendering();
         void BlinkAttributeTick();
 
-        void Search(String text, Boolean goForward, Boolean caseSensitive);
+        SearchResults Search(String text, Boolean goForward, Boolean caseSensitive, Boolean reset);
         void ClearSearch();
         IVector<Int32> SearchResultRows { get; };
         Boolean SnapSearchResultToSelection;
@@ -177,8 +184,7 @@ namespace Microsoft.Terminal.Control
         event Windows.Foundation.TypedEventHandler<Object, RendererWarningArgs> RendererWarning;
         event Windows.Foundation.TypedEventHandler<Object, NoticeEventArgs> RaiseNotice;
         event Windows.Foundation.TypedEventHandler<Object, TransparencyChangedEventArgs> TransparencyChanged;
-        event Windows.Foundation.TypedEventHandler<Object, Object> ReceivedOutput;
-        event Windows.Foundation.TypedEventHandler<Object, UpdateSearchResultsEventArgs> UpdateSearchResults;
+        event Windows.Foundation.TypedEventHandler<Object, Object> OutputIdle;
         event Windows.Foundation.TypedEventHandler<Object, UpdateSelectionMarkersEventArgs> UpdateSelectionMarkers;
         event Windows.Foundation.TypedEventHandler<Object, OpenHyperlinkEventArgs> OpenHyperlink;
         event Windows.Foundation.TypedEventHandler<Object, Object> CloseTerminalRequested;
diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp
index 4c4d1690953..fb69e9e0541 100644
--- a/src/cascadia/TerminalControl/EventArgs.cpp
+++ b/src/cascadia/TerminalControl/EventArgs.cpp
@@ -12,7 +12,6 @@
 #include "ScrollPositionChangedArgs.g.cpp"
 #include "RendererWarningArgs.g.cpp"
 #include "TransparencyChangedEventArgs.g.cpp"
-#include "UpdateSearchResultsEventArgs.g.cpp"
 #include "ShowWindowArgs.g.cpp"
 #include "UpdateSelectionMarkersEventArgs.g.cpp"
 #include "CompletionsChangedEventArgs.g.cpp"
diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h
index d8990c6f212..423e5695d73 100644
--- a/src/cascadia/TerminalControl/EventArgs.h
+++ b/src/cascadia/TerminalControl/EventArgs.h
@@ -12,7 +12,6 @@
 #include "ScrollPositionChangedArgs.g.h"
 #include "RendererWarningArgs.g.h"
 #include "TransparencyChangedEventArgs.g.h"
-#include "UpdateSearchResultsEventArgs.g.h"
 #include "ShowWindowArgs.g.h"
 #include "UpdateSelectionMarkersEventArgs.g.h"
 #include "CompletionsChangedEventArgs.g.h"
@@ -141,17 +140,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         WINRT_PROPERTY(float, Opacity);
     };
 
-    struct UpdateSearchResultsEventArgs : public UpdateSearchResultsEventArgsT<UpdateSearchResultsEventArgs>
-    {
-    public:
-        UpdateSearchResultsEventArgs() = default;
-
-        WINRT_PROPERTY(SearchState, State, SearchState::Inactive);
-        WINRT_PROPERTY(bool, FoundMatch);
-        WINRT_PROPERTY(int32_t, TotalMatches);
-        WINRT_PROPERTY(int32_t, CurrentMatch);
-    };
-
     struct ShowWindowArgs : public ShowWindowArgsT<ShowWindowArgs>
     {
     public:
diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl
index d04044073e8..6b0bf11e359 100644
--- a/src/cascadia/TerminalControl/EventArgs.idl
+++ b/src/cascadia/TerminalControl/EventArgs.idl
@@ -84,14 +84,6 @@ namespace Microsoft.Terminal.Control
         Active = 1,
     };
 
-    runtimeclass UpdateSearchResultsEventArgs
-    {
-        SearchState State { get; };
-        Boolean FoundMatch { get; };
-        Int32 TotalMatches { get; };
-        Int32 CurrentMatch { get; };
-    }
-
     runtimeclass ShowWindowArgs
     {
         Boolean ShowOrHide { get; };
diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp
index 9ce4bba86a8..eaaae04add9 100644
--- a/src/cascadia/TerminalControl/SearchBoxControl.cpp
+++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp
@@ -195,6 +195,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         }
     }
 
+    winrt::hstring SearchBoxControl::Text()
+    {
+        return TextBox().Text();
+    }
+
     // Method Description:
     // - Check if the current search direction is forward
     // Arguments:
@@ -202,7 +207,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // Return Value:
     // - bool: the current search direction, determined by the
     //         states of the two direction buttons
-    bool SearchBoxControl::_GoForward()
+    bool SearchBoxControl::GoForward()
     {
         return GoForwardButton().IsChecked().GetBoolean();
     }
@@ -214,7 +219,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // Return Value:
     // - bool: whether the current search is case sensitive (case button is checked )
     //   or not
-    bool SearchBoxControl::_CaseSensitive()
+    bool SearchBoxControl::CaseSensitive()
     {
         return CaseSensitivityButton().IsChecked().GetBoolean();
     }
@@ -240,11 +245,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             const auto state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift);
             if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down))
             {
-                Search.raise(TextBox().Text(), !_GoForward(), _CaseSensitive());
+                Search.raise(Text(), !GoForward(), CaseSensitive());
             }
             else
             {
-                Search.raise(TextBox().Text(), _GoForward(), _CaseSensitive());
+                Search.raise(Text(), GoForward(), CaseSensitive());
             }
             e.Handled(true);
         }
@@ -335,7 +340,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         }
 
         // kick off search
-        Search.raise(TextBox().Text(), _GoForward(), _CaseSensitive());
+        Search.raise(Text(), GoForward(), CaseSensitive());
     }
 
     // Method Description:
@@ -356,7 +361,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         }
 
         // kick off search
-        Search.raise(TextBox().Text(), _GoForward(), _CaseSensitive());
+        Search.raise(Text(), GoForward(), CaseSensitive());
     }
 
     // Method Description:
@@ -394,7 +399,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // - <none>
     void SearchBoxControl::TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/)
     {
-        SearchChanged.raise(TextBox().Text(), _GoForward(), _CaseSensitive());
+        SearchChanged.raise(Text(), GoForward(), CaseSensitive());
     }
 
     // Method Description:
@@ -406,7 +411,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // - <none>
     void SearchBoxControl::CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/)
     {
-        SearchChanged.raise(TextBox().Text(), _GoForward(), _CaseSensitive());
+        SearchChanged.raise(Text(), GoForward(), CaseSensitive());
     }
 
     // Method Description:
@@ -528,20 +533,4 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     {
         StatusBox().Text(L"");
     }
-
-    // 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 53a738c399e..60ea00da481 100644
--- a/src/cascadia/TerminalControl/SearchBoxControl.h
+++ b/src/cascadia/TerminalControl/SearchBoxControl.h
@@ -35,13 +35,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         void Open(std::function<void()> callback);
         void Close();
 
+        winrt::hstring Text();
+        bool GoForward();
+        bool CaseSensitive();
         void SetFocusOnTextbox();
         void PopulateTextbox(const winrt::hstring& text);
         bool ContainsFocus();
         void SetStatus(int32_t totalMatches, int32_t currentMatch);
         void ClearStatus();
-        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*/);
@@ -77,8 +78,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         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);
         void _CharacterHandler(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::Input::CharacterReceivedRoutedEventArgs& e);
     };
diff --git a/src/cascadia/TerminalControl/SearchBoxControl.idl b/src/cascadia/TerminalControl/SearchBoxControl.idl
index 42abd1fa1b3..3abf4f50229 100644
--- a/src/cascadia/TerminalControl/SearchBoxControl.idl
+++ b/src/cascadia/TerminalControl/SearchBoxControl.idl
@@ -16,8 +16,6 @@ namespace Microsoft.Terminal.Control
         Windows.Foundation.Rect ContentClipRect{ get; };
         Double OpenAnimationStartPoint{ get; };
 
-        Boolean NavigationEnabled;
-
         event SearchHandler Search;
         event SearchHandler SearchChanged;
         event Windows.Foundation.TypedEventHandler<SearchBoxControl, Windows.UI.Xaml.RoutedEventArgs> Closed;
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 81934f10f65..22efc0b18e2 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -204,7 +204,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         _revokers.TransparencyChanged = _core.TransparencyChanged(winrt::auto_revoke, { get_weak(), &TermControl::_coreTransparencyChanged });
         _revokers.RaiseNotice = _core.RaiseNotice(winrt::auto_revoke, { get_weak(), &TermControl::_coreRaisedNotice });
         _revokers.HoveredHyperlinkChanged = _core.HoveredHyperlinkChanged(winrt::auto_revoke, { get_weak(), &TermControl::_hoveredHyperlinkChanged });
-        _revokers.UpdateSearchResults = _core.UpdateSearchResults(winrt::auto_revoke, { get_weak(), &TermControl::_coreUpdateSearchResults });
+        _revokers.OutputIdle = _core.OutputIdle(winrt::auto_revoke, { get_weak(), &TermControl::_coreOutputIdle });
         _revokers.UpdateSelectionMarkers = _core.UpdateSelectionMarkers(winrt::auto_revoke, { get_weak(), &TermControl::_updateSelectionMarkers });
         _revokers.coreOpenHyperlink = _core.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler });
         _revokers.interactivityOpenHyperlink = _interactivity.OpenHyperlink(winrt::auto_revoke, { get_weak(), &TermControl::_HyperlinkHandler });
@@ -534,13 +534,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                     // but since code paths differ, extra work is required to ensure correctness.
                     if (!_core.HasMultiLineSelection())
                     {
-                        _core.SnapSearchResultToSelection(true);
                         const auto selectedLine{ _core.SelectedText(true) };
                         _searchBox->PopulateTextbox(selectedLine);
                     }
                 }
 
-                _searchBox->Open([searchBox]() { searchBox.SetFocusOnTextbox(); });
+                _searchBox->Open([weakThis = get_weak()]() {
+                    if (const auto self = weakThis.get(); !self->_IsClosing())
+                    {
+                        self->_searchBox->SetFocusOnTextbox();
+                        self->_refreshSearch();
+                    }
+                });
             }
         }
     }
@@ -551,13 +556,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         {
             return;
         }
-        if (!_searchBox)
+        if (!_searchBox || _searchBox->Visibility() != Visibility::Visible)
         {
             CreateSearchBoxControl();
         }
         else
         {
-            _core.Search(_searchBox->TextBox().Text(), goForward, false);
+            _handleSearchResults(_core.Search(_searchBox->Text(), goForward, _searchBox->CaseSensitive(), false));
         }
     }
 
@@ -588,7 +593,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                               const bool goForward,
                               const bool caseSensitive)
     {
-        _core.Search(text, goForward, caseSensitive);
+        _handleSearchResults(_core.Search(text, goForward, caseSensitive, false));
     }
 
     // Method Description:
@@ -605,7 +610,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     {
         if (_searchBox && _searchBox->Visibility() == Visibility::Visible)
         {
-            _core.Search(text, goForward, caseSensitive);
+            _handleSearchResults(_core.Search(text, goForward, caseSensitive, false));
         }
     }
 
@@ -621,6 +626,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                              const RoutedEventArgs& /*args*/)
     {
         _searchBox->Close();
+        _core.ClearSearch();
 
         // Set focus back to terminal control
         this->Focus(FocusState::Programmatic);
@@ -3537,69 +3543,63 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         return _core.SelectedText(trimTrailingWhitespace);
     }
 
-    // Method Description:
-    // - Triggers an update on scrollbar to redraw search scroll marks.
-    // - Called when the search results have changed, and the scroll marks'
-    //   positions need to be updated.
-    void TermControl::_UpdateSearchScrollMarks()
+    void TermControl::_refreshSearch()
     {
-        // Manually send a scrollbar update, 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 search marks. The Core
-        // might send a scrollbar update 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->Visibility() != Visibility::Visible)
+        {
+            return;
+        }
+
+        const auto text = _searchBox->Text();
+        if (text.empty())
+        {
+            return;
+        }
+
+        const auto goForward = _searchBox->GoForward();
+        const auto caseSensitive = _searchBox->CaseSensitive();
+        _handleSearchResults(_core.Search(text, goForward, caseSensitive, true));
     }
 
-    // Method Description:
-    // - Called when the core raises a UpdateSearchResults event. That's done in response to:
-    //   - starting a search query with ControlCore::Search.
-    //   - clearing search results due to change in buffer content.
-    // - The args will tell us if there were or were not any results for that
-    //   particular search. We'll use that to control what to announce to
-    //   Narrator. When we have more elaborate search information to report, we
-    //   may want to report that here. (see GH #3920)
-    // Arguments:
-    // - args: contains information about the search state and results that were
-    //         or were not found.
-    // Return Value:
-    // - <none>
-    winrt::fire_and_forget TermControl::_coreUpdateSearchResults(const IInspectable& /*sender*/, Control::UpdateSearchResultsEventArgs args)
+    void TermControl::_handleSearchResults(SearchResults results)
     {
-        co_await wil::resume_foreground(Dispatcher());
-        if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this) })
+        if (!_searchBox)
         {
-            automationPeer.RaiseNotificationEvent(
-                Automation::Peers::AutomationNotificationKind::ActionCompleted,
-                Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent,
-                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 */);
+            return;
         }
 
-        _UpdateSearchScrollMarks();
+        _searchBox->SetStatus(results.TotalMatches, results.CurrentMatch);
 
-        if (_searchBox)
+        if (results.SearchInvalidated)
         {
-            _searchBox->NavigationEnabled(true);
-            if (args.State() == Control::SearchState::Inactive)
+            if (_showMarksInScrollbar)
             {
-                _searchBox->ClearStatus();
+                const auto scrollBar = ScrollBar();
+                ScrollBarUpdate update{
+                    .newValue = scrollBar.Value(),
+                    .newMaximum = scrollBar.Maximum(),
+                    .newMinimum = scrollBar.Minimum(),
+                    .newViewportSize = scrollBar.ViewportSize(),
+                };
+                _updateScrollBar->Run(update);
             }
-            else
+
+            if (auto automationPeer{ FrameworkElementAutomationPeer::FromElement(*this) })
             {
-                _searchBox->SetStatus(args.TotalMatches(), args.CurrentMatch());
+                automationPeer.RaiseNotificationEvent(
+                    AutomationNotificationKind::ActionCompleted,
+                    AutomationNotificationProcessing::ImportantMostRecent,
+                    results.TotalMatches > 0 ? RS_(L"SearchBox_MatchesAvailable") : RS_(L"SearchBox_NoMatches"), // what to announce if results were found
+                    L"SearchBoxResultAnnouncement" /* unique name for this group of notifications */);
             }
         }
     }
 
+    void TermControl::_coreOutputIdle(const IInspectable& /*sender*/, const IInspectable& /*args*/)
+    {
+        _refreshSearch();
+    }
+
     void TermControl::OwningHwnd(uint64_t owner)
     {
         _core.OwningHwnd(owner);
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index 8568cb49b0a..4b7c4a50784 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -371,10 +371,10 @@ 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);
-        void _UpdateSearchScrollMarks();
+        void _refreshSearch();
+        void _handleSearchResults(SearchResults results);
 
         void _hoveredHyperlinkChanged(const IInspectable& sender, const IInspectable& args);
         winrt::fire_and_forget _updateSelectionMarkers(IInspectable sender, Control::UpdateSelectionMarkersEventArgs args);
@@ -383,7 +383,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);
-        winrt::fire_and_forget _coreUpdateSearchResults(const IInspectable& sender, Control::UpdateSearchResultsEventArgs args);
+        void _coreOutputIdle(const IInspectable& sender, const IInspectable& args);
 
         til::point _toPosInDips(const Core::Point terminalCellPos);
         void _throttledUpdateScrollbar(const ScrollBarUpdate& update);
@@ -411,7 +411,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             Control::ControlCore::TransparencyChanged_revoker TransparencyChanged;
             Control::ControlCore::RaiseNotice_revoker RaiseNotice;
             Control::ControlCore::HoveredHyperlinkChanged_revoker HoveredHyperlinkChanged;
-            Control::ControlCore::UpdateSearchResults_revoker UpdateSearchResults;
+            Control::ControlCore::OutputIdle_revoker OutputIdle;
             Control::ControlCore::UpdateSelectionMarkers_revoker UpdateSelectionMarkers;
             Control::ControlCore::OpenHyperlink_revoker coreOpenHyperlink;
             Control::ControlCore::TitleChanged_revoker TitleChanged;
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 25ea33fd47b..854d54394d0 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -1230,11 +1230,6 @@ void Microsoft::Terminal::Core::Terminal::CompletionsChangedCallback(std::functi
     _pfnCompletionsChanged.swap(pfn);
 }
 
-void Terminal::SetTextLayoutUpdatedCallback(std::function<void()> pfn) noexcept
-{
-    _pfnTextLayoutUpdated.swap(pfn);
-}
-
 // Method Description:
 // - Stores the search highlighted regions in the terminal
 void Terminal::SetSearchHighlights(const std::vector<til::point_span>& highlights) noexcept
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 7f1c20789e9..75b822b1386 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -155,7 +155,6 @@ class Microsoft::Terminal::Core::Terminal final :
     bool IsVtInputEnabled() const noexcept override;
     void NotifyAccessibilityChange(const til::rect& changedRect) noexcept override;
     void NotifyBufferRotation(const int delta) override;
-    void NotifyTextLayoutUpdated() override;
 
     void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override;
 
@@ -231,7 +230,6 @@ class Microsoft::Terminal::Core::Terminal final :
     void SetShowWindowCallback(std::function<void(bool)> pfn) noexcept;
     void SetPlayMidiNoteCallback(std::function<void(const int, const int, const std::chrono::microseconds)> pfn) noexcept;
     void CompletionsChangedCallback(std::function<void(std::wstring_view, unsigned int)> pfn) noexcept;
-    void SetTextLayoutUpdatedCallback(std::function<void()> pfn) noexcept;
     void SetSearchHighlights(const std::vector<til::point_span>& highlights) noexcept;
     void SetSearchHighlightFocused(const size_t focusedIdx);
 
@@ -341,7 +339,6 @@ class Microsoft::Terminal::Core::Terminal final :
     std::function<void(bool)> _pfnShowWindowChanged;
     std::function<void(const int, const int, const std::chrono::microseconds)> _pfnPlayMidiNote;
     std::function<void(std::wstring_view, unsigned int)> _pfnCompletionsChanged;
-    std::function<void()> _pfnTextLayoutUpdated;
 
     RenderSettings _renderSettings;
     std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine;
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index 07c81649b7c..42f1c903c33 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -239,8 +239,6 @@ void Terminal::UseAlternateScreenBuffer(const TextAttribute& attrs)
     // Update scrollbars
     _NotifyScrollEvent();
 
-    NotifyTextLayoutUpdated();
-
     // redraw the screen
     try
     {
@@ -298,8 +296,6 @@ void Terminal::UseMainScreenBuffer()
     // Update scrollbars
     _NotifyScrollEvent();
 
-    NotifyTextLayoutUpdated();
-
     // redraw the screen
     _activeBuffer().TriggerRedrawAll();
 }
@@ -374,15 +370,3 @@ void Terminal::NotifyBufferRotation(const int delta)
         _NotifyScrollEvent();
     }
 }
-
-// Method Description:
-// - Notifies the terminal UI layer that the text layout has changed.
-// - This will be called when new text is added, or when the text is
-//   rearranged in the buffer due to window resize.
-void Terminal::NotifyTextLayoutUpdated()
-{
-    if (_pfnTextLayoutUpdated)
-    {
-        _pfnTextLayoutUpdated();
-    }
-}
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index 25a2f1fdb2d..fb20d74b75b 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -424,12 +424,7 @@ void ConhostInternalGetSet::NotifyBufferRotation(const int delta)
     }
 }
 
-void ConhostInternalGetSet::NotifyTextLayoutUpdated()
-{
-    // Not implemented for conhost.
-}
-
 void ConhostInternalGetSet::InvokeCompletions(std::wstring_view /*menuJson*/, unsigned int /*replaceLength*/)
 {
     // Not implemented for conhost.
-}
\ No newline at end of file
+}
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index ca5711f6387..01d8abaf17f 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -68,7 +68,6 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
 
     void NotifyAccessibilityChange(const til::rect& changedRect) override;
     void NotifyBufferRotation(const int delta) override;
-    void NotifyTextLayoutUpdated() override;
 
     void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override;
 
diff --git a/src/inc/til/throttled_func.h b/src/inc/til/throttled_func.h
index 01c4e356aa3..d250f475af5 100644
--- a/src/inc/til/throttled_func.h
+++ b/src/inc/til/throttled_func.h
@@ -81,7 +81,7 @@ namespace til
         };
     } // namespace details
 
-    template<bool leading, typename... Args>
+    template<bool Debounce, bool Leading, typename... Args>
     class throttled_func
     {
     public:
@@ -118,15 +118,35 @@ namespace til
         throttled_func& operator=(throttled_func&&) = delete;
 
         // Throttles the invocation of the function passed to the constructor.
-        // If this is a trailing_throttled_func:
-        //   If you call this function again before the underlying
-        //   timer has expired, the new arguments will be used.
+        //
+        // If Debounce is true and you call this function again before the
+        // underlying timer has expired, its timeout will be reset.
+        //
+        // If Leading is true and you call this function again before the
+        // underlying timer has expired, the new arguments will be used.
         template<typename... MakeArgs>
         void operator()(MakeArgs&&... args)
         {
-            if (!_storage.emplace(std::forward<MakeArgs>(args)...))
+            const auto hadValue = _storage.emplace(std::forward<MakeArgs>(args)...);
+
+            if constexpr (Debounce)
             {
-                _leading_edge();
+                SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
+            }
+            else
+            {
+                if (!hadValue)
+                {
+                    SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
+                }
+            }
+
+            if constexpr (Leading)
+            {
+                if (!hadValue)
+                {
+                    _func();
+                }
             }
         }
 
@@ -165,19 +185,9 @@ namespace til
         }
         CATCH_LOG()
 
-        void _leading_edge()
-        {
-            if constexpr (leading)
-            {
-                _func();
-            }
-
-            SetThreadpoolTimerEx(_timer.get(), &_delay, 0, 0);
-        }
-
         void _trailing_edge()
         {
-            if constexpr (leading)
+            if constexpr (Leading)
             {
                 _storage.reset();
             }
@@ -187,7 +197,7 @@ namespace til
             }
         }
 
-        inline wil::unique_threadpool_timer _createTimer()
+        wil::unique_threadpool_timer _createTimer()
         {
             wil::unique_threadpool_timer timer{ CreateThreadpoolTimer(&_timer_callback, this, nullptr) };
             THROW_LAST_ERROR_IF(!timer);
@@ -201,6 +211,10 @@ namespace til
     };
 
     template<typename... Args>
-    using throttled_func_trailing = throttled_func<false, Args...>;
-    using throttled_func_leading = throttled_func<true>;
+    using throttled_func_trailing = throttled_func<false, false, Args...>;
+    using throttled_func_leading = throttled_func<false, true>;
+
+    template<typename... Args>
+    using debounced_func_trailing = throttled_func<true, false, Args...>;
+    using debounced_func_leading = throttled_func<true, true>;
 } // namespace til
diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp
index 9ef05279bf5..674dc3baf7e 100644
--- a/src/renderer/atlas/AtlasEngine.api.cpp
+++ b/src/renderer/atlas/AtlasEngine.api.cpp
@@ -5,6 +5,7 @@
 #include "AtlasEngine.h"
 
 #include "Backend.h"
+#include "../../buffer/out/textBuffer.hpp"
 #include "../base/FontCache.h"
 
 // #### NOTE ####
@@ -95,7 +96,7 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2<T>& out) noexcept
     return S_OK;
 }
 
-[[nodiscard]] HRESULT AtlasEngine::InvalidateHighlight(std::span<const til::point_span> highlights, const std::vector<LineRendition>& renditions) noexcept
+[[nodiscard]] HRESULT AtlasEngine::InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept
 {
     const auto viewportOrigin = til::point{ _api.s->viewportOffset.x, _api.s->viewportOffset.y };
     const auto viewport = til::rect{ 0, 0, _api.s->viewportCellCount.x, _api.s->viewportCellCount.y };
@@ -103,7 +104,7 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2<T>& out) noexcept
     for (const auto& hi : highlights)
     {
         hi.iterate_rows(cellCountX, [&](til::CoordType row, til::CoordType beg, til::CoordType end) {
-            const auto shift = til::at(renditions, row) != LineRendition::SingleWidth ? 1 : 0;
+            const auto shift = buffer.GetLineRendition(row) != LineRendition::SingleWidth ? 1 : 0;
             beg <<= shift;
             end <<= shift;
             til::rect rect{ beg, row, end + 1, row + 1 };
diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h
index 9f38b7094ff..9ad4425beae 100644
--- a/src/renderer/atlas/AtlasEngine.h
+++ b/src/renderer/atlas/AtlasEngine.h
@@ -33,7 +33,7 @@ namespace Microsoft::Console::Render::Atlas
         [[nodiscard]] HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept override;
         [[nodiscard]] HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept override;
         [[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
-        [[nodiscard]] HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const std::vector<LineRendition>& renditions) noexcept override;
+        [[nodiscard]] HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept override;
         [[nodiscard]] HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept override;
         [[nodiscard]] HRESULT InvalidateAll() noexcept override;
         [[nodiscard]] HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept override;
diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp
index 6ab523a1a4d..1847c1d287f 100644
--- a/src/renderer/base/RenderEngineBase.cpp
+++ b/src/renderer/base/RenderEngineBase.cpp
@@ -7,7 +7,7 @@
 using namespace Microsoft::Console;
 using namespace Microsoft::Console::Render;
 
-[[nodiscard]] HRESULT RenderEngineBase::InvalidateHighlight(std::span<const til::point_span> /*highlights*/, const std::vector<LineRendition>& /*renditions*/) noexcept
+[[nodiscard]] HRESULT RenderEngineBase::InvalidateHighlight(std::span<const til::point_span> /*highlights*/, const TextBuffer& /*renditions*/) noexcept
 {
     return S_OK;
 }
diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp
index deb8925c943..82b1eaea1b4 100644
--- a/src/renderer/base/renderer.cpp
+++ b/src/renderer/base/renderer.cpp
@@ -459,24 +459,21 @@ void Renderer::TriggerSelection()
 void Renderer::TriggerSearchHighlight(const std::vector<til::point_span>& oldHighlights)
 try
 {
-    const auto& buffer = _pData->GetTextBuffer();
-    const auto rows = buffer.TotalRowCount();
+    // no need to invalidate focused search highlight separately as they are
+    // included in (all) search highlights.
+    const auto newHighlights = _pData->GetSearchHighlights();
 
-    std::vector<LineRendition> renditions;
-    renditions.reserve(rows);
-    for (til::CoordType row = 0; row < rows; ++row)
+    if (oldHighlights.empty() && newHighlights.empty())
     {
-        renditions.emplace_back(buffer.GetLineRendition(row));
+        return;
     }
 
-    // no need to invalidate focused search highlight separately as they are
-    // included in (all) search highlights.
-    const auto newHighlights = _pData->GetSearchHighlights();
+    const auto& buffer = _pData->GetTextBuffer();
 
     FOREACH_ENGINE(pEngine)
     {
-        LOG_IF_FAILED(pEngine->InvalidateHighlight(oldHighlights, renditions));
-        LOG_IF_FAILED(pEngine->InvalidateHighlight(newHighlights, renditions));
+        LOG_IF_FAILED(pEngine->InvalidateHighlight(oldHighlights, buffer));
+        LOG_IF_FAILED(pEngine->InvalidateHighlight(newHighlights, buffer));
     }
 
     NotifyPaintFrame();
diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp
index 7533de8a4d0..a7afb4244ba 100644
--- a/src/renderer/inc/IRenderEngine.hpp
+++ b/src/renderer/inc/IRenderEngine.hpp
@@ -67,7 +67,7 @@ namespace Microsoft::Console::Render
         [[nodiscard]] virtual HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept = 0;
         [[nodiscard]] virtual HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept = 0;
         [[nodiscard]] virtual HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept = 0;
-        [[nodiscard]] virtual HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const std::vector<LineRendition>& renditions) noexcept = 0;
+        [[nodiscard]] virtual HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept = 0;
         [[nodiscard]] virtual HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept = 0;
         [[nodiscard]] virtual HRESULT InvalidateAll() noexcept = 0;
         [[nodiscard]] virtual HRESULT InvalidateFlush(_In_ const bool circled, _Out_ bool* const pForcePaint) noexcept = 0;
diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp
index 5fd8a94c8a3..1108921edb5 100644
--- a/src/renderer/inc/RenderEngineBase.hpp
+++ b/src/renderer/inc/RenderEngineBase.hpp
@@ -24,7 +24,7 @@ namespace Microsoft::Console::Render
     class RenderEngineBase : public IRenderEngine
     {
     public:
-        [[nodiscard]] HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const std::vector<LineRendition>& renditions) noexcept override;
+        [[nodiscard]] HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept override;
         [[nodiscard]] HRESULT InvalidateTitle(const std::wstring_view proposedTitle) noexcept override;
 
         [[nodiscard]] HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept override;
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index b42ffe283f9..4381c4ecbdd 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -80,7 +80,6 @@ namespace Microsoft::Console::VirtualTerminal
 
         virtual void NotifyAccessibilityChange(const til::rect& changedRect) = 0;
         virtual void NotifyBufferRotation(const int delta) = 0;
-        virtual void NotifyTextLayoutUpdated() = 0;
 
         virtual void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) = 0;
     };
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 1a887f99f83..34bc5cd80e6 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -182,7 +182,6 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
     // It's important to do this here instead of in TextBuffer, because here you
     // have access to the entire line of text, whereas TextBuffer writes it one
     // character at a time via the OutputCellIterator.
-    _api.NotifyTextLayoutUpdated();
     textBuffer.TriggerNewTextNotification(string);
 }
 
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index b17bc4429f9..5cd5fbdc29e 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -215,11 +215,6 @@ class TestGetSet final : public ITerminalApi
         Log::Comment(L"NotifyBufferRotation MOCK called...");
     }
 
-    void NotifyTextLayoutUpdated() override
-    {
-        Log::Comment(L"NotifyTextLayoutUpdated MOCK called...");
-    }
-
     void InvokeCompletions(std::wstring_view menuJson, unsigned int replaceLength) override
     {
         Log::Comment(L"InvokeCompletions MOCK called...");


Test patch: