terminal/src/cascadia/TerminalCore/terminalrenderdata.cpp
Carlos Zamora ee6060b3a4
Fix search not scrolling to result past view (#19571)
## Summary of the Pull Request
Fixes a bug where search would not scroll to results just below the
viewport.

This was caused by code intended to scroll the search result in such a
way that it isn't covered by the search box. The scroll offset is
calculated in `TermControl::_calculateSearchScrollOffset()` then handed
down in the `SearchRequest` when conducting a search. This would get to
`Terminal::ScrollToSearchHighlight()` where the offset is applied to the
search result's position so that we would scroll to the adjusted
position.

The adjustment was overly aggressive in that it would apply it to both
"start" and "end". In reality, we don't need to apply it to "end"
because it wouldn't be covered by the search box (we only scroll to end
if it's past the end of the current view anyways).

The fix applies the adjustment only to "start" and only does so if it's
actually in the first few rows that would be covered by the search box.

That unveiled another bug where `Terminal::_ScrollToPoints()` would also
be too aggressive about scrolling the "end" into view. In some testing,
it would generally end up scrolling to the end of the buffer. To fix
this cascading bug, I just had `_ScrollToPoints()` just call
`Terminal::_ScrollToPoint()` (singular, not plural) which is
consistently used throughout the Terminal code for selection (so it's
battle tested).

`_ScrollToPoints()` was kept since it's still used for accessibility
when selecting a new region to keep the new selection in view. It's also
just a nice wrapper that ensures a range is visible (or at least as much
as it could be).

## References and Relevant Issues
Scroll offset was added in #17516

## Validation Steps Performed
 search results that would be covered by the search box are still
adjusted
 search results that are past the end of the view become visible
 UIA still selects properly and brings the selection into view

## PR Checklist
Duncan reported this bug internally, but there doesn't seem to be one on
the repo.
2025-11-24 09:35:52 -08:00

220 lines
6.4 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "Terminal.hpp"
using namespace Microsoft::Terminal::Core;
using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::Render;
Viewport Terminal::GetViewport() noexcept
{
return _GetVisibleViewport();
}
til::point Terminal::GetTextBufferEndPosition() const noexcept
{
// We use the end line of mutableViewport as the end
// of the text buffer, it always moves with the written
// text
return { _GetMutableViewport().Width() - 1, ViewEndIndex() };
}
TextBuffer& Terminal::GetTextBuffer() const noexcept
{
return _activeBuffer();
}
const FontInfo& Terminal::GetFontInfo() const noexcept
{
_assertLocked();
return _fontInfo;
}
void Terminal::SetFontInfo(const FontInfo& fontInfo)
{
_assertLocked();
_fontInfo = fontInfo;
}
TimerDuration Terminal::GetBlinkInterval() noexcept
{
if (!_cursorBlinkInterval)
{
const auto enabled = GetSystemMetrics(SM_CARETBLINKINGENABLED);
const auto interval = GetCaretBlinkTime();
// >10s --> no blinking. The limit is arbitrary, because technically the valid range
// on Windows is 200-1200ms. GetCaretBlinkTime() returns INFINITE for no blinking, 0 for errors.
_cursorBlinkInterval = enabled && interval <= 10000 ? std ::chrono::milliseconds(interval) : TimerDuration::max();
}
return *_cursorBlinkInterval;
}
ULONG Terminal::GetCursorPixelWidth() const noexcept
{
return 1;
}
bool Terminal::IsGridLineDrawingAllowed() noexcept
{
return true;
}
std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkUri(uint16_t id) const
{
return _activeBuffer().GetHyperlinkUriFromId(id);
}
std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkCustomId(uint16_t id) const
{
return _activeBuffer().GetCustomIdFromId(id);
}
// Method Description:
// - Gets the regex pattern ids of a location
// Arguments:
// - The location
// Return value:
// - The pattern IDs of the location
std::vector<size_t> Terminal::GetPatternId(const til::point location) const
{
_assertLocked();
// Look through our interval tree for this location
const auto intervals = _patternIntervalTree.findOverlapping({ location.x + 1, location.y }, location);
if (intervals.size() == 0)
{
return {};
}
else
{
std::vector<size_t> result{};
for (const auto& interval : intervals)
{
result.emplace_back(interval.value);
}
return result;
}
return {};
}
std::pair<COLORREF, COLORREF> Terminal::GetAttributeColors(const TextAttribute& attr) const noexcept
{
return GetRenderSettings().GetAttributeColors(attr);
}
std::span<const til::point_span> Terminal::GetSelectionSpans() const noexcept
try
{
if (_selection.generation() != _lastSelectionGeneration)
{
_lastSelectionSpans = _GetSelectionSpans();
_lastSelectionGeneration = _selection.generation();
}
return _lastSelectionSpans;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return {};
}
// Method Description:
// - Helper to determine the search highlights in the buffer. Used for rendering.
// Return Value:
// - A vector of rectangles representing the regions to select, line by line. They are absolute coordinates relative to the buffer origin.
std::span<const til::point_span> Terminal::GetSearchHighlights() const noexcept
{
_assertLocked();
return _searchHighlights;
}
const til::point_span* Terminal::GetSearchHighlightFocused() const noexcept
{
_assertLocked();
if (_searchHighlightFocused < _searchHighlights.size())
{
return &til::at(_searchHighlights, _searchHighlightFocused);
}
return nullptr;
}
// Method Description:
// - If necessary, scrolls the viewport such that the start point is in the
// viewport, and if that's already the case, also brings the end point inside
// the viewport
// Arguments:
// - coordStart - The start point
// - coordEnd - The end point
// Return Value:
// - The updated scroll offset
til::CoordType Terminal::_ScrollToPoints(const til::point coordStart, const til::point coordEnd)
{
if (coordStart.y < _VisibleStartIndex())
{
_ScrollToPoint(coordStart);
}
else if (coordEnd.y > _VisibleEndIndex())
{
_ScrollToPoint(coordEnd);
}
return _VisibleStartIndex();
}
// Method Description:
// - selects the region from coordStart to coordEnd
// Arguments:
// - coordStart - The start point (inclusive)
// - coordEnd - The end point (inclusive)
void Terminal::SelectNewRegion(const til::point coordStart, const til::point coordEnd)
{
const auto newScrollOffset = _ScrollToPoints(coordStart, coordEnd);
// update the selection coordinates so they're relative to the new scroll-offset
const auto newCoordStart = til::point{ coordStart.x, coordStart.y - newScrollOffset };
const auto newCoordEnd = til::point{ coordEnd.x, coordEnd.y - newScrollOffset };
SetSelectionAnchor(newCoordStart);
SetSelectionEnd(newCoordEnd, SelectionExpansion::Char);
_activeBuffer().TriggerSelection();
}
std::wstring_view Terminal::GetConsoleTitle() const noexcept
{
_assertLocked();
if (_title.has_value())
{
return *_title;
}
return _startingTitle;
}
// Method Description:
// - Lock the terminal for reading the contents of the buffer. Ensures that the
// contents of the terminal won't be changed in the middle of a paint
// operation.
// Callers should make sure to also call Terminal::UnlockConsole once
// they're done with any querying they need to do.
void Terminal::LockConsole() noexcept
{
_readWriteLock.lock();
}
// Method Description:
// - Unlocks the terminal after a call to Terminal::LockConsole.
void Terminal::UnlockConsole() noexcept
{
_readWriteLock.unlock();
}
bool Terminal::IsUiaDataInitialized() const noexcept
{
// GH#11135: Windows Terminal needs to create and return an automation peer
// when a screen reader requests it. However, the terminal might not be fully
// initialized yet. So we use this to check if any crucial components of
// UiaData are not yet initialized.
_assertLocked();
return !!_mainBuffer;
}