diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 55732eeb71a..17ff3f3978d 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -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(); @@ -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 @@ -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(); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 9a7e9185e5b..03ad7642159 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -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 @@ -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(); diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index e490d3cd287..2c5f2f4c9b3 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -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(); } @@ -1239,23 +1242,21 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const COORD* const pcoordSize, srNewViewport.Top = std::max(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()) || + _virtualBottom < newViewport.Height() - 1) { - const auto newTop = static_cast(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 @@ -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(); @@ -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; + _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(sCursorHeightInViewportAfter - sCursorHeightInViewportBefore); - LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, true)); + LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, false)); newTextBuffer->SetCurrentAttributes(oldPrimaryAttributes); @@ -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; @@ -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)); } } @@ -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)); diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index cb1b7f7dad3..0042c55b530 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -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; diff --git a/src/host/selection.cpp b/src/host/selection.cpp index 4706c20c3a0..70ec0336f46 100644 --- a/src/host/selection.cpp +++ b/src/host/selection.cpp @@ -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; @@ -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 @@ -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)); } diff --git a/src/host/selectionInput.cpp b/src/host/selectionInput.cpp index d178b33b0bd..490718d7255 100644 --- a/src/host/selectionInput.cpp +++ b/src/host/selectionInput.cpp @@ -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; } diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 311470e6c62..030e8f46d8a 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -80,6 +80,8 @@ class ScreenBufferTests auto defaultSize = COORD{ CommonState::s_csWindowWidth, CommonState::s_csWindowHeight }; currentBuffer.SetViewport(Viewport::FromDimensions(defaultSize), true); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), currentBuffer.GetTextBuffer().GetCursor().GetPosition()); + // Make sure the virtual bottom is correctly positioned. + currentBuffer.UpdateBottom(); return true; } @@ -225,6 +227,12 @@ class ScreenBufferTests TEST_METHOD(TestAddHyperlinkCustomIdDifferentUri); TEST_METHOD(UpdateVirtualBottomWhenCursorMovesBelowIt); + TEST_METHOD(UpdateVirtualBottomWithSetConsoleCursorPosition); + TEST_METHOD(UpdateVirtualBottomAfterInternalSetViewportSize); + TEST_METHOD(UpdateVirtualBottomAfterResizeWithReflow); + TEST_METHOD(DontChangeVirtualBottomWithOffscreenLinefeed); + TEST_METHOD(DontChangeVirtualBottomAfterResizeWindow); + TEST_METHOD(DontChangeVirtualBottomWithMakeCursorVisible); TEST_METHOD(RetainHorizontalOffsetWhenMovingToBottom); TEST_METHOD(TestWriteConsoleVTQuirkMode); @@ -6133,6 +6141,272 @@ void ScreenBufferTests::UpdateVirtualBottomWhenCursorMovesBelowIt() VERIFY_ARE_EQUAL(newVirtualBottom, si.GetViewport().BottomInclusive()); } +void ScreenBufferTests::UpdateVirtualBottomWithSetConsoleCursorPosition() +{ + auto& g = ServiceLocator::LocateGlobals(); + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& cursor = si.GetTextBuffer().GetCursor(); + + Log::Comment(L"Pan down so the initial viewport is a couple of pages down"); + const auto initialOrigin = COORD{ 0, si.GetViewport().Height() * 2 }; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, initialOrigin, true)); + VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); + + Log::Comment(L"Make sure the initial virtual bottom is at the bottom of the viewport"); + const auto initialVirtualBottom = si.GetViewport().BottomInclusive(); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Pan to the top of the buffer without changing the virtual bottom"); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Set the cursor position to the initial origin"); + VERIFY_SUCCEEDED(g.api->SetConsoleCursorPositionImpl(si, initialOrigin)); + VERIFY_ARE_EQUAL(initialOrigin, cursor.GetPosition()); + + Log::Comment(L"Confirm that the viewport has moved down"); + VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); + + Log::Comment(L"Confirm that the virtual bottom has not changed"); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Pan further down so the viewport is below the cursor"); + const auto belowCursor = COORD{ 0, cursor.GetPosition().Y + 10 }; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, belowCursor, false)); + VERIFY_ARE_EQUAL(belowCursor, si.GetViewport().Origin()); + + Log::Comment(L"Set the cursor position one line down, still inside the virtual viewport"); + const auto oneLineDown = COORD{ 0, cursor.GetPosition().Y + 1 }; + VERIFY_SUCCEEDED(g.api->SetConsoleCursorPositionImpl(si, oneLineDown)); + VERIFY_ARE_EQUAL(oneLineDown, cursor.GetPosition()); + + Log::Comment(L"Confirm that the viewport has moved back to the initial origin"); + VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); + + Log::Comment(L"Confirm that the virtual bottom has not changed"); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Set the cursor position to the top of the buffer"); + const auto topOfBuffer = COORD{ 0, 0 }; + VERIFY_SUCCEEDED(g.api->SetConsoleCursorPositionImpl(si, topOfBuffer)); + VERIFY_ARE_EQUAL(topOfBuffer, cursor.GetPosition()); + + Log::Comment(L"Confirm that the viewport has moved to the top of the buffer"); + VERIFY_ARE_EQUAL(topOfBuffer, si.GetViewport().Origin()); + + Log::Comment(L"Confirm that the virtual bottom has also moved up"); + VERIFY_ARE_EQUAL(si.GetViewport().BottomInclusive(), si._virtualBottom); +} + +void ScreenBufferTests::UpdateVirtualBottomAfterInternalSetViewportSize() +{ + auto& g = ServiceLocator::LocateGlobals(); + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& cursor = si.GetTextBuffer().GetCursor(); + auto& stateMachine = si.GetStateMachine(); + + Log::Comment(L"Pan down so the initial viewport is a couple of pages down"); + const auto initialOrigin = COORD{ 0, si.GetViewport().Height() * 2 }; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, initialOrigin, true)); + VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); + + Log::Comment(L"Make sure the initial virtual bottom is at the bottom of the viewport"); + const auto initialVirtualBottom = si.GetViewport().BottomInclusive(); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Set the cursor to the bottom of the current page"); + stateMachine.ProcessString(L"\033[9999H"); + VERIFY_ARE_EQUAL(initialVirtualBottom, cursor.GetPosition().Y); + + Log::Comment(L"Pan to the top of the buffer without changing the virtual bottom"); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Shrink the viewport height by two lines"); + auto viewportSize = si.GetViewport().Dimensions(); + viewportSize.Y -= 2; + si._InternalSetViewportSize(&viewportSize, false, false); + VERIFY_ARE_EQUAL(viewportSize, si.GetViewport().Dimensions()); + + Log::Comment(L"Confirm that the virtual bottom has not changed"); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Position the viewport just above the virtual bottom"); + short viewportTop = si._virtualBottom - viewportSize.Y; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, viewportTop }, false)); + VERIFY_ARE_EQUAL(si._virtualBottom - 1, si.GetViewport().BottomInclusive()); + + Log::Comment(L"Expand the viewport height so it 'passes through' the virtual bottom"); + viewportSize.Y += 2; + si._InternalSetViewportSize(&viewportSize, false, false); + VERIFY_ARE_EQUAL(viewportSize, si.GetViewport().Dimensions()); + + Log::Comment(L"Confirm that the virtual bottom has aligned with the viewport bottom"); + VERIFY_ARE_EQUAL(si._virtualBottom, si.GetViewport().BottomInclusive()); + + Log::Comment(L"Position the viewport bottom just below the virtual bottom"); + viewportTop = si._virtualBottom - viewportSize.Y + 2; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, viewportTop }, false)); + VERIFY_ARE_EQUAL(si._virtualBottom + 1, si.GetViewport().BottomInclusive()); + + Log::Comment(L"Shrink the viewport height so it 'passes through' the virtual bottom"); + viewportSize.Y -= 2; + si._InternalSetViewportSize(&viewportSize, false, false); + VERIFY_ARE_EQUAL(viewportSize, si.GetViewport().Dimensions()); + + Log::Comment(L"Confirm that the virtual bottom has aligned with the viewport bottom"); + VERIFY_ARE_EQUAL(si._virtualBottom, si.GetViewport().BottomInclusive()); +} + +void ScreenBufferTests::UpdateVirtualBottomAfterResizeWithReflow() +{ + auto& g = ServiceLocator::LocateGlobals(); + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& cursor = si.GetTextBuffer().GetCursor(); + auto& stateMachine = si.GetStateMachine(); + + Log::Comment(L"Output a couple of pages of content"); + auto bufferSize = si.GetTextBuffer().GetSize().Dimensions(); + const auto viewportSize = si.GetViewport().Dimensions(); + const auto line = std::wstring(bufferSize.X - 1, L'X') + L'\n'; + for (auto i = 0; i < viewportSize.Y * 2; i++) + { + stateMachine.ProcessString(line); + } + + Log::Comment(L"Set the cursor to the top of the current page"); + stateMachine.ProcessString(L"\033[H"); + VERIFY_ARE_EQUAL(si.GetViewport().Origin(), cursor.GetPosition()); + + Log::Comment(L"Pan to the top of the buffer without changing the virtual bottom"); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); + + Log::Comment(L"Shrink the viewport width by a half"); + bufferSize.X /= 2; + VERIFY_NT_SUCCESS(si.ResizeWithReflow(bufferSize)); + + Log::Comment(L"Confirm that the virtual viewport includes the last non-space row"); + const auto lastNonSpaceRow = si.GetTextBuffer().GetLastNonSpaceCharacter().Y; + VERIFY_IS_GREATER_THAN_OR_EQUAL(si._virtualBottom, lastNonSpaceRow); +} + +void ScreenBufferTests::DontChangeVirtualBottomWithOffscreenLinefeed() +{ + auto& g = ServiceLocator::LocateGlobals(); + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& cursor = si.GetTextBuffer().GetCursor(); + auto& stateMachine = si.GetStateMachine(); + + Log::Comment(L"Pan down so the initial viewport is a couple of pages down"); + const auto initialOrigin = COORD{ 0, si.GetViewport().Height() * 2 }; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, initialOrigin, true)); + VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); + + Log::Comment(L"Make sure the initial virtual bottom is at the bottom of the viewport"); + const auto initialVirtualBottom = si.GetViewport().BottomInclusive(); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Set the cursor to the top of the current page"); + stateMachine.ProcessString(L"\033[H"); + VERIFY_ARE_EQUAL(initialOrigin, cursor.GetPosition()); + + Log::Comment(L"Pan to the top of the buffer without changing the virtual bottom"); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Output a line feed"); + stateMachine.ProcessString(L"\n"); + + Log::Comment(L"Confirm that the virtual bottom has not changed"); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); +} + +void ScreenBufferTests::DontChangeVirtualBottomAfterResizeWindow() +{ + auto& g = ServiceLocator::LocateGlobals(); + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& cursor = si.GetTextBuffer().GetCursor(); + auto& stateMachine = si.GetStateMachine(); + + Log::Comment(L"Pan down so the initial viewport is a couple of pages down"); + const auto initialOrigin = COORD{ 0, si.GetViewport().Height() * 2 }; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, initialOrigin, true)); + VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); + + Log::Comment(L"Make sure the initial virtual bottom is at the bottom of the viewport"); + const auto initialVirtualBottom = si.GetViewport().BottomInclusive(); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Set the cursor to the bottom of the current page"); + stateMachine.ProcessString(L"\033[9999H"); + VERIFY_ARE_EQUAL(initialVirtualBottom, cursor.GetPosition().Y); + + Log::Comment(L"Pan to the top of the buffer without changing the virtual bottom"); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Shrink the viewport height"); + std::wstringstream ss; + auto viewportWidth = si.GetViewport().Width(); + auto viewportHeight = si.GetViewport().Height() - 2; + ss << L"\x1b[8;" << viewportHeight << L";" << viewportWidth << L"t"; + stateMachine.ProcessString(ss.str()); + VERIFY_ARE_EQUAL(viewportHeight, si.GetViewport().Height()); + + Log::Comment(L"Confirm that the virtual bottom has not changed"); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); +} + +void ScreenBufferTests::DontChangeVirtualBottomWithMakeCursorVisible() +{ + auto& g = ServiceLocator::LocateGlobals(); + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& cursor = si.GetTextBuffer().GetCursor(); + auto& stateMachine = si.GetStateMachine(); + + Log::Comment(L"Pan down so the initial viewport is a couple of pages down"); + const auto initialOrigin = COORD{ 0, si.GetViewport().Height() * 2 }; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, initialOrigin, true)); + VERIFY_ARE_EQUAL(initialOrigin, si.GetViewport().Origin()); + + Log::Comment(L"Make sure the initial virtual bottom is at the bottom of the viewport"); + const auto initialVirtualBottom = si.GetViewport().BottomInclusive(); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Set the cursor to the top of the current page"); + stateMachine.ProcessString(L"\033[H"); + VERIFY_ARE_EQUAL(initialOrigin, cursor.GetPosition()); + + Log::Comment(L"Pan to the top of the buffer without changing the virtual bottom"); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, false)); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Make the cursor visible"); + si.MakeCurrentCursorVisible(); + VERIFY_ARE_EQUAL(si.GetViewport().BottomInclusive(), cursor.GetPosition().Y); + + Log::Comment(L"Confirm that the virtual bottom has not changed"); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); + + Log::Comment(L"Pan further down so the viewport is below the cursor"); + const auto belowCursor = COORD{ 0, cursor.GetPosition().Y + 10 }; + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, belowCursor, false)); + VERIFY_ARE_EQUAL(belowCursor, si.GetViewport().Origin()); + + Log::Comment(L"Make the cursor visible"); + si.MakeCurrentCursorVisible(); + VERIFY_ARE_EQUAL(si.GetViewport().Top(), cursor.GetPosition().Y); + + Log::Comment(L"Confirm that the virtual bottom has not changed"); + VERIFY_ARE_EQUAL(initialVirtualBottom, si._virtualBottom); +} + void ScreenBufferTests::RetainHorizontalOffsetWhenMovingToBottom() { auto& g = ServiceLocator::LocateGlobals();