Fix search highlights during reflow (#17092)

This PR extends `til::throttled_func` to also support debouncing:
* throttling: "At most 1 call every N seconds"
* debouncing: "Exactly 1 call after N seconds of inactivity"

Based on the latter the following series of changes were made:
* An `OutputIdle` event was added to `ControlCore` which is
  raised once there hasn't been any incoming data in 100ms.
  This also triggers an update of our regex patterns (URL detection).
* The event is then caught by `TermControl` which calls `Search()`.
* `Search()` in turn was modified to return its results by-value
  as a struct, which avoids the need for a search-update event
  and simplifies how we update the UI.

This architectural change, most importantly the removal of the
`TextLayoutUpdated` event, fixes a DoS bug in Windows Terminal:
As the event leads to UI thread activity, printing lots of text
continuously results in the UI thread becoming unresponsive.

On top of these, a number of improvements were made:
* `IRenderEngine::InvalidateHighlight` was changed to take the
  `TextBuffer` by-reference which avoids the need to accumulate the
  line renditions in a `std::vector` first. This improves Debug build
  performance during reflow by what I guess must be roughly
  a magnitude faster. This difference is very noticeable.
* When closing the search box, `ClearSearch()` is called to remove
  the highlights. The search text is restored when it's reopened,
  however the current search position isn't.

Closes #17073
Closes #17089

## Validation Steps Performed
* UIA announcements:
  * Pressing Ctrl+Shift+F the first time does not lead to one 
  * Typing the first letter does 
  * Closing doesn't 
  * Reopening does (as it restores the letter) 
* Closing the search box dismisses the highlights 
* Resizing the window recalculates the highlights 
* Changing the terminal output while the box is open
  recalculates the highlights 
This commit is contained in:
Leonard Hecker 2024-04-24 00:04:35 +02:00 committed by GitHub
parent 87a9f72b9a
commit 360e86b536
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 203 additions and 276 deletions

View File

@ -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;
}
}

View File

@ -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;
}
// let the UI know that the text layout has been updated
_terminal->NotifyTextLayoutUpdated();
_connection.Resize(vp.Height(), vp.Width());
// 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());
}
int32_t totalMatches = 0;
int32_t currentMatch = 0;
if (const auto idx = _searcher.CurrentMatch(); idx >= 0)
{
totalMatches = gsl::narrow<int32_t>(_searcher.Results().size());
currentMatch = gsl::narrow<int32_t>(idx);
_terminal->SetSearchHighlightFocused(gsl::narrow<size_t>(idx));
}
_renderer->TriggerSearchHighlight(oldResults);
auto evArgs = winrt::make_self<implementation::UpdateSearchResultsEventArgs>();
if (!text.empty())
{
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()));
}
}
// 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);
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 (...)

View File

@ -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);

View File

@ -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;

View File

@ -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"

View File

@ -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:

View File

@ -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; };

View File

@ -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();
}
}

View File

@ -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);
};

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -155,7 +155,6 @@ public:
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 @@ public:
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 @@ private:
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;

View File

@ -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();
}
}

View File

@ -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.
}
}

View File

@ -68,7 +68,6 @@ public:
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;

View File

@ -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

View File

@ -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 };

View File

@ -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;

View File

@ -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;
}

View File

@ -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();
std::vector<LineRendition> renditions;
renditions.reserve(rows);
for (til::CoordType row = 0; row < rows; ++row)
{
renditions.emplace_back(buffer.GetLineRendition(row));
}
// no need to invalidate focused search highlight separately as they are
// included in (all) search highlights.
const auto newHighlights = _pData->GetSearchHighlights();
if (oldHighlights.empty() && newHighlights.empty())
{
return;
}
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();

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -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);
}

View File

@ -215,11 +215,6 @@ public:
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...");