-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Improve Search Highlighting #16611
Improve Search Highlighting #16611
Conversation
Ah, I see now... I believe the correct fix would actually be to move the Also, I just noticed I really need to finish documenting all the AtlasEngine code... Less important thoughts: In my opinion the place we put the terminal/src/tools/ConsoleMonitor/main.cpp Lines 101 to 150 in 0d47c86
I've put that idea on pause for now though, because I also want to replace |
@lhecker is this a request for a change, or a "i will sign off and we can fix it more later"? |
@DHowett I'm still working on the search highlight right now. I'll definitely have something by tomorrow. I'm planning to repurpose this PR for that. Just a note on what I'm working on: Planning to decouple our Selection logic from Search highlighting. You can expect it to be like "find on page" in browsers. Starting a new selection while search highlight is active won't clear the search highlight 🙂 @lhecker as you mentioned, the current approach won't work properly due to M:N character to glyph relation. I think we can use clusterMap for figuring out what range of glyphs belong to the text within |
Hmm... Is it really necessary to use the cluster map at all? Any modifications we make to the foreground color bitmap before |
I'm not sure how obvious this since I've authored the code. So just to make sure I'll explain it: The purpose of the color bitmaps (both are in |
…olors just before painting text to the screen
ca8a1e6
to
d03abaf
Compare
Fixed the scroll invalidation issue. The Automation peer based (I've updated the PR implementation details. Might be helpful for review) |
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.
Yea I'm cool with this. This is a PR that IMO is easiest to read from bottom-up.
One thing that might change here - I think currently you can open the find dialog, search for something, then hit esc and you've got that text selected. I don't think that'll work anymore, but I'm also not sure how niche that use case is.
{ | ||
LOG_IF_FAILED(pEngine->PaintSelections(std::move(dirtySearchRectangles))); |
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.
note to self: okay so this still works in conhost, in GDI, same as before, because GDI is still just using the selection rect from _GetSelectionRects
, and painting in the PaintSelection call on L1253
foundResults->TotalMatches(gsl::narrow<int32_t>(_searcher.Results().size())); | ||
foundResults->CurrentMatch(gsl::narrow<int32_t>(_searcher.CurrentMatch())); | ||
|
||
_terminal->AlwaysNotifyOnBufferRotation(true); |
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.
little sus that this is missing now... note to self, make sure that search marks still update when search box is open? ...
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 answered this to myself: it's down in TermControl::_coreUpdateSearchResults
.newViewportSize = scrollBar.ViewportSize(), | ||
}; | ||
_throttledUpdateScrollbar(update); | ||
_UpdateSearchScrollMarks(); |
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.
...... ah yea here it is. We always send the update if we got an explicit change to the search marks, which we will when the buffer is circling.
I apologize for the late review. I was sort of hung up in graphemes and some other work. To be honest, I have some concerns with this PR, primarily due to 2 reasons:
Here's why:
I'm trying to implement buffer snapshotting. Additionally, the My long term vision is to have this: // Just like AtlasEngine's Settings, and with the exact same idea, but cleaned up for general use.
// It's a snapshot of the Terminal's settings.
struct Settings {
til::generational<TargetSettings> target;
til::generational<FontSettings> font;
til::generational<CursorSettings> cursor;
til::generational<MiscellaneousSettings> misc;
til::size targetSize;
til::size viewportCellCount;
til::point viewportOffset;
};
// Just like AtlasEngine's RenderingPayload, also with the exact same idea.
// It's a snapshot of the TextBuffer contents and terminal settings.
struct RenderingPayload {
til::generational<Settings> s;
TextBuffer buffer;
std::vector<til::point_span> searchResults; // <---- your addition
};
struct IRenderData {
void Snapshot(RenderingPayload& payload) = 0;
} A terminal would then implement it as void Terminal::Snapshot(RenderingPayload& p)
{
if (p.s->targetSize != _windowSize) {
p.s.write()->targetSize = _windowSize;
}
if (_searcher.HasChangedSinceLastTime()) {
p.searchResults = _searcher.GetResults();
}
// Followed by a lot more settings update checks, which makes it very verbose. However, all those
// checks will ultimately be extremely cheap overall, much cheaper than virtual function calls.
// They'll also be easy to debug and easy to author exactly because they're so verbose.
// I suspect the introduction of a couple helper methods will make it reasonable though.
// Snapshot the buffer
if (p.buffer.Size() != viewportSize) {
p.buffer.Resize(viewportSize);
}
_activeBuffer.CopyTo(viewport, p.buffer);
} This PR however moves further away from this goal, due to the two new setters. I'd prefer if You can add the two If you need any help with this, please let me know. This is the only "blocker" I have for this PR. Anything else I'll say below is mostly a nitpick. (If you plan to address any of the nitpicks, I personally think the one about the dirty rects is the only important one. Unless I'm mistaking, I believe it would simplify your PR a lot.)
The transformation from Additionally, filtering currently happens in You could improve the performance a lot here by moving the transformation over into struct point_span
{
til::point start;
til::point end;
// Calls func(row, begX, endX) for each row and begX and begY are inclusive coordinates,
// because point_span itself also uses inclusive coordinates.
// In other words, it turns a
// +----------------+
// | #########|
// |################|
// |#### |
// +----------------+
// into
// func(0, 8, 15)
// func(1, 0, 15)
// func(2, 0, 4)
void iterate_rows(til::CoordType width, auto&& func) {
// Copy the members so that the compiler knows it doesn't
// need to re-read them on every loop iteration.
const auto w = width - 1;
const auto ax = std::clamp(start.x, 0, w);
const auto ay = start.y;
const auto bx = std::clamp(end.x, 0, w);
const auto by = end.y;
for (auto y = ay; y <= by; ++y) {
const auto x1 = y != ay ? 0 : ax;
const auto x2 = y != by ? w : bx;
func(y, x1, x2);
}
}
}; Then you can iterate through the highlights directly, offset the For this to work you need access to all line renditions, and so I would suggest changing More importantly, I haven't quite understood whether the dirty rect manipulation is strictly necessary, but I suspect it's not. I would suggest to take the void AtlasEngine::_drawColorBitmap(size_t index, size_t y, size_t x1, size_t x2, u32 color) noexcept
{
const auto shift = _api.lineRenditions[y] != LineRendition::SingleWidth ? 1 : 0;
const auto row = _p.colorBitmap.begin() + _p.colorBitmapDepthStride * index + _p.colorBitmapRowStride * y;
auto beg = row + (x1 << shift);
auto end = row + (x2 << shift);
for (auto it = beg; it != end; ++it)
{
if (*it != color)
{
_p.colorBitmapGenerations[index].bump();
_p.dirtyRectInPx.left = 0;
_p.dirtyRectInPx.right = _p.s->targetSize.x;
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, gsl::narrow_cast<i32>(y * _p.s->font->cellSize.y));
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, _p.dirtyRectInPx.top + _p.s->font->cellSize.y);
std::fill(it, end, color);
return;
}
}
} |
This comment has been minimized.
This comment has been minimized.
(As mentioned in the other PR, I'm currently hung up in finishing up a massive PR myself. I'll review your PR as soon as I'm done with that. Should be soon. 😣) |
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.
// 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); |
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.
hey @lhecker, wonder if we should put the pattern matcher update in here ;P
@@ -173,6 +180,10 @@ namespace Microsoft::Console::Render::Atlas | |||
// UpdateHyperlinkHoveredId() | |||
u16 hyperlinkHoveredId = 0; | |||
|
|||
// These tracks the highlighted regions on the screen that are yet to be painted. | |||
std::span<const til::point_span> searchHighlights; |
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.
what keeps these pointing to valid memory?
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.
My understanding was that these are only used to smuggle data between different IRenderEngine
API calls only within a single render pass (since the interface is stateful and AtlasEngine is more "snapshot-y").
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.
_api.searchHighlights
is always reset with the new value at the start of each render pass. The highlights are read and drawn all under the lock, and if I'm not wrong, we never reset highlights without holding a lock on Terminal.
I do think that we now need to be extra careful to always call ResetIfStale()
under the lock, otherwise it could corrupt the AtlasEngine _api.searchHighlights
. But, as @lhecker mentioned, in the future, the highlights will be stored as std::vector
instead of std::span
so there won't be any safety issues here.
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 think I should've at least reset the spans to be empty in EndPaint()
.
// have access to the entire line of text. | ||
// have access to the entire line of text, whereas TextBuffer writes it one | ||
// character at a time via the OutputCellIterator. | ||
_api.NotifyTextLayoutUpdated(); |
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 sounds expensive; is it?
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.
You're right, this is likely to be expensive, in particular in extreme cases like rainbowbench.
We could raise the event only once after processing an entire VT string to reduce the cost. One option would be to just do it as part of the updatePatternLocations
flow, because that way it would also get throttled.
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 could raise the event only once after processing an entire VT string to reduce the cost.
That makes sense. We don't render in between parsing so doing it at every write step vs at the end of the entire vt string is equivalent.
I'm also planning to introduce throttling in this codepath, but I've a few queries like currently typing acts like a way for user to dismiss highlights from the screen. Once we introduce throttling this won't happen anymore. Instead typing into the terminal will dispatch a delayed refresh of search highlights which will bring back the highlights after a short delay. So, I'm not sure how should a user dismiss highlights in this case.
Any suggestions?
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.
Hmm, I may be misunderstanding you, but what you want is to simply remove the search highlights when someone types, right? That's basically just like TrySnapOnInput
- if you search it in the project, there should only be a few hits. That function is what makes sure you're scrolled all the way down when you type, and we could just extend that, or add code near it, to also dismiss the highlights.
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.
what you want is to simply remove the search highlights when someone types, right?
Huh.. quite the opposite 😅 Highlights shouldn't be removed if search box is open. So, unless the searchbox is closed, we should keep the results fresh and active.
To be honest, I don't know what is a good UX here 😄
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, now I got it. 🙂
In a perfect world we'd probably reflow the highlights while reflowing the text during a resize, but I don't think that's worth the effort at the moment. I think putting it into the throttled updatePatternLocations
callback is probably the right choice for now as it presents a decent trade-off between correctness and complexity.
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.
FYI I just started making the necessary changes. 😊
I think this subtly regressed in #16611. Jump to 90b8bb7#diff-f9112caf8cb75e7a48a7b84987724d754181227385fbfcc2cc09a879b1f97c12L171-L223 `Terminal::SelectNewRegion` is the only thing that uses the return value from `Terminal::_ScrollToPoints`. Before that PR, `_ScrollToPoints` was just a part of `SelectNewRegion`, and it moved the start & end coords by the `_VisibleStartIndex`, not the `_scrollOffset`. Kinda weird there weren't any _other_ tests for `SelectNewRegion`? I also caught a second bug while I was here - If you had a line with an exact wrap, and tried to select that like with selectOutput, we'd explode. Closes #17131
The changeset involves:
Closes: #16355
Some Implementation Details
Detecting text layout changes in the Control layer
As Search Highlight regions need to be removed when new text is added, or the existing text is re-arranged due to window resize or similar events, a new event
TextLayoutUpdated
is added that notifiesCoreControl
of any text layout changes. The event is used to invalidate and remove all search highlight regions from the buffer (because the regions might not be fresh anymore.The new event is raised when:
AdaptDispatch
writes new text into the buffer.(Intensionally,) It's not raised when:
When
ControlCore
receives aTextLayoutUpdated
event, it clears the Search Highlights in the render data, and raises anUpdateSearchResults
event to notifyTermControl
to update the Search UI (SearchBoxControl
).In the future, we can use
TextLayoutUpdated
event to start a new search which would refresh the results automatically after a slight delay (throttled). VSCode already does this today.How does AtlasEngine draw the highlighted regions?
We follow a similar idea as for drawing the Selection region. When new regions are available, the old+new regions are marked invalidated. Later, a call to
_drawHighlighted()
is made at the end ofPaintBufferLine()
to override the highlighted regions' colors with highlight colors. The highlighting colors replace the buffer colors while search highlights are active.Note that to paint search highlights, we currently invalidate the row completely. This forces text shaping for the rows in the viewport that have at least one highlighted region. This is done to keep the (already lengthy) PR... simple. We could take advantage of the fact that only colors have changed and not the characters (or glyphs). I'm expecting that this could be improved like:
Validation Steps: