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

Prevent the virtual viewport bottom being updated incorrectly #12972

Merged
9 commits merged into from
May 2, 2022
17 changes: 9 additions & 8 deletions src/host/getset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -688,8 +688,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });

auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();

auto& buffer = context.GetActiveBuffer();

const auto coordScreenBufferSize = buffer.GetBufferSize().Dimensions();
Expand All @@ -707,12 +705,9 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
// Attempt to "snap" the viewport to the cursor position. If the cursor
// is not in the current viewport, we'll try and move the viewport so
// that the cursor is visible.
// microsoft/terminal#1222 - Use the "virtual" viewport here, so that
// when the console is in terminal-scrolling mode, the viewport snaps
// back to the virtual viewport's location.
const auto currentViewport = gci.IsTerminalScrolling() ?
buffer.GetVirtualViewport().ToInclusive() :
buffer.GetViewport().ToInclusive();
// GH#1222 and GH#9754 - Use the "virtual" viewport here, so that the
// viewport snaps back to the virtual viewport's location.
const auto currentViewport = buffer.GetVirtualViewport().ToInclusive();
COORD delta{ 0 };
{
// When evaluating the X offset, we must convert the buffer position to
Expand Down Expand Up @@ -746,6 +741,12 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
// buffer for us.
RETURN_IF_NTSTATUS_FAILED(buffer.SetViewportOrigin(true, newWindowOrigin, true));

// SetViewportOrigin will only move the virtual bottom down, but in
// this particular case we also need to allow the virtual bottom to
// be moved up, so we have to call UpdateBottom explicitly. This is
// how the cmd shell's CLS command resets the buffer.
buffer.UpdateBottom();

return S_OK;
}
CATCH_RETURN();
Expand Down
14 changes: 12 additions & 2 deletions src/host/outputStream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ bool ConhostInternalGetSet::ResizeWindow(const size_t width, const size_t height
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
api->GetConsoleScreenBufferInfoExImpl(screenInfo, csbiex);

const auto oldViewport = Viewport::FromInclusive(csbiex.srWindow);
const auto newViewport = Viewport::FromDimensions(oldViewport.Origin(), sColumns, sRows);
const auto oldViewport = screenInfo.GetVirtualViewport();
auto newViewport = Viewport::FromDimensions(oldViewport.Origin(), sColumns, sRows);
// Always resize the width of the console
csbiex.dwSize.X = sColumns;
// Only set the screen buffer's height if it's currently less than
Expand All @@ -315,6 +315,16 @@ bool ConhostInternalGetSet::ResizeWindow(const size_t width, const size_t height
csbiex.dwSize.Y = sRows;
}

// If the cursor row is now past the bottom of the viewport, we'll have to
// move the viewport down to bring it back into view. However, we don't want
// to do this in pty mode, because the conpty resize operation is dependent
// on the viewport *not* being adjusted.
const short cursorOverflow = csbiex.dwCursorPosition.Y - newViewport.BottomInclusive();
if (cursorOverflow > 0 && !IsConsolePty())
{
newViewport = Viewport::Offset(newViewport, { 0, cursorOverflow });
}

// SetWindowInfo expect inclusive rects
const auto sri = newViewport.ToInclusive();

Expand Down
49 changes: 30 additions & 19 deletions src/host/screenInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,10 @@ void SCREEN_INFORMATION::SetViewportSize(const COORD* const pcoordSize)
// Update our internal virtual bottom tracker if requested. This helps keep
// the viewport's logical position consistent from the perspective of a
// VT client application, even if the user scrolls the viewport with the mouse.
if (updateBottom)
// We typically only want to this to move the virtual bottom down, though,
// otherwise it can end up "truncating" the buffer if the user is viewing
// the scrollback at the time the viewport origin is updated.
if (updateBottom && _virtualBottom < _viewport.BottomInclusive())
{
UpdateBottom();
}
Expand Down Expand Up @@ -1239,23 +1242,21 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const COORD* const pcoordSize,
srNewViewport.Top = std::max<SHORT>(0, srNewViewport.Top - offBottomDelta);
}

