Skip to content
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

Merged
merged 6 commits into from
Apr 23, 2024
Merged

Conversation

lhecker
Copy link
Member

@lhecker lhecker commented Apr 20, 2024

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 ✅

@@ -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 &&
Copy link
Member Author

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);
}
});
Copy link
Member Author

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);
Copy link
Member Author

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.

Copy link
Contributor

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 🤕🥲

Copy link
Member Author

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)
Copy link
Member Author

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.

Comment on lines +1730 to +1734
_terminal->SetSearchHighlights({});
_terminal->SetSearchHighlightFocused({});
_renderer->TriggerSearchHighlight(_searcher.Results());
_searcher = {};
_cachedSearchResultRows = {};
Copy link
Member Author

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);
Copy link
Member Author

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)
Copy link
Member Author

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
Copy link
Member Author

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.

@lhecker
Copy link
Member Author

lhecker commented Apr 20, 2024

@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 ResetIfStale really isn't a good API, isn't it? I refrained from changing any of that in this PR, however.

@tusharsnx
Copy link
Contributor

tusharsnx commented Apr 21, 2024

I hope these are fine.

I'm ABSOLUTELY fine happy that this PR fixed the remaining issues. I can see how it simplifies the whole adapt-dispatcher ➡️ Terminal ➡️ TermControl interop thing. This is how my PR should've done it already. Thanks for doing this 😄

BTW ResetIfStale really isn't a good API, isn't it?

Its idempotency is questionable 😅


OK, now on to this PR:

  1. The Focused highlight seems to force a scroll towards it. If I scroll toward the top, it scrolls the buffer back to where the focused highlight is.
  2. The renderer doesn't seem to re-render the focused highlight region correctly. (I also added a review comment about it). For testing, you can try searching for a word/letter that appears more than once within the viewport.

.newViewportSize = scrollBar.ViewportSize(),
};
_throttledUpdateScrollbar(update);
if (!_searchBox || _searchBox->Visibility() != Visibility::Visible)
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened the search box, and typed "tusha" into it, and then closed it. Search highlights were removed as expected. Used Find next search match, and the highlights are active again. Entered some text containing "tusha". Highlight didn't refresh. After running clear command:

image

Copy link
Member Author

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!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Did we fix this?)

Copy link
Member Author

@lhecker lhecker Apr 23, 2024

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)
Copy link
Contributor

@tusharsnx tusharsnx Apr 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same query as above.

Copy link
Contributor

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.

@zadjii-msft
Copy link
Member

FWIW this does seem to fix #17089

@DHowett DHowett enabled auto-merge April 23, 2024 20:23
Copy link
Member

@zadjii-msft zadjii-msft left a 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

@DHowett DHowett added this pull request to the merge queue Apr 23, 2024
Merged via the queue into main with commit 360e86b Apr 23, 2024
20 checks passed
@DHowett DHowett deleted the dev/lhecker/17073-search-highlights branch April 23, 2024 22:59
Copy link
Member

@zadjii-msft zadjii-msft left a 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)
Copy link
Member

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 the throttled_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 the throttled_func in that timeout, they'll further delay the callback.

@tusharsnx
Copy link
Contributor

Sorry for the late report, but it seems like this is not yet fixed:

The Focused highlight seems to force a scroll towards it. If I scroll toward the top, it scrolls the buffer back to where the focused highlight is.

@lhecker
Copy link
Member Author

lhecker commented Apr 24, 2024

Ah fuck I forgot about that comment. Sorry about that! 😣
I'll try to investigate soon.

github-merge-queue bot pushed a commit that referenced this pull request Apr 30, 2024
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 ✅
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[1.21.1091 Canary] Terminal crashes on launch Search highlights are gone if the window is resized
4 participants