-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix search highlights during reflow #17092
Conversation
@@ -13,23 +13,25 @@ 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 && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made this change because I grew a little worried about the robustness of the _lastMutationId == lastMutationId
check.
{ | ||
self->OutputIdle.raise(*self, nullptr); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can imagine that this debounced event may be quite useful for other things in the future.
@@ -1691,30 +1678,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation | |||
} | |||
|
|||
_terminal->SetSearchHighlights(_searcher.Results()); | |||
_terminal->SetSearchHighlightFocused(_searcher.CurrentMatch()); | |||
_renderer->TriggerSearchHighlight(oldResults); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I can tell, we only need to call this function if the search got reset so I moved it inside this branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This results in focused search highlight to be not invalidated even though it might change in the below else
block.
I know it sounds inefficient to invalidate all highlights when just the focused one is changed, but to fix this inefficiency I guess you have to add TriggerSearchHighlightFocused()
or something along those lines. And the Atlas engine also doesn't know which one got invalidated so it currently just re-draws both 🤕🥲
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see. I think it's fine to have a bit of inefficiency here. I bet that the invalidation finishes in less than 1ms even if you had a million search hits - and we only refresh it 10 times per second at most.
If we get to the point where we have buffer snapshotting in the renderer, we can simply scribble the highlights into the snapshot directly. Then we simply check if the text or attributes of each row have changed compared to the last snapshot. (= We have 2 snapshots which we swap back and forth every other frame.) I'm pretty excited for that, as all those subtle issues like this one will just instantly vanish.
} | ||
else | ||
else if (!reset) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows us to ensure that we don't accidentally call FindNext()
just because the OutputIdle
event got triggered, which results in a Search()
call.
_terminal->SetSearchHighlights({}); | ||
_terminal->SetSearchHighlightFocused({}); | ||
_renderer->TriggerSearchHighlight(_searcher.Results()); | ||
_searcher = {}; | ||
_cachedSearchResultRows = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't check _searcher.GetCurrent()
here because otherwise we don't reset the _lastMutationId
of a Searcher
that found nothing. This is relevant in case you open the search box (the Ctrl+Shift+F one), enter something that doesn't exist, close it, and reopen it. If it gets closed and doesn't get reset here, then reopening it won't trigger a search otherwise.
.newMinimum = scrollBar.Minimum(), | ||
.newViewportSize = scrollBar.ViewportSize(), | ||
}; | ||
_updateScrollBar->Run(update); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the throttled func here is IMO just fine, and should ensure that we don't unnecessarily overwrite a pending scrollbar update.
|
||
if (_searchBox) | ||
if (results.SearchInvalidated) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This avoids doing a RaiseNotificationEvent
even though the search mask hasn't changed.
@@ -95,15 +96,15 @@ 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing the TextBuffer
directly is IMO fine, because long-term we'll remove Renderer
and give each render engine direct access to the underlying TextBuffer
anyway. (In all my tests so far I found that we can remove at least half of all rendering code by doing that. I'm excited to think about how much easier it'll make introducing new features!)
BTW, as mentioned in the PR comment, you may have noticed that a debug build felt kind of laggy while resizing the window - This change fixes it.
@tusharsnx I apologize for making this many changes so soon after we merged your PR. I hope these are fine. 😣 They do fix a couple more edge cases around highlights, however. If you have any concerns with my changes, or dislike anything, let me know and I'll try to fix it ASAP. BTW |
I'm ABSOLUTELY
Its idempotency is questionable 😅 OK, now on to this PR:
|
.newViewportSize = scrollBar.ViewportSize(), | ||
}; | ||
_throttledUpdateScrollbar(update); | ||
if (!_searchBox || _searchBox->Visibility() != Visibility::Visible) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this cause a problem when the search box is closed and the user uses Find previous/next match actions to navigate through the results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I wasn't aware we had an action for that. If you use such an action in VS Code while its search box is closed, it opens it. I think we should do the same thing. Thanks for finding this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Did we fix this?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both underlying issues should be fixed now:
- When using the action, we now open the search box. This triggers a search once it finished opening
- While searching we'll now always call
TriggerSearchHighlight
{ | ||
co_await wil::resume_foreground(Dispatcher()); | ||
if (auto automationPeer{ Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this) }) | ||
if (!_searchBox) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same query as above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this one isn't a problem because we're only checking for the existence of the search box and not its visibility.
FWIW this does seem to fix #17089 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm 23/27 but i'll review those in post tomorrow, I just want my sweet sweet canary builds back
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
confirming: yea I'm cool with this
if (!_storage.emplace(std::forward<MakeArgs>(args)...)) | ||
const auto hadValue = _storage.emplace(std::forward<MakeArgs>(args)...); | ||
|
||
if constexpr (Debounce) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh oh okay I get it now. For a while I was thinking "a debounced trailing func is just a trailing func", but the difference is subtle:
- A trailing func will call the callback {one timeout} after the first call to the
throttled_func
. If there are more calls to thethrottled_func
in that timeout, they'll just update the value which will be used at the end of the first timeout. - A debounced trailing func will call the callback {one timeout} after the last call to the
throttled_func
. If there are more calls to thethrottled_func
in that timeout, they'll further delay the callback.
Sorry for the late report, but it seems like this is not yet fixed:
|
Ah fuck I forgot about that comment. Sorry about that! 😣 |
This addresses a review comment left by tusharsnx in #17092 which I forgot to fix before merging the PR. The fix itself is somewhat simple: `Terminal::SetSearchHighlightFocused` triggers a scroll if the target is outside of the current (scrolled) viewport and avoiding the call unless necessary fixes it. To do it properly though, I've split up `Search::ResetIfStale` into `IsStale` and `Reset`. Now we can properly detect staleness in advance and branch out the search reset cleanly. Additionally, I've taken the liberty to replace the `IVector` in `SearchResultRows` with a direct `const std::vector&` into `Searcher`. This removes a bunch of code and makes it faster to boot. ## Validation Steps Performed * Print lots of text * Search a common letter * Scroll up * Doesn't scroll back down ✅ * Hold enter to search more occurrences scrolls up as needed ✅ * `showMarksOnScrollbar` still works ✅
This PR extends
til::throttled_func
to also support debouncing:Based on the latter the following series of changes were made:
OutputIdle
event was added toControlCore
which israised once there hasn't been any incoming data in 100ms.
This also triggers an update of our regex patterns (URL detection).
TermControl
which callsSearch()
.Search()
in turn was modified to return its results by-valueas 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 theTextBuffer
by-reference which avoids the need to accumulate theline renditions in a
std::vector
first. This improves Debug buildperformance during reflow by what I guess must be roughly
a magnitude faster. This difference is very noticeable.
ClearSearch()
is called to removethe highlights. The search text is restored when it's reopened,
however the current search position isn't.
Closes #17073
Closes #17089
Validation Steps Performed
recalculates the highlights ✅