Compare commits

...

21 commits

Author SHA1 Message Date
Mike Griese c091471fa4 okay this fixes the test to work too 2020-01-22 16:28:59 -06:00
Mike Griese 3c4d1577fe Merge remote-tracking branch 'origin/master' into dev/migrie/b/3490-resize-down 2020-01-22 15:42:11 -06:00
Mike Griese 48de202e83 I think this fixes the bug, for maximizing, but not for the test? 2020-01-22 15:41:58 -06:00
Mike Griese 13c6171224 add more notes, cleanup 2020-01-22 15:08:34 -06:00
Mike Griese 02445fd2da lots of cleanup 2020-01-22 14:49:56 -06:00
Mike Griese 252f466c2d woo all the tests pass 2020-01-22 12:49:23 -06:00
Mike Griese f37ce0180b move the cursor back down in the vt renderer, to keep it roughly aligned. This commit seems to work very well 2020-01-22 09:11:30 -06:00
Mike Griese 872f4c433c Stash this for the weekend
// TODO! Set the virtual top to the spot where we just inserted a
            // bunch of blank lines.

            // TODO! Something seems to still be invalidating the bottom line,
            // when you increase the height of the buffer. I dunno how. I need
            // to debug this.
2020-01-17 14:55:52 -06:00
Mike Griese c6a7e4ee2c Update the tests, but something's not right 2020-01-17 14:54:04 -06:00
Mike Griese 2e9ebcc3ae initial roundtrip test draft 2020-01-17 11:58:46 -06:00
Mike Griese 3398b19d25 Merge remote-tracking branch 'origin/master' into dev/migrie/b/3490-resize-down
# Conflicts:
#	src/cascadia/TerminalCore/Terminal.hpp
2020-01-17 10:48:24 -06:00
Mike Griese d116c6bbf6 cleanup some dead code, add comments 2020-01-17 10:44:51 -06:00
Mike Griese 9ccc3bcca3 Merge branch 'master' into dev/migrie/b/3490-resize-down 2020-01-17 09:45:50 -06:00
Mike Griese 668bad39c9 I couldn't tell you want all I've done but it _works_ 2020-01-16 12:02:53 -06:00
Mike Griese 17d25ee113 I thought maybe without this it would work better but it doesn't 2020-01-16 11:25:27 -06:00
Mike Griese 95122e325c I thought I could make some conpty changes to fix this, but I'm not sure that this is better. 2020-01-16 11:11:58 -06:00
Mike Griese ee08a5b866 Revert "Horrifyingly try to not increment the buffer in the middle of multiple resize operations."
This reverts commit c8c794f0d2.
2020-01-16 10:04:29 -06:00
Mike Griese c8c794f0d2 Horrifyingly try to not increment the buffer in the middle of multiple resize operations.
This actually made the problem a lot _worse_
2020-01-16 10:04:14 -06:00
Mike Griese 38ebbb6f11 This is good, but when we resize down quickly, sometimes a line gets duplicated.
I believe this is due to conpty painting too many lines. When we resize from Y
  to Y-1 lines, we'll emit Y-1 lines of text. If while we're rendering, the
  Terminal resize to Y-2, then that resize is going to sit in the buffer for the
  PtySignalThread until the paint is done, while the Paint buffers up Y-1 lines
  and sends them to the Terminal. So the Terminal has Y-2 lines in it's viewport
  at this point, but conpty just sent Y-1 lines of text, which means the top
  line is going to get scrolled off the top (and duplicated in the buffer)
2020-01-16 10:03:29 -06:00
Mike Griese 399b002af5 This seems to fix it? 2020-01-15 16:15:16 -06:00
Mike Griese 4319589510 start by authoring a simple test class 2020-01-15 15:34:16 -06:00
20 changed files with 512 additions and 67 deletions

View file

@ -1554,9 +1554,13 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
// Arguments:
// - oldBuffer - the text buffer to copy the contents FROM
// - newBuffer - the text buffer to copy the contents TO
// - padTop - If true, and we're increasing the height of the buffer, we should
// start by inserting new lines at the top of the buffer, so the contents stay
// in the relatively same position. This is only ever set to `true` in the
// console by conpty mode.
// Return Value:
// - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT.
HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const bool padTop)
{
Cursor& oldCursor = oldBuffer.GetCursor();
Cursor& newCursor = newBuffer.GetCursor();
@ -1576,6 +1580,17 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
COORD cNewCursorPos = { 0 };
bool fFoundCursorPos = false;
// If we increased the height of the buffer, pad the top of the new buffer
// with newlines, so that the contents stay in the same relative location.
if (padTop && oldBuffer.GetSize().Height() < newBuffer.GetSize().Height())
{
const auto diff = newBuffer.GetSize().Height() - oldBuffer.GetSize().Height();
for (int i = 0; i < diff; i++)
{
newBuffer.NewlineCursor();
}
}
HRESULT hr = S_OK;
// Loop through all the rows of the old buffer and reprint them into the new buffer
for (short iOldRow = 0; iOldRow < cOldRowsTotal; iOldRow++)

View file

@ -158,7 +158,7 @@ public:
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);
static HRESULT Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer);
static HRESULT Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const bool padTop);
private:
std::deque<ROW> _storage;

View file

@ -177,22 +177,25 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting
}
const auto oldTop = _mutableViewport.Top();
const auto oldBottom = _mutableViewport.BottomExclusive();
const short newBufferHeight = viewportSize.Y + _scrollbackLines;
COORD bufferSize{ viewportSize.X, newBufferHeight };
RETURN_IF_FAILED(_buffer->ResizeTraditional(bufferSize));
auto proposedTop = oldTop;
const auto newView = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
const auto proposedBottom = newView.BottomExclusive();
// If the new bottom would be below the bottom of the buffer, then slide the
// top up so that we'll still fit within the buffer.
// Attempt to "stick" to the bottom of the current viewport. Lines will be
// moved into scrollback.
const auto newViewSize = Viewport::FromDimensions({ 0, 0 }, viewportSize);
auto proposedBottom = oldBottom;
if (proposedBottom > bufferSize.Y)
{
proposedTop -= (proposedBottom - bufferSize.Y);
proposedBottom = bufferSize.Y;
}
short proposedTop = proposedBottom - newViewSize.Height();
_mutableViewport = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
_scrollOffset = 0;
_NotifyScrollEvent();

View file

@ -30,7 +30,11 @@ namespace Microsoft::Terminal::Core
// fwdecl unittest classes
#ifdef UNIT_TESTING
class ConptyRoundtripTests;
namespace TerminalCoreUnitTests
{
class TerminalBufferTests;
class ConptyRoundtripTests;
};
#endif
class Microsoft::Terminal::Core::Terminal final :
@ -252,6 +256,7 @@ private:
#pragma endregion
#ifdef UNIT_TESTING
friend class ::ConptyRoundtripTests;
friend class TerminalCoreUnitTests::TerminalBufferTests;
friend class TerminalCoreUnitTests::ConptyRoundtripTests;
#endif
};

View file

@ -40,8 +40,17 @@ using namespace Microsoft::Console::Types;
using namespace Microsoft::Terminal::Core;
class ConptyRoundtripTests
namespace TerminalCoreUnitTests
{
class TerminalBufferTests;
};
using namespace TerminalCoreUnitTests;
class TerminalCoreUnitTests::ConptyRoundtripTests final
{
static const SHORT TerminalViewWidth = 80;
static const SHORT TerminalViewHeight = 32;
TEST_CLASS(ConptyRoundtripTests);
TEST_CLASS_SETUP(ClassSetup)
@ -50,7 +59,7 @@ class ConptyRoundtripTests
m_state->InitEvents();
m_state->PrepareGlobalFont();
m_state->PrepareGlobalScreenBuffer();
m_state->PrepareGlobalScreenBuffer(TerminalViewWidth, TerminalViewHeight, TerminalViewWidth, TerminalViewHeight);
m_state->PrepareGlobalInputBuffer();
return true;
@ -71,18 +80,19 @@ class ConptyRoundtripTests
{
// STEP 1: Set up the Terminal
term = std::make_unique<Terminal>();
term->Create({ CommonState::s_csBufferWidth, CommonState::s_csBufferHeight }, 0, emptyRT);
term->Create({ TerminalViewWidth, TerminalViewHeight }, 100, emptyRT);
// STEP 2: Set up the Conpty
// Set up some sane defaults
auto& g = ServiceLocator::LocateGlobals();
auto& gci = g.getConsoleInformation();
gci.SetDefaultForegroundColor(INVALID_COLOR);
gci.SetDefaultBackgroundColor(INVALID_COLOR);
gci.SetFillAttribute(0x07); // DARK_WHITE on DARK_BLACK
m_state->PrepareNewTextBufferInfo(true);
m_state->PrepareNewTextBufferInfo(true, TerminalViewWidth, TerminalViewHeight);
auto& currentBuffer = gci.GetActiveOutputBuffer();
// Make sure a test hasn't left us in the alt buffer on accident
VERIFY_IS_FALSE(currentBuffer._IsAltBuffer());
@ -106,6 +116,13 @@ class ConptyRoundtripTests
g.pRender->AddRenderEngine(_pVtRenderEngine.get());
gci.GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get());
_pConApi = std::make_unique<ConhostInternalGetSet>(gci);
// Manually set the console into conpty mode. We're not actually going
// to set up the pipes for conpty, but we want the console to behave
// like it would in conpty mode.
g.EnableConptyModeForTests();
expectedOutput.clear();
return true;
@ -129,13 +146,18 @@ class ConptyRoundtripTests
TEST_METHOD(SimpleWriteOutputTest);
TEST_METHOD(WriteTwoLinesUsesNewline);
TEST_METHOD(WriteAFewSimpleLines);
TEST_METHOD(TestResizeHeight);
private:
bool _writeCallback(const char* const pch, size_t const cch);
void _flushFirstFrame();
void _resizeConpty(const unsigned short sx, const unsigned short sy);
std::deque<std::string> expectedOutput;
std::unique_ptr<Microsoft::Console::Render::VtEngine> _pVtRenderEngine;
std::unique_ptr<CommonState> m_state;
std::unique_ptr<Microsoft::Console::VirtualTerminal::ConGetSet> _pConApi;
bool _checkConptyOutput{ true };
DummyRenderTarget emptyRT;
std::unique_ptr<Terminal> term;
@ -144,18 +166,27 @@ private:
bool ConptyRoundtripTests::_writeCallback(const char* const pch, size_t const cch)
{
std::string actualString = std::string(pch, cch);
VERIFY_IS_GREATER_THAN(expectedOutput.size(),
static_cast<size_t>(0),
NoThrowString().Format(L"writing=\"%hs\", expecting %u strings", actualString.c_str(), expectedOutput.size()));
std::string first = expectedOutput.front();
expectedOutput.pop_front();
if (_checkConptyOutput)
{
VERIFY_IS_GREATER_THAN(expectedOutput.size(),
static_cast<size_t>(0),
NoThrowString().Format(L"writing=\"%hs\", expecting %u strings", actualString.c_str(), expectedOutput.size()));
Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", first.c_str()));
Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", actualString.c_str()));
std::string first = expectedOutput.front();
expectedOutput.pop_front();
VERIFY_ARE_EQUAL(first.length(), cch);
VERIFY_ARE_EQUAL(first, actualString);
Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", first.c_str()));
Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", actualString.c_str()));
VERIFY_ARE_EQUAL(first.length(), cch);
VERIFY_ARE_EQUAL(first, actualString);
}
else
{
Log::Comment(NoThrowString().Format(
L"Writing \"%hs\" to Terminal", actualString.c_str()));
}
// Write the string back to our Terminal
const auto converted = ConvertToW(CP_UTF8, actualString);
@ -177,6 +208,19 @@ void ConptyRoundtripTests::_flushFirstFrame()
VERIFY_SUCCEEDED(renderer.PaintFrame());
}
void ConptyRoundtripTests::_resizeConpty(const unsigned short sx,
const unsigned short sy)
{
// Largely taken from implementation in PtySignalInputThread::_InputThread
if (DispatchCommon::s_ResizeWindow(*_pConApi, sx, sy))
{
// Instead of going through the VtIo to suppress the resize repaint,
// just call the method directly on the renderer. This is implemented in
// VtIo::SuppressResizeRepaint
VERIFY_SUCCEEDED(_pVtRenderEngine->SuppressResizeRepaint());
}
}
// Function Description:
// - Helper function to validate that a number of characters in a row are all
// the same. Validates that the next end-start characters are all equal to the
@ -364,3 +408,153 @@ void ConptyRoundtripTests::WriteAFewSimpleLines()
verifyData(termTb);
}
void ConptyRoundtripTests::TestResizeHeight()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
TEST_METHOD_PROPERTY(L"Data:dx", L"{-1, 0, 1}")
TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}")
END_TEST_METHOD_PROPERTIES()
int dx, dy;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer");
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer");
_checkConptyOutput = false;
auto& g = ServiceLocator::LocateGlobals();
auto& renderer = *g.pRender;
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& hostSm = si.GetStateMachine();
auto* hostTb = &si.GetTextBuffer();
auto* termTb = term->_buffer.get();
const auto initialHostView = si.GetViewport();
const auto initialTermView = term->GetViewport();
VERIFY_ARE_EQUAL(0, initialHostView.Top());
VERIFY_ARE_EQUAL(TerminalViewHeight, initialHostView.BottomExclusive());
VERIFY_ARE_EQUAL(0, initialTermView.Top());
VERIFY_ARE_EQUAL(TerminalViewHeight, initialTermView.BottomExclusive());
Log::Comment(NoThrowString().Format(
L"Print 50 lines of output, which will scroll the viewport"));
for (auto i = 0; i < 50; i++)
{
auto wstr = std::wstring(1, static_cast<wchar_t>(L'0' + i));
hostSm.ProcessString(wstr);
hostSm.ProcessString(L"\r\n");
}
// Conpty's doesn't have a scrollback, it's view's origin is always 0,0
const auto secondHostView = si.GetViewport();
VERIFY_ARE_EQUAL(0, secondHostView.Top());
VERIFY_ARE_EQUAL(TerminalViewHeight, secondHostView.BottomExclusive());
VERIFY_SUCCEEDED(renderer.PaintFrame());
const auto secondTermView = term->GetViewport();
VERIFY_ARE_EQUAL(50 - initialTermView.Height() + 1, secondTermView.Top());
VERIFY_ARE_EQUAL(50, secondTermView.BottomInclusive());
auto verifyTermData = [](TextBuffer& termTb) {
for (short row = 0; row < 50; row++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
auto iter = termTb.GetCellDataAt({ 0, row });
auto expectedString = std::wstring(1, static_cast<wchar_t>(L'0' + row));
if (iter->Chars() != expectedString)
{
Log::Comment(NoThrowString().Format(L"row [%d] was mismatched", row));
}
VERIFY_ARE_EQUAL(expectedString, (iter++)->Chars());
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
}
};
auto verifyHostData = [&si](TextBuffer& hostTb, const int resizeDy = 0) {
const auto hostView = si.GetViewport();
// The last row of the viewport should be empty
// The second last row will have '0'+50
// The third last row will have '0'+49
// ...
// The <height> last row will have '0'+(50-height+1)
const auto firstChar = static_cast<wchar_t>(L'0' + (50 - hostView.Height() + 1));
// If we increased the height of the buffer, then we're going to insert
// pad the top of the buffer with blank lines, to keep the real content
// we had at the bottom of the buffer.
// In that case, check for the first lines to be filled with spaces.
const short firstRowWithChars = gsl::narrow_cast<short>(resizeDy > 0 ? resizeDy : 0);
if (resizeDy > 0)
{
for (short row = 0; row < firstRowWithChars; row++)
{
auto iter = hostTb.GetCellDataAt({ 0, row });
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
}
}
// Don't include the last row of the viewport in this check, since it'll be blank
for (short row = firstRowWithChars; row < hostView.Height() - 1; row++)
{
auto iter = hostTb.GetCellDataAt({ 0, row });
auto expectedString = std::wstring(1, static_cast<wchar_t>(firstChar + row));
if (iter->Chars() != expectedString)
{
Log::Comment(NoThrowString().Format(L"row [%d] was mismatched", row));
}
VERIFY_ARE_EQUAL(expectedString, (iter++)->Chars(), NoThrowString().Format(L"%s", expectedString.data()));
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
}
// Check the last row of the viewport here
auto iter = hostTb.GetCellDataAt({ 0, hostView.Height() - 1 });
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
};
verifyHostData(*hostTb);
verifyTermData(*termTb);
const COORD newViewportSize{
gsl::narrow_cast<short>(TerminalViewWidth + dx),
gsl::narrow_cast<short>(TerminalViewHeight + dy)
};
auto resizeResult = term->UserResize(newViewportSize);
VERIFY_SUCCEEDED(resizeResult);
// DebugBreak();
_resizeConpty(newViewportSize.X, newViewportSize.Y);
// After we resize, make sure to get the new textBuffers
hostTb = &si.GetTextBuffer();
termTb = term->_buffer.get();
// Conpty's doesn't have a scrollback, it's view's origin is always 0,0
const auto thirdHostView = si.GetViewport();
VERIFY_ARE_EQUAL(0, thirdHostView.Top());
VERIFY_ARE_EQUAL(newViewportSize.Y, thirdHostView.BottomExclusive());
// The Terminal should be stuck on the bottom of the viewport
const auto thirdTermView = term->GetViewport();
VERIFY_ARE_EQUAL(50 - thirdTermView.Height() + 1, thirdTermView.Top());
VERIFY_ARE_EQUAL(50, thirdTermView.BottomInclusive());
verifyHostData(*hostTb, dy);
verifyTermData(*termTb);
VERIFY_SUCCEEDED(renderer.PaintFrame());
// Conpty's doesn't have a scrollback, it's view's origin is always 0,0
const auto fourthHostView = si.GetViewport();
VERIFY_ARE_EQUAL(0, fourthHostView.Top());
VERIFY_ARE_EQUAL(newViewportSize.Y, fourthHostView.BottomExclusive());
// The Terminal should be stuck on the bottom of the viewport
const auto fourthTermView = term->GetViewport();
VERIFY_ARE_EQUAL(50 - fourthTermView.Height() + 1, fourthTermView.Top());
VERIFY_ARE_EQUAL(50, fourthTermView.BottomInclusive());
}

View file

@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include <WexTestClass.h>
#include "../renderer/inc/DummyRenderTarget.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"
#include "consoletaeftemplates.hpp"
using namespace winrt::Microsoft::Terminal::Settings;
using namespace Microsoft::Terminal::Core;
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
namespace TerminalCoreUnitTests
{
class TerminalBufferTests;
};
using namespace TerminalCoreUnitTests;
class TerminalCoreUnitTests::TerminalBufferTests final
{
TEST_CLASS(TerminalBufferTests);
TEST_METHOD(TestResizeHeight);
TEST_METHOD_SETUP(MethodSetup)
{
// STEP 1: Set up the Terminal
term = std::make_unique<Terminal>();
term->Create({ 80, 32 }, 100, emptyRT);
return true;
}
TEST_METHOD_CLEANUP(MethodCleanup)
{
term = nullptr;
return true;
}
private:
DummyRenderTarget emptyRT;
std::unique_ptr<Terminal> term;
};
void TerminalBufferTests::TestResizeHeight()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}")
END_TEST_METHOD_PROPERTIES()
int dy;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer");
auto& termTb = *term->_buffer;
auto& termSm = *term->_stateMachine;
const auto initialView = term->GetViewport();
VERIFY_ARE_EQUAL(0, initialView.Top());
VERIFY_ARE_EQUAL(32, initialView.BottomExclusive());
Log::Comment(NoThrowString().Format(
L"Print 50 lines of output, which will scroll the viewport"));
for (auto i = 0; i < 50; i++)
{
auto wstr = std::wstring(1, static_cast<wchar_t>(L'0' + i));
termSm.ProcessString(wstr);
termSm.ProcessString(L"\r\n");
}
const auto secondView = term->GetViewport();
VERIFY_ARE_EQUAL(50 - initialView.Height() + 1, secondView.Top());
VERIFY_ARE_EQUAL(50, secondView.BottomInclusive());
auto verifyBufferContents = [&termTb]() {
for (short row = 0; row < 50; row++)
{
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
auto iter = termTb.GetCellDataAt({ 0, row });
auto expectedString = std::wstring(1, static_cast<wchar_t>(L'0' + row));
if (iter->Chars() != expectedString)
{
Log::Comment(NoThrowString().Format(L"row [%d] was mismatched", row));
}
VERIFY_ARE_EQUAL(expectedString, (iter++)->Chars());
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
}
};
verifyBufferContents();
auto resizeResult = term->UserResize({ 80, gsl::narrow_cast<short>(32 + dy) });
VERIFY_SUCCEEDED(resizeResult);
const auto thirdView = term->GetViewport();
VERIFY_ARE_EQUAL(50 - thirdView.Height() + 1, thirdView.Top());
VERIFY_ARE_EQUAL(50, thirdView.BottomInclusive());
verifyBufferContents();
}

View file

@ -11,6 +11,7 @@
<Import Project="$(SolutionDir)\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(SolutionDir)\src\common.build.pre.props" />
<ItemGroup>
<ClCompile Include="TerminalBufferTests.cpp" />
<ClCompile Include="ScreenSizeLimitsTest.cpp" />
<ClCompile Include="SelectionTest.cpp" />
<ClCompile Include="InputTest.cpp" />

View file

@ -638,3 +638,17 @@ void ConsoleArguments::SetExpectedSize(COORD dimensions) noexcept
_recievedEarlySizeChange = true;
}
}
#ifdef UNIT_TESTING
// Method Description:
// - This is a test helper method. It can be used to trick us into thinking
// we're headless (in conpty mode), even without parsing any arguments.
// Arguments:
// - <none>
// Return Value:
// - <none>
void ConsoleArguments::EnableConptyModeForTests()
{
_headless = true;
}
#endif

View file

@ -53,6 +53,10 @@ public:
void SetExpectedSize(COORD dimensions) noexcept;
#ifdef UNIT_TESTING
void EnableConptyModeForTests();
#endif
static const std::wstring_view VT_MODE_ARG;
static const std::wstring_view HEADLESS_ARG;
static const std::wstring_view SERVER_HANDLE_ARG;

View file