// See MSFT:19917443
// If we're in terminal scrolling mode, and we've changed the height of the
// viewport, the new viewport's bottom to the _virtualBottom.
// GH#1206 - Only do this if the viewport is _growing_ in height. This can
// cause unexpected behavior if we try to anchor the _virtualBottom to a
// position that will be greater than the height of the buffer.
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto newViewport = Viewport::FromInclusive(srNewViewport);
if (gci.IsTerminalScrolling() && newViewport.Height() >= _viewport.Height())
// In general we want to avoid moving the virtual bottom unless it's aligned with
// the visible viewport, so we check whether the changes we're making would cause
// the bottom of the visible viewport to intersect the virtual bottom at any point.
// If so, we update the virtual bottom to match. We also update the virtual bottom
// if it's less than the new viewport height minus 1, because that would otherwise
// leave the virtual viewport extended past the top of the buffer.
const auto newViewport = Viewport::FromInclusive(srNewViewport);
if ((_virtualBottom >= _viewport.BottomInclusive() && _virtualBottom < newViewport.BottomInclusive()) ||
(_virtualBottom <= _viewport.BottomInclusive() && _virtualBottom > newViewport.BottomInclusive()) ||
lhecker marked this conversation as resolved.
Show resolved Hide resolved
_virtualBottom < newViewport.Height() - 1)
{
const auto newTop = static_cast<short>(std::max(0, _virtualBottom - (newViewport.Height() - 1)));

newViewport = Viewport::FromDimensions(COORD({ newViewport.Left(), newTop }), newViewport.Dimensions());
_virtualBottom = srNewViewport.Bottom;
}

_viewport = newViewport;
UpdateBottom();
Tracing::s_TraceWindowViewport(_viewport);

// In Conpty mode, call TriggerScroll here without params. By not providing
Expand All @@ -1267,6 +1268,7 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const COORD* const pcoordSize,
// till the start of the next frame. If any other text gets output before
// that frame starts, there's a very real chance that it'll cause errors as
// the engine tries to invalidate those regions.
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
if (gci.IsInVtIoMode() && ServiceLocator::LocateGlobals().pRender)
{
ServiceLocator::LocateGlobals().pRender->TriggerScroll();
Expand Down Expand Up @@ -1462,12 +1464,17 @@ bool SCREEN_INFORMATION::IsMaximizedY() const

if (SUCCEEDED(hr))
{
auto& newCursor = newTextBuffer->GetCursor();
// Make sure the new virtual bottom is far enough down to include both
// the cursor row and the last non-space row.
const auto cursorRow = newTextBuffer->GetCursor().GetPosition().Y;
const auto lastNonSpaceRow = newTextBuffer->GetLastNonSpaceCharacter().Y;
Copy link
Member

Choose a reason for hiding this comment

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

GetLastNonSpaceCharacter is very very slow if the text buffer is mostly empty.
Will this be a problem? Is there some other way we can extract this information?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah. I would have thought the TextBuffer::Reflow should have been able to provide that information for us, since it's just created that new buffer. But then that made me ask why the TextBuffer isn't tracking the virtual bottom in general? And why we've rewritten essentially the same resizing code in the Terminal class but in a completely different way? (Although it's worth noting that the Terminal implementation also uses GetLastNonSpaceCharacter.) And my conclusion was that all of this buffer management code really needs a unified rewrite, but that was way more effort than I wanted to get into now, if ever. Hence the quick fix hack.

Copy link
Member

Choose a reason for hiding this comment

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

That's mostly on me. SCREEN_INFO is pretty tightly bound to the rest of the console itself. It was a lot harder to separate out from... everything else that makes the console the console. I was trying to create a version with a cleaner interface in Terminal.

In retrospect, yes, a unified implementation would have been smart. I think I would have liked to create a cleaner boundary around SCREEN_INFO so that terminal could have reused it too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I realise it won't be easy decoupling that code from the console, but I think it'll be worth revisiting at some point, possibly when/if we get around to implementing VT paging. Because ideally I'd like for us to have a unified buffer management class that can handle both the console buffers and the VT pages in the same way (along with XTerm's alt/main weirdness). And that class could hopefully then be shared between conhost and Terminal and reduce some of the duplication. But it's not urgent.

_virtualBottom = std::max({ cursorRow, lastNonSpaceRow });