@ -206,6 +206,21 @@ bool VtIo::IsUsingVt() const
return _objectsCreated;
}
#ifdef UNIT_TESTING
// Method Description:
// - This is a test helper method. It can be used to trick VtIo into responding
// true to `IsUsingVt`, which will cause the console host to act in conpty
// mode.
// Arguments:
// - <none>
// Return Value:
// - <none>
void VtIo::EnableConptyModeForTests()
{
_objectsCreated = true;
}
#endif
// Routine Description:
// Potentially starts this VtIo's input thread and render engine.
// If the VtIo hasn't yet been given pipes, then this function will
@ -431,3 +446,11 @@ void VtIo::EndResize()
_pVtRenderEngine->EndResizeRequest();
}
}
void VtIo::SetVirtualTop(const short virtualTop) noexcept
{
if (_pVtRenderEngine)
{
_pVtRenderEngine->SetVirtualTop(virtualTop);
}
}

View file

@ -38,6 +38,11 @@ namespace Microsoft::Console::VirtualTerminal
void BeginResize();
void EndResize();
void SetVirtualTop(const short virtualTop) noexcept;
#ifdef UNIT_TESTING
void EnableConptyModeForTests();
#endif
private:
// After CreateIoHandlers is called, these will be invalid.

View file

@ -776,7 +776,15 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
{
// TODO: MSFT: 9574827 - shouldn't we be looking at or at least logging the failure codes here? (Or making them non-void?)
context.PostUpdateWindowSize();
WriteToScreen(context, context.GetViewport());
// Use WriteToScreen to invalidate the viewport with the renderer.
// GH#3490 - If we're in conpty mode, don't invalidate the entire
// viewport. In conpty mode, the VtEngine will later decide what
// part of the buffer actually needs to be re-sent to the terminal.
if (!g.getConsoleInformation().IsInVtIoMode())
{
WriteToScreen(context, context.GetViewport());
}
}
return S_OK;
}

View file

@ -16,3 +16,19 @@ bool Globals::IsHeadless() const
{
return launchArgs.IsHeadless();
}
#ifdef UNIT_TESTING
// Method Description:
// - This is a test helper method. It can be used to trick us into responding
// true to `IsHeadless`, which will cause the console host to act in conpty
// mode.
// Arguments:
// - <none>
// Return Value:
// - <none>
void Globals::EnableConptyModeForTests()
{
launchArgs.EnableConptyModeForTests();
getConsoleInformation().GetVtIo()->EnableConptyModeForTests();
}
#endif

View file

@ -69,6 +69,10 @@ public:
ApiRoutines api;
#ifdef UNIT_TESTING
void EnableConptyModeForTests();
#endif
private:
CONSOLE_INFORMATION ciConsoleInformation;
};

View file

@ -1423,17 +1423,38 @@ bool SCREEN_INFORMATION::IsMaximizedY() const
}
// Save cursor's relative height versus the viewport
SHORT const sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top();
short cursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top();
HRESULT hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get());
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const bool isConpty = gci.IsInVtIoMode();
HRESULT hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get(), isConpty);
if (SUCCEEDED(hr))
{
// GH#3490 - In conpty mode, we want to pad new lines into to the top of
// the buffer when we increase the buffer height. Terminals typically
// keep the buffer contents "stuck" to the bottom of the viewport when
// they increase in height, moving lines from the scrollback into the
// viewport. We don't have a scrollback at all in conpty mode, but we
// can still keep the content we have at the bottom by inserting new
// lines at the top.
//
// When that happens, make sure to move our relative cursor position
// down, so that it stays in the same position relative to the bottom
// that it was before.
if (isConpty && _textBuffer->GetSize().Height() < newTextBuffer->GetSize().Height())
{
const short diff = newTextBuffer->GetSize().Height() - _textBuffer->GetSize().Height();
cursorHeightInViewportBefore += diff;
gci.GetVtIo()->SetVirtualTop(diff);
}
Cursor& newCursor = newTextBuffer->GetCursor();
// Adjust the viewport so the cursor doesn't wildly fly off up or down.
SHORT const sCursorHeightInViewportAfter = newCursor.GetPosition().Y - _viewport.Top();
const short cursorHeightInViewportAfter = newCursor.GetPosition().Y - _viewport.Top();
COORD coordCursorHeightDiff = { 0 };
coordCursorHeightDiff.Y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore;
coordCursorHeightDiff.Y = cursorHeightInViewportAfter - cursorHeightInViewportBefore;
LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, true));
_textBuffer.swap(newTextBuffer);
@ -2176,8 +2197,13 @@ void SCREEN_INFORMATION::SetDefaultAttributes(const TextAttribute& attributes,
commandLine.UpdatePopups(attributes, popupAttributes, oldPrimaryAttributes, oldPopupAttributes);
}
// force repaint of entire viewport
GetRenderTarget().TriggerRedrawAll();
// Force repaint of entire viewport, unless we're in conpty mode. In that
// case, we don't really need to force a redraw of the entire screen just
// because the text attributes changed.
if (!gci.IsInVtIoMode())
{
GetRenderTarget().TriggerRedrawAll();
}
gci.ConsoleIme.RefreshAreaAttributes();

View file

@ -50,6 +50,14 @@ Revision History:
#include "../types/IConsoleWindow.hpp"
class ConversionAreaInfo; // forward decl window. circular reference
// fwdecl unittest classes
#ifdef UNIT_TESTING
namespace TerminalCoreUnitTests
{
class ConptyRoundtripTests;
};
#endif
class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console::IIoProvider
{
public:
@ -308,6 +316,6 @@ private:
friend class ScreenBufferTests;
friend class CommonState;
friend class ConptyOutputTests;
friend class ConptyRoundtripTests;
friend class TerminalCoreUnitTests::ConptyRoundtripTests;
#endif
};

View file

@ -85,6 +85,11 @@ class ConptyOutputTests
g.pRender->AddRenderEngine(_pVtRenderEngine.get());
gci.GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get());
// Manually set the console into conpty mode. We're not actually going
// to set up the pipes for conpty, but we want the console to behave
// like it would in conpty mode.
g.EnableConptyModeForTests();
expectedOutput.clear();
return true;

View file