// Adjust the viewport so the cursor doesn't wildly fly off up or down.
const auto sCursorHeightInViewportAfter = newCursor.GetPosition().Y - _viewport.Top();
const auto sCursorHeightInViewportAfter = cursorRow - _viewport.Top();
COORD coordCursorHeightDiff = { 0 };
coordCursorHeightDiff.Y = gsl::narrow_cast<short>(sCursorHeightInViewportAfter - sCursorHeightInViewportBefore);
LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, true));
LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, false));

newTextBuffer->SetCurrentAttributes(oldPrimaryAttributes);

Expand Down Expand Up @@ -1749,7 +1756,7 @@ void SCREEN_INFORMATION::SetCursorDBMode(const bool DoubleCursor)
return STATUS_SUCCESS;
}

void SCREEN_INFORMATION::MakeCursorVisible(const COORD CursorPosition, const bool updateBottom)
void SCREEN_INFORMATION::MakeCursorVisible(const COORD CursorPosition)
{
COORD WindowOrigin;

Expand Down Expand Up @@ -1781,7 +1788,7 @@ void SCREEN_INFORMATION::MakeCursorVisible(const COORD CursorPosition, const boo

if (WindowOrigin.X != 0 || WindowOrigin.Y != 0)
{
LOG_IF_FAILED(SetViewportOrigin(false, WindowOrigin, updateBottom));
LOG_IF_FAILED(SetViewportOrigin(false, WindowOrigin, false));
}
}

Expand Down Expand Up @@ -2308,6 +2315,10 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport,
const COORD coordNewOrigin = { 0, sNewTop };
RETURN_IF_FAILED(SetViewportOrigin(true, coordNewOrigin, true));

// SetViewportOrigin will only move the virtual bottom down, but in this
// case we need to reset it to the top, so we have to update it explicitly.
UpdateBottom();

// Place the cursor at the same x coord, on the row that's now the top
RETURN_IF_FAILED(SetCursorPosition(COORD{ oldCursorPos.X, sNewTop }, false));

Expand Down
2 changes: 1 addition & 1 deletion src/host/screenInfo.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console
void SetCursorDBMode(const bool DoubleCursor);
[[nodiscard]] NTSTATUS SetCursorPosition(const COORD Position, const bool TurnOn);

void MakeCursorVisible(const COORD CursorPosition, const bool updateBottom = true);
void MakeCursorVisible(const COORD CursorPosition);

Microsoft::Console::Types::Viewport GetRelativeScrollMargins() const;
Microsoft::Console::Types::Viewport GetAbsoluteScrollMargins() const;
Expand Down
6 changes: 3 additions & 3 deletions src/host/selection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ void Selection::ExtendSelection(_In_ COORD coordBufferPos)
}

// scroll if necessary to make cursor visible.
screenInfo.MakeCursorVisible(coordBufferPos, false);
screenInfo.MakeCursorVisible(coordBufferPos);

_dwSelectionFlags |= CONSOLE_SELECTION_NOT_EMPTY;
_srSelectionRect.Left = _srSelectionRect.Right = _coordSelectionAnchor.X;
Expand All @@ -224,7 +224,7 @@ void Selection::ExtendSelection(_In_ COORD coordBufferPos)
else
{
// scroll if necessary to make cursor visible.
screenInfo.MakeCursorVisible(coordBufferPos, false);
screenInfo.MakeCursorVisible(coordBufferPos);
}

// remember previous selection rect
Expand Down Expand Up @@ -609,5 +609,5 @@ void Selection::SelectAll()
SelectNewRegion(coordNewSelStart, coordNewSelEnd);

// restore the old window position
LOG_IF_FAILED(screenInfo.SetViewportOrigin(true, coordWindowOrigin, true));
LOG_IF_FAILED(screenInfo.SetViewportOrigin(true, coordWindowOrigin, false));
}
2 changes: 1 addition & 1 deletion src/host/selectionInput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe

cursor.SetHasMoved(true);
_coordSelectionAnchor = textBuffer.GetCursor().GetPosition();
ScreenInfo.MakeCursorVisible(_coordSelectionAnchor, false);
ScreenInfo.MakeCursorVisible(_coordSelectionAnchor);
_srSelectionRect.Left = _srSelectionRect.Right = _coordSelectionAnchor.X;
_srSelectionRect.Top = _srSelectionRect.Bottom = _coordSelectionAnchor.Y;
}
Expand Down
Loading