@ -81,16 +81,19 @@ public:
}
}
void PrepareGlobalScreenBuffer()
void PrepareGlobalScreenBuffer(const short viewWidth = s_csWindowWidth,
const short viewHeight = s_csWindowHeight,
const short bufferWidth = s_csBufferWidth,
const short bufferHeight = s_csBufferHeight)
{
CONSOLE_INFORMATION& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation();
COORD coordWindowSize;
coordWindowSize.X = s_csWindowWidth;
coordWindowSize.Y = s_csWindowHeight;
coordWindowSize.X = viewWidth;
coordWindowSize.Y = viewHeight;
COORD coordScreenBufferSize;
coordScreenBufferSize.X = s_csBufferWidth;
coordScreenBufferSize.Y = s_csBufferHeight;
coordScreenBufferSize.X = bufferWidth;
coordScreenBufferSize.Y = bufferHeight;
UINT uiCursorSize = 12;
@ -143,12 +146,14 @@ public:
gci.SetCookedReadData(nullptr);
}
void PrepareNewTextBufferInfo(const bool useDefaultAttributes = false)
void PrepareNewTextBufferInfo(const bool useDefaultAttributes = false,
const short bufferWidth = s_csBufferWidth,
const short bufferHeight = s_csBufferHeight)
{
CONSOLE_INFORMATION& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation();
COORD coordScreenBufferSize;
coordScreenBufferSize.X = s_csBufferWidth;
coordScreenBufferSize.Y = s_csBufferHeight;
coordScreenBufferSize.X = bufferWidth;
coordScreenBufferSize.Y = bufferHeight;
UINT uiCursorSize = 12;

View file

@ -37,7 +37,6 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe,
_lastViewport(initialViewport),
_invalidRect(Viewport::Empty()),
_fInvalidRectUsed(false),
_lastRealCursor({ 0 }),
_lastText({ 0 }),
_scrollDelta({ 0 }),
_quickReturn(false),
@ -280,34 +279,29 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe,
if (SUCCEEDED(hr))
{
// Viewport is smaller now - just update it all.
if (oldView.Height() > newView.Height() || oldView.Width() > newView.Width())
if (oldView.Width() != newView.Width())
{
// Viewport is a different width now - just update it all. We may
// have re-wrapped the buffer contents.
hr = InvalidateAll();
}
else
else if (oldView.Height() < newView.Height())
{
// At least one of the directions grew.
// First try and add everything to the right of the old viewport,
// then everything below where the old viewport ended.
if (oldView.Width() < newView.Width())
{
short left = oldView.RightExclusive();
short top = 0;
short right = newView.RightInclusive();
short bottom = oldView.BottomInclusive();
Viewport rightOfOldViewport = Viewport::FromInclusive({ left, top, right, bottom });
hr = _InvalidCombine(rightOfOldViewport);
}
if (SUCCEEDED(hr) && oldView.Height() < newView.Height())
{
short left = 0;
short top = oldView.BottomExclusive();
short right = newView.RightInclusive();
short bottom = newView.BottomInclusive();
Viewport belowOldViewport = Viewport::FromInclusive({ left, top, right, bottom });
hr = _InvalidCombine(belowOldViewport);
}
// We grew in height. We inserted empty lines at the top of the
// buffer. The cursor is now _actually_ lower (larger Y/row value)
// than it was before.
_lastText.Y += gsl::narrow_cast<short>(newView.Height() - oldView.Height());
// The text content will try and stay "stuck" at the bottom of the
// viewport of the terminal, and invalidating the bottom here can
// cause unnecessary lines to get written to the terminal. See
// GH#3490.
}
else if (oldView.Height() > newView.Height())
{
// We shrunk in height. We don't really need to do anything here.
// Shrinking in height will remove lines from the top of the buffer
// (pushing them into scrollback in the terminal).
// The cursor will stay in the same relative position.
}
}
_resized = true;
@ -409,6 +403,11 @@ bool VtEngine::_AllIsInvalid() const
return S_OK;
}
void VtEngine::SetVirtualTop(const short virtualTop) noexcept
{
_virtualTop = std::max(_virtualTop, virtualTop);
}
void VtEngine::SetTerminalOwner(Microsoft::Console::ITerminalOwner* const terminalOwner)
{
_terminalOwner = terminalOwner;

View file

@ -26,7 +26,10 @@ Author(s):
// fwdecl unittest classes
#ifdef UNIT_TESTING
class ConptyRoundtripTests;
namespace TerminalCoreUnitTests
{
class ConptyRoundtripTests;
};
#endif
namespace Microsoft::Console::Render
@ -101,6 +104,7 @@ namespace Microsoft::Console::Render
void SetTerminalOwner(Microsoft::Console::ITerminalOwner* const terminalOwner);
void BeginResizeRequest();
void EndResizeRequest();
void SetVirtualTop(const short virtualTop) noexcept;
protected:
wil::unique_hfile _hFile;
@ -116,7 +120,6 @@ namespace Microsoft::Console::Render
Microsoft::Console::Types::Viewport _invalidRect;
bool _fInvalidRectUsed;
COORD _lastRealCursor;
COORD _lastText;
COORD _scrollDelta;
@ -226,7 +229,7 @@ namespace Microsoft::Console::Render
friend class VtRendererTest;
friend class ConptyOutputTests;
friend class ConptyRoundtripTests;
friend class TerminalCoreUnitTests::ConptyRoundtripTests;
#endif
void SetTestCallback(_In_ std::function<bool(const char* const, size_t const)> pfn);