Add support for "reflow"ing the Terminal buffer (#4741)
This PR adds support for "Resize with Reflow" to the Terminal. In conhost, `ResizeWithReflow` is the function that's responsible for reflowing wrapped lines of text as the buffer gets resized. Now that #4415 has merged, we can also implement this in the Terminal. Now, when the Terminal is resized, it will reflow the lines of it's buffer in the same way that conhost does. This means, the terminal will no longer chop off the ends of lines as the buffer is too small to represent them. As a happy side effect of this PR, it also fixed #3490. This was a bug that plagued me during the investigation into this functionality. The original #3490 PR, #4354, tried to fix this bug with some heavy conpty changes. Turns out, that only made things worse, and far more complicated. When I really got to thinking about it, I realized "conhost can handle this right, why can't the Terminal?". Turns out, by adding resize with reflow, I was also able to fix this at the same time. Conhost does a little bit of math after reflowing to attempt to keep the viewport in the same relative place after a reflow. By re-using that logic in the Terminal, I was able to fix #3490. I also included that big ole test from #3490, because everyone likes adding 60 test cases in a PR. ## References * #4200 - this scenario * #405/#4415 - conpty emits wrapped lines, which was needed for this PR * #4403 - delayed EOL wrapping via conpty, which was also needed for this * #4354 - we don't speak of this PR anymore ## PR Checklist * [x] Closes #1465 * [x] Closes #3490 * [x] Closes #4771 * [x] Tests added/passed ## EDIT: Changes to this PR on 5 March 2020 I learned more since my original version of this PR. I wrote that in January, and despite my notes that say it was totally working, it _really_ wasn't. Part of the hard problem, as mentioned in #3490, is that the Terminal might request a resize to (W, H-1), and while conpty is preparing that frame, or before the terminal has received that frame, the Terminal resizes to (W, H-2). Now, there aren't enough lines in the terminal buffer to catch all the lines that conpty is about to emit. When that happens, lines get duplicated in the buffer. From a UX perspective, this certainly looks a lot worse than a couple lost lines. It looks like utter chaos. So I've introduced a new mode to conpty to try and counteract this behavior. This behavior I'm calling "quirky resize". The **TL;DR** of quirky resize mode is that conpty won't emit the entire buffer on a resize, and will trust that the terminal is prepared to reflow it's buffer on it's own. This will enable the quirky resize behavior for applications that are prepared for it. The "quirky resize" is "don't `InvalidateAll` when the terminal resizes". This is added as a quirk as to not regress other terminal applications that aren't prepared for this behavior (gnome-terminal, conhost in particular). For those kinds of terminals, when the buffer is resized, it's just going to lose lines. That's what currently happens for them. When the quirk is enabled, conpty won't repaint the entire buffer. This gets around the "duplicated lines" issue that requesting multiple resizes in a row can cause. However, for these terminals that are unprepared, the conpty cursor might end up in the wrong position after a quirky resize. The case in point is maximizing the terminal. For maximizing (height->50) from a buffer that's 30 lines tall, with the cursor on y=30, this is what happens: * With the quirk disabled, conpty reprints the entire buffer. This is 60 lines that get printed. This ends up blowing away about 20 lines of scrollback history, as the terminal app would have tried to keep the text pinned to the bottom of the window. The term. app moved the viewport up 20 lines, and then the 50 lines of conpty output (30 lines of text, and 20 blank lines at the bottom) overwrote the lines from the scrollback. This is bad, but not immediately obvious, and is **what currently happens**. * With the quirk enabled, conpty doesn't emit any lines, but the actual content of the window is still only in the top 30 lines. However, the terminal app has still moved 20 lines down from the scrollback back into the viewport. So the terminal's cursor is at y=50 now, but conpty's is at 30. This means that the terminal and conpty are out of sync, and there's not a good way of re-syncing these. It's very possible (trivial in `powershell`) that the new output will jump up to y=30 override the existing output in the terminal buffer. The Windows Terminal is already prepared for this quirky behavior, so it doesn't keep the output at the bottom of the window. It shifts it's viewport down to match what conpty things the buffer looks like. What happens when we have passthrough mode and WT is like "I would like quirky resize"? I guess things will just work fine, cause there won't be a buffer behind the passthrough app that the terminal cares about. Sure, in the passthrough case the Terminal could _not_ quirky resize, but the quirky resize won't be wrong.
This commit is contained in:
parent
068e3e7bc2
commit
93b31f6e3f
|
@ -573,27 +573,21 @@ bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
|
|||
}
|
||||
|
||||
//Routine Description:
|
||||
// - Retrieves the position of the last non-space character on the final line of the text buffer.
|
||||
// - By default, we search the entire buffer to find the last non-space character
|
||||
//Arguments:
|
||||
// - <none>
|
||||
//Return Value:
|
||||
// - Coordinate position in screen coordinates (offset coordinates, not array index coordinates).
|
||||
COORD TextBuffer::GetLastNonSpaceCharacter() const
|
||||
{
|
||||
return GetLastNonSpaceCharacter(GetSize());
|
||||
}
|
||||
|
||||
//Routine Description:
|
||||
// - Retrieves the position of the last non-space character in the given viewport
|
||||
// - This is basically an optimized version of GetLastNonSpaceCharacter(), and can be called when
|
||||
// - we know the last character is within the given viewport (so we don't need to check the entire buffer)
|
||||
// - Retrieves the position of the last non-space character in the given
|
||||
// viewport
|
||||
// - By default, we search the entire buffer to find the last non-space
|
||||
// character.
|
||||
// - If we know the last character is within the given viewport (so we don't
|
||||
// need to check the entire buffer), we can provide a value in viewOptional
|
||||
// that we'll use to search for the last character in.
|
||||
//Arguments:
|
||||
// - The viewport
|
||||
//Return value:
|
||||
// - Coordinate position (relative to the text buffer)
|
||||
COORD TextBuffer::GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const
|
||||
COORD TextBuffer::GetLastNonSpaceCharacter(std::optional<const Microsoft::Console::Types::Viewport> viewOptional) const
|
||||
{
|
||||
const auto viewport = viewOptional.has_value() ? viewOptional.value() : GetSize();
|
||||
|
||||
COORD coordEndOfText = { 0 };
|
||||
// Search the given viewport by starting at the bottom.
|
||||
coordEndOfText.Y = viewport.BottomInclusive();
|
||||
|
@ -1872,9 +1866,18 @@ 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
|
||||
// - lastCharacterViewport - Optional. If the caller knows that the last
|
||||
// nonspace character is in a particular Viewport, the caller can provide this
|
||||
// parameter as an optimization, as opposed to searching the entire buffer.
|
||||
// - oldViewportTop - Optional. The caller can provide a row in this parameter
|
||||
// and we'll calculate the position of the _end_ of that row in the new
|
||||
// buffer. The row's new value is placed back into this parameter.
|
||||
// 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 std::optional<Viewport> lastCharacterViewport,
|
||||
std::optional<short>& oldViewportTop)
|
||||
{
|
||||
Cursor& oldCursor = oldBuffer.GetCursor();
|
||||
Cursor& newCursor = newBuffer.GetCursor();
|
||||
|
@ -1886,14 +1889,14 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
|
|||
// place the new cursor back on the equivalent character in
|
||||
// the new buffer.
|
||||
const COORD cOldCursorPos = oldCursor.GetPosition();
|
||||
const COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter();
|
||||
const COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport);
|
||||
|
||||
short const cOldRowsTotal = cOldLastChar.Y + 1;
|
||||
short const cOldColsTotal = oldBuffer.GetSize().Width();
|
||||
const short cOldRowsTotal = cOldLastChar.Y + 1;
|
||||
const short cOldColsTotal = oldBuffer.GetSize().Width();
|
||||
|
||||
COORD cNewCursorPos = { 0 };
|
||||
bool fFoundCursorPos = false;
|
||||
|
||||
bool foundOldRow = false;
|
||||
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++)
|
||||
|
@ -1953,6 +1956,19 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer)
|
|||
}
|
||||
CATCH_RETURN();
|
||||
}
|
||||
|
||||
// If we found the old row that the caller was interested in, set the
|
||||
// out value of that parameter to the cursor's current Y position (the
|
||||
// new location of the _end_ of that row in the buffer).
|
||||
if (oldViewportTop.has_value() && !foundOldRow)
|
||||
{
|
||||
if (iOldRow >= oldViewportTop.value())
|
||||
{
|
||||
oldViewportTop = newCursor.GetPosition().Y;
|
||||
foundOldRow = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
// If we didn't have a full row to copy, insert a new
|
||||
|
|
|
@ -103,8 +103,7 @@ public:
|
|||
// Scroll needs access to this to quickly rotate around the buffer.
|
||||
bool IncrementCircularBuffer(const bool inVtMode = false);
|
||||
|
||||
COORD GetLastNonSpaceCharacter() const;
|
||||
COORD GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const;
|
||||
COORD GetLastNonSpaceCharacter(std::optional<const Microsoft::Console::Types::Viewport> viewOptional = std::nullopt) const;
|
||||
|
||||
Cursor& GetCursor() noexcept;
|
||||
const Cursor& GetCursor() const noexcept;
|
||||
|
@ -162,7 +161,10 @@ public:
|
|||
const std::wstring_view fontFaceName,
|
||||
const COLORREF backgroundColor);
|
||||
|
||||
static HRESULT Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer);
|
||||
static HRESULT Reflow(TextBuffer& oldBuffer,
|
||||
TextBuffer& newBuffer,
|
||||
const std::optional<Microsoft::Console::Types::Viewport> lastCharacterViewport,
|
||||
std::optional<short>& oldViewportTop);
|
||||
|
||||
private:
|
||||
std::deque<ROW> _storage;
|
||||
|
|
|
@ -519,7 +519,6 @@ namespace winrt::TerminalApp::implementation
|
|||
|
||||
// Create a connection based on the values in our settings object.
|
||||
const auto connection = _CreateConnectionFromSettings(profileGuid, settings);
|
||||
|
||||
TermControl term{ settings, connection };
|
||||
|
||||
// Add the new tab to the list of our tabs.
|
||||
|
|
|
@ -201,7 +201,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
|
|||
try
|
||||
{
|
||||
const COORD dimensions{ gsl::narrow_cast<SHORT>(_initialCols), gsl::narrow_cast<SHORT>(_initialRows) };
|
||||
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, 0, &_inPipe, &_outPipe, &_hPC));
|
||||
THROW_IF_FAILED(_CreatePseudoConsoleAndPipes(dimensions, PSEUDOCONSOLE_RESIZE_QUIRK, &_inPipe, &_outPipe, &_hPC));
|
||||
THROW_IF_FAILED(_LaunchAttachedClient());
|
||||
|
||||
_startTime = std::chrono::high_resolution_clock::now();
|
||||
|
|
|
@ -173,24 +173,143 @@ void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSetting
|
|||
{
|
||||
return S_FALSE;
|
||||
}
|
||||
const auto dx = viewportSize.X - oldDimensions.X;
|
||||
|
||||
const auto oldTop = _mutableViewport.Top();
|
||||
|
||||
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();
|
||||
// Save cursor's relative height versus the viewport
|
||||
const short sCursorHeightInViewportBefore = _buffer->GetCursor().GetPosition().Y - _mutableViewport.Top();
|
||||
|
||||
// This will be used to determine where the viewport should be in the new buffer.
|
||||
const short oldViewportTop = _mutableViewport.Top();
|
||||
short newViewportTop = oldViewportTop;
|
||||
|
||||
// First allocate a new text buffer to take the place of the current one.
|
||||
std::unique_ptr<TextBuffer> newTextBuffer;
|
||||
try
|
||||
{
|
||||
newTextBuffer = std::make_unique<TextBuffer>(bufferSize,
|
||||
_buffer->GetCurrentAttributes(),
|
||||
0, // temporarily set size to 0 so it won't render.
|
||||
_buffer->GetRenderTarget());
|
||||
|
||||
std::optional<short> oldViewStart{ oldViewportTop };
|
||||
RETURN_IF_FAILED(TextBuffer::Reflow(*_buffer.get(),
|
||||
*newTextBuffer.get(),
|
||||
_mutableViewport,
|
||||
oldViewStart));
|
||||
newViewportTop = oldViewStart.value();
|
||||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
// Conpty resizes a little oddly - if the height decreased, and there were
|
||||
// blank lines at the bottom, those lines will get trimmed. If there's not
|
||||
// blank lines, then the top will get "shifted down", moving the top line
|
||||
// into scrollback. See GH#3490 for more details.
|
||||
//
|
||||
// If the final position in the buffer is on the bottom row of the new
|
||||
// viewport, then we're going to need to move the top down. Otherwise, move
|
||||
// the bottom up.
|
||||
//
|
||||
// There are also important things to consider with line wrapping.
|
||||
// * If a line in scrollback wrapped that didn't previously, we'll need to
|
||||
// make sure to have the new viewport down another line. This will cause
|
||||
// our top to move down.
|
||||
// * If a line _in the viewport_ wrapped that didn't previously, then the
|
||||
// conpty buffer will also have that wrapped line, and will move the
|
||||
// cursor & text down a line in response. This causes our bottom to move
|
||||
// down.
|
||||
//
|
||||
// We're going to use a combo of both these things to calculate where the
|
||||
// new viewport should be. To keep in sync with conpty, we'll need to make
|
||||
// sure that any lines that entered the scrollback _stay in scrollback_. We
|
||||
// do that by taking the max of
|
||||
// * Where the old top line in the viewport exists in the new buffer (as
|
||||
// calculated by TextBuffer::Reflow)
|
||||
// * Where the bottom of the text in the new buffer is (and using that to
|
||||
// calculate another proposed top location).
|
||||
|
||||
const COORD newCursorPos = newTextBuffer->GetCursor().GetPosition();
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26496) // cpp core checks wants this const, but it's assigned immediately below...
|
||||
COORD newLastChar = newCursorPos;
|
||||
try
|
||||
{
|
||||
newLastChar = newTextBuffer->GetLastNonSpaceCharacter();
|
||||
}
|
||||
CATCH_LOG();
|
||||
#pragma warning(pop)
|
||||
|
||||
const auto maxRow = std::max(newLastChar.Y, newCursorPos.Y);
|
||||
|
||||
const short proposedTopFromLastLine = ::base::saturated_cast<short>(maxRow - viewportSize.Y + 1);
|
||||
const short proposedTopFromScrollback = newViewportTop;
|
||||
|
||||
short proposedTop = std::max(proposedTopFromLastLine,
|
||||
proposedTopFromScrollback);
|
||||
|
||||
// If we're using the new location of the old top line to place the
|
||||
// viewport, we might need to make an adjustment to it.
|
||||
//
|
||||
// We're using the last cell of the line to calculate where the top line is
|
||||
// in the new buffer. If that line wrapped, then all the lines below it
|
||||
// shifted down in the buffer. If there's space for all those lines in the
|
||||
// conpty buffer, then the originally unwrapped top line will _still_ be in
|
||||
// the buffer. In that case, don't stick to the _end_ of the old top line,
|
||||
// instead stick to the _start_, which is one line up.
|
||||
//
|
||||
// We can know if there's space in the conpty buffer by checking if the
|
||||
// maxRow (the highest row we've written text to) is above the viewport from
|
||||
// this proposed top position.
|
||||
if (proposedTop == proposedTopFromScrollback)
|
||||
{
|
||||
const auto proposedViewFromTop = Viewport::FromDimensions({ 0, proposedTopFromScrollback }, viewportSize);
|
||||
if (maxRow < proposedViewFromTop.BottomInclusive())
|
||||
{
|
||||
if (dx < 0 && proposedTop > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto row = newTextBuffer->GetRowByOffset(::base::saturated_cast<short>(proposedTop - 1));
|
||||
if (row.GetCharRow().WasWrapForced())
|
||||
{
|
||||
proposedTop--;
|
||||
}
|
||||
}
|
||||
CATCH_LOG();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the new bottom would be higher than the last row of text, then we
|
||||
// definitely want to use the last row of text to determine where the
|
||||
// viewport should be.
|
||||
const auto proposedViewFromTop = Viewport::FromDimensions({ 0, proposedTopFromScrollback }, viewportSize);
|
||||
if (maxRow > proposedViewFromTop.BottomInclusive())
|
||||
{
|
||||
proposedTop = proposedTopFromLastLine;
|
||||
}
|
||||
|
||||
// Make sure the proposed viewport is within the bounds of the buffer.
|
||||
// First make sure the top is >=0
|
||||
proposedTop = std::max(static_cast<short>(0), proposedTop);
|
||||
|
||||
// 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.
|
||||
const auto newView = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
|
||||
const auto proposedBottom = newView.BottomExclusive();
|
||||
if (proposedBottom > bufferSize.Y)
|
||||
{
|
||||
proposedTop -= (proposedBottom - bufferSize.Y);
|
||||
proposedTop = ::base::saturated_cast<short>(proposedTop - (proposedBottom - bufferSize.Y));
|
||||
}
|
||||
|
||||
_mutableViewport = Viewport::FromDimensions({ 0, proposedTop }, viewportSize);
|
||||
|
||||
_buffer.swap(newTextBuffer);
|
||||
|
||||
_scrollOffset = 0;
|
||||
_NotifyScrollEvent();
|
||||
|
||||
|
@ -456,18 +575,15 @@ void Terminal::_WriteBuffer(const std::wstring_view& stringView)
|
|||
// Try the character again.
|
||||
i--;
|
||||
|
||||
// Mark the line we're currently on as wrapped
|
||||
// If we write the last cell of the row here, TextBuffer::Write will
|
||||
// mark this line as wrapped for us. If the next character we
|
||||
// process is a newline, the Terminal::CursorLineFeed will unmark
|
||||
// this line as wrapped.
|
||||
|
||||
// TODO: GH#780 - This should really be a _deferred_ newline. If
|
||||
// the next character to come in is a newline or a cursor
|
||||
// movement or anything, then we should _not_ wrap this line
|
||||
// here.
|
||||
//
|
||||
// This is more WriteCharsLegacy2ElectricBoogaloo work. I'm
|
||||
// leaving it like this for now - it'll break for lines that
|
||||
// _exactly_ wrap, but we can't re-wrap lines now anyways, so it
|
||||
// doesn't matter.
|
||||
_buffer->GetRowByOffset(cursorPosBefore.Y).GetCharRow().SetWrapForced(true);
|
||||
}
|
||||
|
||||
_AdjustCursorPosition(proposedCursorPosition);
|
||||
|
|
|
@ -142,6 +142,11 @@ bool Terminal::CursorLineFeed(const bool withReturn) noexcept
|
|||
try
|
||||
{
|
||||
auto cursorPos = _buffer->GetCursor().GetPosition();
|
||||
|
||||
// since we explicitly just moved down a row, clear the wrap status on the
|
||||
// row we just came from
|
||||
_buffer->GetRowByOffset(cursorPos.Y).GetCharRow().SetWrapForced(false);
|
||||
|
||||
cursorPos.Y++;
|
||||
if (withReturn)
|
||||
{
|
||||
|
|
|
@ -115,6 +115,9 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final
|
|||
auto pfn = std::bind(&ConptyRoundtripTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2);
|
||||
_pVtRenderEngine->SetTestCallback(pfn);
|
||||
|
||||
// Enable the resize quirk, as the Terminal is going to be reacting as if it's enabled.
|
||||
_pVtRenderEngine->SetResizeQuirk(true);
|
||||
|
||||
// Configure the OutputStateMachine's _pfnFlushToTerminal
|
||||
// Use OutputStateMachineEngine::SetTerminalConnection
|
||||
g.pRender->AddRenderEngine(_pVtRenderEngine.get());
|
||||
|
@ -164,6 +167,8 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final
|
|||
|
||||
TEST_METHOD(MoveCursorAtEOL);
|
||||
|
||||
TEST_METHOD(TestResizeHeight);
|
||||
|
||||
private:
|
||||
bool _writeCallback(const char* const pch, size_t const cch);
|
||||
void _flushFirstFrame();
|
||||
|
@ -504,21 +509,15 @@ void ConptyRoundtripTests::TestExactWrappingWithoutSpaces()
|
|||
hostSm.ProcessString(L"\n");
|
||||
hostSm.ProcessString(L"1234567890");
|
||||
|
||||
auto verifyBuffer = [&](const TextBuffer& tb, const bool isTerminal) {
|
||||
auto verifyBuffer = [&](const TextBuffer& tb) {
|
||||
auto& cursor = tb.GetCursor();
|
||||
// Verify the cursor wrapped to the second line
|
||||
VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y);
|
||||
VERIFY_ARE_EQUAL(10, cursor.GetPosition().X);
|
||||
|
||||
// TODO: GH#780 - In the Terminal, neither line should be wrapped.
|
||||
// Unfortunately, until WriteCharsLegacy2ElectricBoogaloo is complete,
|
||||
// the Terminal will still treat the first line as wrapped. When #780 is
|
||||
// implemented, these tests will fail, and should again expect the first
|
||||
// line to not be wrapped.
|
||||
|
||||
// Verify that we marked the 0th row as _not wrapped_
|
||||
const auto& row0 = tb.GetRowByOffset(0);
|
||||
VERIFY_ARE_EQUAL(isTerminal, row0.GetCharRow().WasWrapForced());
|
||||
VERIFY_IS_FALSE(row0.GetCharRow().WasWrapForced());
|
||||
|
||||
const auto& row1 = tb.GetRowByOffset(1);
|
||||
VERIFY_IS_FALSE(row1.GetCharRow().WasWrapForced());
|
||||
|
@ -527,7 +526,7 @@ void ConptyRoundtripTests::TestExactWrappingWithoutSpaces()
|
|||
TestUtils::VerifyExpectedString(tb, L"1234567890", { 0, 1 });
|
||||
};
|
||||
|
||||
verifyBuffer(hostTb, false);
|
||||
verifyBuffer(hostTb);
|
||||
|
||||
// First write the first 80 characters from the string
|
||||
expectedOutput.push_back(R"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop)");
|
||||
|
@ -540,7 +539,7 @@ void ConptyRoundtripTests::TestExactWrappingWithoutSpaces()
|
|||
expectedOutput.push_back("\x1b[K");
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
|
||||
verifyBuffer(termTb, true);
|
||||
verifyBuffer(termTb);
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::TestExactWrappingWithSpaces()
|
||||
|
@ -552,6 +551,7 @@ void ConptyRoundtripTests::TestExactWrappingWithSpaces()
|
|||
auto& gci = g.getConsoleInformation();
|
||||
auto& si = gci.GetActiveOutputBuffer();
|
||||
auto& hostSm = si.GetStateMachine();
|
||||
|
||||
auto& hostTb = si.GetTextBuffer();
|
||||
auto& termTb = *term->_buffer;
|
||||
const auto initialTermView = term->GetViewport();
|
||||
|
@ -573,21 +573,15 @@ void ConptyRoundtripTests::TestExactWrappingWithSpaces()
|
|||
hostSm.ProcessString(L" ");
|
||||
hostSm.ProcessString(L"1234567890");
|
||||
|
||||
auto verifyBuffer = [&](const TextBuffer& tb, const bool isTerminal) {
|
||||
auto verifyBuffer = [&](const TextBuffer& tb) {
|
||||
auto& cursor = tb.GetCursor();
|
||||
// Verify the cursor wrapped to the second line
|
||||
VERIFY_ARE_EQUAL(1, cursor.GetPosition().Y);
|
||||
VERIFY_ARE_EQUAL(20, cursor.GetPosition().X);
|
||||
|
||||
// TODO: GH#780 - In the Terminal, neither line should be wrapped.
|
||||
// Unfortunately, until WriteCharsLegacy2ElectricBoogaloo is complete,
|
||||
// the Terminal will still treat the first line as wrapped. When #780 is
|
||||
// implemented, these tests will fail, and should again expect the first
|
||||
// line to not be wrapped.
|
||||
|
||||
// Verify that we marked the 0th row as _not wrapped_
|
||||
const auto& row0 = tb.GetRowByOffset(0);
|
||||
VERIFY_ARE_EQUAL(isTerminal, row0.GetCharRow().WasWrapForced());
|
||||
VERIFY_IS_FALSE(row0.GetCharRow().WasWrapForced());
|
||||
|
||||
const auto& row1 = tb.GetRowByOffset(1);
|
||||
VERIFY_IS_FALSE(row1.GetCharRow().WasWrapForced());
|
||||
|
@ -596,7 +590,7 @@ void ConptyRoundtripTests::TestExactWrappingWithSpaces()
|
|||
TestUtils::VerifyExpectedString(tb, L" 1234567890", { 0, 1 });
|
||||
};
|
||||
|
||||
verifyBuffer(hostTb, false);
|
||||
verifyBuffer(hostTb);
|
||||
|
||||
// First write the first 80 characters from the string
|
||||
expectedOutput.push_back(R"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop)");
|
||||
|
@ -609,7 +603,7 @@ void ConptyRoundtripTests::TestExactWrappingWithSpaces()
|
|||
expectedOutput.push_back("\x1b[K");
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
|
||||
verifyBuffer(termTb, true);
|
||||
verifyBuffer(termTb);
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::MoveCursorAtEOL()
|
||||
|
@ -622,6 +616,7 @@ void ConptyRoundtripTests::MoveCursorAtEOL()
|
|||
auto& gci = g.getConsoleInformation();
|
||||
auto& si = gci.GetActiveOutputBuffer();
|
||||
auto& hostSm = si.GetStateMachine();
|
||||
|
||||
auto& hostTb = si.GetTextBuffer();
|
||||
auto& termTb = *term->_buffer;
|
||||
_flushFirstFrame();
|
||||
|
@ -675,6 +670,230 @@ void ConptyRoundtripTests::MoveCursorAtEOL()
|
|||
verifyData1(termTb);
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::TestResizeHeight()
|
||||
{
|
||||
// This test class is _60_ tests to ensure that resizing the terminal works
|
||||
// with conpty correctly. There's a lot of min/maxing in expressions here,
|
||||
// to account for the sheer number of cases here, and that we have to handle
|
||||
// both resizing larger and smaller all in one test.
|
||||
|
||||
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}")
|
||||
TEST_METHOD_PROPERTY(L"Data:printedRows", L"{1, 10, 50, 200}")
|
||||
END_TEST_METHOD_PROPERTIES()
|
||||
int dx, dy;
|
||||
int printedRows;
|
||||
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");
|
||||
VERIFY_SUCCEEDED(TestData::TryGetValue(L"printedRows", printedRows), L"Number of rows of text to print");
|
||||
|
||||
_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();
|
||||
const auto initialTerminalBufferHeight = term->GetTextBuffer().GetSize().Height();
|
||||
|
||||
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 %d lines of output, which will scroll the viewport", printedRows));
|
||||
|
||||
for (auto i = 0; i < printedRows; i++)
|
||||
{
|
||||
// This looks insane, but this expression is carefully crafted to give
|
||||
// us only printable characters, starting with `!` (0n33).
|
||||
// Similar statements are used elsewhere throughout this test.
|
||||
auto wstr = std::wstring(1, static_cast<wchar_t>((i) % 93) + 33);
|
||||
hostSm.ProcessString(wstr);
|
||||
hostSm.ProcessString(L"\r\n");
|
||||
}
|
||||
|
||||
// Conpty 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();
|
||||
// If we've printed more lines than the height of the buffer, then we're
|
||||
// expecting the viewport to have moved down. Otherwise, the terminal's
|
||||
// viewport will stay at 0,0.
|
||||
const auto expectedTerminalViewBottom = std::max(std::min(::base::saturated_cast<short>(printedRows + 1),
|
||||
term->GetBufferHeight()),
|
||||
term->GetViewport().Height());
|
||||
|
||||
VERIFY_ARE_EQUAL(expectedTerminalViewBottom, secondTermView.BottomExclusive());
|
||||
VERIFY_ARE_EQUAL(expectedTerminalViewBottom - initialTermView.Height(), secondTermView.Top());
|
||||
|
||||
auto verifyTermData = [&expectedTerminalViewBottom, &printedRows, this, &initialTerminalBufferHeight](TextBuffer& termTb, const int resizeDy = 0) {
|
||||
// Some number of lines of text were lost from the scrollback. The
|
||||
// number of lines lost will be determined by whichever of the initial
|
||||
// or current buffer is smaller.
|
||||
const auto numLostRows = std::max(0,
|
||||
printedRows - std::min(term->GetTextBuffer().GetSize().Height(), initialTerminalBufferHeight) + 1);
|
||||
|
||||
const auto rowsWithText = std::min(::base::saturated_cast<short>(printedRows),
|
||||
expectedTerminalViewBottom) -
|
||||
1 + std::min(resizeDy, 0);
|
||||
|
||||
for (short row = 0; row < rowsWithText; row++)
|
||||
{
|
||||
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
|
||||
auto iter = termTb.GetCellDataAt({ 0, row });
|
||||
const wchar_t expectedChar = static_cast<wchar_t>((row + numLostRows) % 93) + 33;
|
||||
|
||||
auto expectedString = std::wstring(1, expectedChar);
|
||||
|
||||
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, &initialHostView, &printedRows](TextBuffer& hostTb, const int resizeDy = 0) {
|
||||
const auto hostView = si.GetViewport();
|
||||
|
||||
// In the host, there are two regions we're interested in:
|
||||
|
||||
// 1. the first section of the buffer with the output in it. Before
|
||||
// we're resized, this will be filled with one character on each row.
|
||||
// 2. The second area below the first that's empty (filled with spaces).
|
||||
// Initially, this is only one row.
|
||||
// After we resize, different things will happen.
|
||||
// * If we decrease the height of the buffer, the characters in the
|
||||
// buffer will all move _up_ the same number of rows. We'll want to
|
||||
// only check the first initialView+dy rows for characters.
|
||||
// * If we increase the height, rows will be added at the bottom. We'll
|
||||
// want to check the initial viewport height for the original
|
||||
// characters, but then we'll want to look for more blank rows at the
|
||||
// bottom. The characters in the initial viewport won't have moved.
|
||||
|
||||
const short originalViewHeight = ::base::saturated_cast<short>(resizeDy < 0 ?
|
||||
initialHostView.Height() + resizeDy :
|
||||
initialHostView.Height());
|
||||
const auto rowsWithText = std::min(originalViewHeight - 1, printedRows);
|
||||
const bool scrolled = printedRows > initialHostView.Height();
|
||||
// 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>(scrolled ?
|
||||
(printedRows - originalViewHeight + 1) :
|
||||
0);
|
||||
|
||||
short row = 0;
|
||||
// Don't include the last row of the viewport in this check, since it'll
|
||||
// be blank. We'll check it in the below loop.
|
||||
for (; row < rowsWithText; row++)
|
||||
{
|
||||
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
|
||||
auto iter = hostTb.GetCellDataAt({ 0, row });
|
||||
|
||||
const auto expectedChar = static_cast<wchar_t>(((firstChar + row) % 93) + 33);
|
||||
auto expectedString = std::wstring(1, static_cast<wchar_t>(expectedChar));
|
||||
|
||||
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 that the remaining rows in the viewport are empty.
|
||||
for (; row < hostView.Height(); row++)
|
||||
{
|
||||
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
|
||||
auto iter = hostTb.GetCellDataAt({ 0, row });
|
||||
VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
|
||||
}
|
||||
};
|
||||
|
||||
verifyHostData(*hostTb);
|
||||
verifyTermData(*termTb);
|
||||
|
||||
const COORD newViewportSize{
|
||||
::base::saturated_cast<short>(TerminalViewWidth + dx),
|
||||
::base::saturated_cast<short>(TerminalViewHeight + dy)
|
||||
};
|
||||
|
||||
Log::Comment(NoThrowString().Format(L"Resize the Terminal and conpty here"));
|
||||
auto resizeResult = term->UserResize(newViewportSize);
|
||||
VERIFY_SUCCEEDED(resizeResult);
|
||||
_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 to the top of the viewport, unless dy<0,
|
||||
// rows=50. In that set of cases, we _didn't_ pin the top of the Terminal to
|
||||
// the old top, we actually shifted it down (because the output was at the
|
||||
// bottom of the window, not empty lines).
|
||||
const auto thirdTermView = term->GetViewport();
|
||||
if (dy < 0 && (printedRows > initialTermView.Height() && printedRows < initialTerminalBufferHeight))
|
||||
{
|
||||
VERIFY_ARE_EQUAL(secondTermView.Top() - dy, thirdTermView.Top());
|
||||
VERIFY_ARE_EQUAL(expectedTerminalViewBottom, thirdTermView.BottomExclusive());
|
||||
}
|
||||
else
|
||||
{
|
||||
VERIFY_ARE_EQUAL(secondTermView.Top(), thirdTermView.Top());
|
||||
VERIFY_ARE_EQUAL(expectedTerminalViewBottom + dy, thirdTermView.BottomExclusive());
|
||||
}
|
||||
|
||||
verifyHostData(*hostTb, dy);
|
||||
// Note that at this point, nothing should have changed with the Terminal.
|
||||
verifyTermData(*termTb, dy);
|
||||
|
||||
Log::Comment(NoThrowString().Format(L"Paint a frame to update the Terminal"));
|
||||
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 to the top of the viewport, unless dy<0,
|
||||
// rows=50. In that set of cases, we _didn't_ pin the top of the Terminal to
|
||||
// the old top, we actually shifted it down (because the output was at the
|
||||
// bottom of the window, not empty lines).
|
||||
const auto fourthTermView = term->GetViewport();
|
||||
if (dy < 0 && (printedRows > initialTermView.Height() && printedRows < initialTerminalBufferHeight))
|
||||
{
|
||||
VERIFY_ARE_EQUAL(secondTermView.Top() - dy, thirdTermView.Top());
|
||||
VERIFY_ARE_EQUAL(expectedTerminalViewBottom, thirdTermView.BottomExclusive());
|
||||
}
|
||||
else
|
||||
{
|
||||
VERIFY_ARE_EQUAL(secondTermView.Top(), thirdTermView.Top());
|
||||
VERIFY_ARE_EQUAL(expectedTerminalViewBottom + dy, thirdTermView.BottomExclusive());
|
||||
}
|
||||
verifyHostData(*hostTb, dy);
|
||||
verifyTermData(*termTb, dy);
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::PassthroughCursorShapeImmediately()
|
||||
{
|
||||
// This is a test for GH#4106, and more indirectly, GH #2011.
|
||||
|
@ -715,6 +934,7 @@ void ConptyRoundtripTests::PassthroughClearScrollback()
|
|||
auto& gci = g.getConsoleInformation();
|
||||
auto& si = gci.GetActiveOutputBuffer();
|
||||
auto& hostSm = si.GetStateMachine();
|
||||
|
||||
auto& termTb = *term->_buffer;
|
||||
|
||||
_flushFirstFrame();
|
||||
|
|
|
@ -18,6 +18,7 @@ const std::wstring_view ConsoleArguments::FILEPATH_LEADER_PREFIX = L"\\??\\";
|
|||
const std::wstring_view ConsoleArguments::WIDTH_ARG = L"--width";
|
||||
const std::wstring_view ConsoleArguments::HEIGHT_ARG = L"--height";
|
||||
const std::wstring_view ConsoleArguments::INHERIT_CURSOR_ARG = L"--inheritcursor";
|
||||
const std::wstring_view ConsoleArguments::RESIZE_QUIRK = L"--resizeQuirk";
|
||||
const std::wstring_view ConsoleArguments::FEATURE_ARG = L"--feature";
|
||||
const std::wstring_view ConsoleArguments::FEATURE_PTY_ARG = L"pty";
|
||||
|
||||
|
@ -479,6 +480,12 @@ void ConsoleArguments::s_ConsumeArg(_Inout_ std::vector<std::wstring>& args, _In
|
|||
s_ConsumeArg(args, i);
|
||||
hr = S_OK;
|
||||
}
|
||||
else if (arg == RESIZE_QUIRK)
|
||||
{
|
||||
_resizeQuirk = true;
|
||||
s_ConsumeArg(args, i);
|
||||
hr = S_OK;
|
||||
}
|
||||
else if (arg == CLIENT_COMMANDLINE_ARG)
|
||||
{
|
||||
// Everything after this is the explicit commandline
|
||||
|
@ -611,6 +618,10 @@ bool ConsoleArguments::GetInheritCursor() const
|
|||
{
|
||||
return _inheritCursor;
|
||||
}
|
||||
bool ConsoleArguments::IsResizeQuirkEnabled() const
|
||||
{
|
||||
return _resizeQuirk;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Tell us to use a different size than the one parsed as the size of the
|
||||
|
|
|
@ -50,6 +50,7 @@ public:
|
|||
short GetWidth() const;
|
||||
short GetHeight() const;
|
||||
bool GetInheritCursor() const;
|
||||
bool IsResizeQuirkEnabled() const;
|
||||
|
||||
void SetExpectedSize(COORD dimensions) noexcept;
|
||||
|
||||
|
@ -68,6 +69,7 @@ public:
|
|||
static const std::wstring_view WIDTH_ARG;
|
||||
static const std::wstring_view HEIGHT_ARG;
|
||||
static const std::wstring_view INHERIT_CURSOR_ARG;
|
||||
static const std::wstring_view RESIZE_QUIRK;
|
||||
static const std::wstring_view FEATURE_ARG;
|
||||
static const std::wstring_view FEATURE_PTY_ARG;
|
||||
|
||||
|
@ -100,6 +102,7 @@ private:
|
|||
_serverHandle(serverHandle),
|
||||
_signalHandle(signalHandle),
|
||||
_inheritCursor(inheritCursor),
|
||||
_resizeQuirk(false),
|
||||
_receivedEarlySizeChange{ false },
|
||||
_originalWidth{ -1 },
|
||||
_originalHeight{ -1 }
|
||||
|
@ -127,6 +130,7 @@ private:
|
|||
DWORD _serverHandle;
|
||||
DWORD _signalHandle;
|
||||
bool _inheritCursor;
|
||||
bool _resizeQuirk{ false };
|
||||
|
||||
bool _receivedEarlySizeChange;
|
||||
short _originalWidth;
|
||||
|
|
|
@ -73,6 +73,7 @@ VtIo::VtIo() :
|
|||
[[nodiscard]] HRESULT VtIo::Initialize(const ConsoleArguments* const pArgs)
|
||||
{
|
||||
_lookingForCursorPosition = pArgs->GetInheritCursor();
|
||||
_resizeQuirk = pArgs->IsResizeQuirkEnabled();
|
||||
|
||||
// If we were already given VT handles, set up the VT IO engine to use those.
|
||||
if (pArgs->InConptyMode())
|
||||
|
@ -192,6 +193,7 @@ VtIo::VtIo() :
|
|||
if (_pVtRenderEngine)
|
||||
{
|
||||
_pVtRenderEngine->SetTerminalOwner(this);
|
||||
_pVtRenderEngine->SetResizeQuirk(_resizeQuirk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -446,3 +448,19 @@ void VtIo::EnableConptyModeForTests()
|
|||
_objectsCreated = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Method Description:
|
||||
// - Returns true if the Resize Quirk is enabled. This changes the behavior of
|
||||
// conpty to _not_ InvalidateAll the entire viewport on a resize operation.
|
||||
// This is used by the Windows Terminal, because it is prepared to be
|
||||
// connected to a conpty, and handles it's own buffer specifically for a
|
||||
// conpty scenario.
|
||||
// - See also: GH#3490, #4354, #4741
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - true iff we were started with the `--resizeQuirk` flag enabled.
|
||||
bool VtIo::IsResizeQuirkEnabled() const
|
||||
{
|
||||
return _resizeQuirk;
|
||||
}
|
||||
|
|
|
@ -43,6 +43,8 @@ namespace Microsoft::Console::VirtualTerminal
|
|||
void EnableConptyModeForTests();
|
||||
#endif
|
||||
|
||||
bool IsResizeQuirkEnabled() const;
|
||||
|
||||
private:
|
||||
// After CreateIoHandlers is called, these will be invalid.
|
||||
wil::unique_hfile _hInput;
|
||||
|
@ -57,6 +59,8 @@ namespace Microsoft::Console::VirtualTerminal
|
|||
bool _lookingForCursorPosition;
|
||||
std::mutex _shutdownLock;
|
||||
|
||||
bool _resizeQuirk{ false };
|
||||
|
||||
std::unique_ptr<Microsoft::Console::Render::VtEngine> _pVtRenderEngine;
|
||||
std::unique_ptr<Microsoft::Console::VtInputThread> _pVtInputThread;
|
||||
std::unique_ptr<Microsoft::Console::PtySignalInputThread> _pPtySignalInputThread;
|
||||
|
|
|
@ -759,16 +759,16 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
|
|||
// if we're headless, not so much. However, GetMaxWindowSizeInCharacters
|
||||
// will only return the buffer size, so we can't use that to clip the arg here.
|
||||
// So only clip the requested size if we're not headless
|
||||
if (g.getConsoleInformation().IsInVtIoMode())
|
||||
{
|
||||
// SetViewportRect doesn't cause the buffer to resize. Manually resize the buffer.
|
||||
RETURN_IF_NTSTATUS_FAILED(context.ResizeScreenBuffer(Viewport::FromInclusive(Window).Dimensions(), false));
|
||||
}
|
||||
if (!g.IsHeadless())
|
||||
{
|
||||
COORD const coordMax = context.GetMaxWindowSizeInCharacters();
|
||||
RETURN_HR_IF(E_INVALIDARG, (NewWindowSize.X > coordMax.X || NewWindowSize.Y > coordMax.Y));
|
||||
}
|
||||
else if (g.getConsoleInformation().IsInVtIoMode())
|
||||
{
|
||||
// SetViewportRect doesn't cause the buffer to resize. Manually resize the buffer.
|
||||
RETURN_IF_NTSTATUS_FAILED(context.ResizeScreenBuffer(Viewport::FromInclusive(Window).Dimensions(), false));
|
||||
}
|
||||
|
||||
// Even if it's the same size, we need to post an update in case the scroll bars need to go away.
|
||||
context.SetViewport(Viewport::FromInclusive(Window), true);
|
||||
|
@ -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() && g.getConsoleInformation().GetVtIo()->IsResizeQuirkEnabled()))
|
||||
{
|
||||
WriteToScreen(context, context.GetViewport());
|
||||
}
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
|
|
@ -1414,7 +1414,9 @@ bool SCREEN_INFORMATION::IsMaximizedY() const
|
|||
// Save cursor's relative height versus the viewport
|
||||
SHORT const sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top();
|
||||
|
||||
HRESULT hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get());
|
||||
// Reflow requires a optional<short>& , which can't just be done inline with the call.
|
||||
std::optional<short> unused{ std::nullopt };
|
||||
HRESULT hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get(), std::nullopt, unused);
|
||||
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
|
@ -2176,8 +2178,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();
|
||||
|
||||
|
|
|
@ -262,7 +262,6 @@ void VtRendererTest::Xterm256TestInvalidate()
|
|||
Log::Comment(NoThrowString().Format(
|
||||
L"Make sure that invalidating all invalidates the whole viewport."));
|
||||
VERIFY_SUCCEEDED(engine->InvalidateAll());
|
||||
qExpectedInput.push_back("\x1b[2J");
|
||||
TestPaint(*engine, [&]() {
|
||||
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
|
||||
});
|
||||
|
@ -730,7 +729,6 @@ void VtRendererTest::XtermTestInvalidate()
|
|||
Log::Comment(NoThrowString().Format(
|
||||
L"Make sure that invalidating all invalidates the whole viewport."));
|
||||
VERIFY_SUCCEEDED(engine->InvalidateAll());
|
||||
qExpectedInput.push_back("\x1b[2J");
|
||||
TestPaint(*engine, [&]() {
|
||||
VERIFY_ARE_EQUAL(view, engine->_invalidRect);
|
||||
});
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PSEUDOCONSOLE_RESIZE_QUIRK (2u)
|
||||
|
||||
HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
|
||||
|
||||
HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size);
|
||||
|
|
|
@ -73,18 +73,6 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
|
|||
{
|
||||
dirtyView = Viewport::Union(dirtyView, Viewport::FromInclusive(til::at(dirty, i)));
|
||||
}
|
||||
|
||||
// This is expecting the dirty view to be the union of all dirty regions as one big
|
||||
// rectangle descrbing them all.
|
||||
if (!_resized && dirtyView == _lastViewport)
|
||||
{
|
||||
// TODO: MSFT:21096414 - This is never actually hit. We set
|
||||
// _resized=true on every frame (see VtEngine::UpdateViewport).
|
||||
// Unfortunately, not always setting _resized is not a good enough
|
||||
// solution, see that work item for a description why.
|
||||
RETURN_IF_FAILED(_ClearScreen());
|
||||
_clearedAllThisFrame = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_quickReturn)
|
||||
|
@ -254,6 +242,10 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
|
|||
_needToDisableCursor = true;
|
||||
hr = _CursorHome();
|
||||
}
|
||||
else if (_resized && _resizeQuirk)
|
||||
{
|
||||
hr = _CursorPosition(coord);
|
||||
}
|
||||
else if (coord.X == 0 && coord.Y == (_lastText.Y + 1))
|
||||
{
|
||||
// Down one line, at the start of the line.
|
||||
|
|
|
@ -298,6 +298,7 @@ CATCH_RETURN();
|
|||
{
|
||||
hr = _ResizeWindow(newView.Width(), newView.Height());
|
||||
}
|
||||
_resized = true;
|
||||
}
|
||||
|
||||
// See MSFT:19408543
|
||||
|
@ -309,39 +310,50 @@ CATCH_RETURN();
|
|||
// lead to the first _actual_ resize being suppressed.
|
||||
_suppressResizeRepaint = false;
|
||||
|
||||
if (SUCCEEDED(hr))
|
||||
if (_resizeQuirk)
|
||||
{
|
||||
// Viewport is smaller now - just update it all.
|
||||
if (oldView.Height() > newView.Height() || oldView.Width() > newView.Width())
|
||||
// GH#3490 - When the viewport width changed, don't do anything extra here.
|
||||
// If the buffer had areas that were invalid due to the resize, then the
|
||||
// buffer will have triggered it's own invalidations for what it knows is
|
||||
// invalid. Previously, we'd invalidate everything if the width changed,
|
||||
// because we couldn't be sure if lines were reflowed.
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
hr = InvalidateAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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())
|
||||
// Viewport is smaller now - just update it all.
|
||||
if (oldView.Height() > newView.Height() || 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);
|
||||
hr = InvalidateAll();
|
||||
}
|
||||
if (SUCCEEDED(hr) && oldView.Height() < newView.Height())
|
||||
else
|
||||
{
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_resized = true;
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
|
@ -487,3 +499,19 @@ void VtEngine::EndResizeRequest()
|
|||
{
|
||||
_inResizeRequest = false;
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
// - Configure the renderer for the resize quirk. This changes the behavior of
|
||||
// conpty to _not_ InvalidateAll the entire viewport on a resize operation.
|
||||
// This is used by the Windows Terminal, because it is prepared to be
|
||||
// connected to a conpty, and handles it's own buffer specifically for a
|
||||
// conpty scenario.
|
||||
// - See also: GH#3490, #4354, #4741
|
||||
// Arguments:
|
||||
// - <none>
|
||||
// Return Value:
|
||||
// - true iff we were started with the `--resizeQuirk` flag enabled.
|
||||
void VtEngine::SetResizeQuirk(const bool resizeQuirk)
|
||||
{
|
||||
_resizeQuirk = resizeQuirk;
|
||||
}
|
||||
|
|
|
@ -228,17 +228,20 @@ void RenderTracing::TraceLastText(const COORD lastTextPos) const
|
|||
void RenderTracing::TraceMoveCursor(const COORD lastTextPos, const COORD cursor) const
|
||||
{
|
||||
#ifndef UNIT_TESTING
|
||||
const auto lastTextStr = _CoordToString(lastTextPos);
|
||||
const auto lastText = lastTextStr.c_str();
|
||||
if (TraceLoggingProviderEnabled(g_hConsoleVtRendererTraceProvider, WINEVENT_LEVEL_VERBOSE, 0))
|
||||
{
|
||||
const auto lastTextStr = _CoordToString(lastTextPos);
|
||||
const auto lastText = lastTextStr.c_str();
|
||||
|
||||
const auto cursorStr = _CoordToString(cursor);
|
||||
const auto cursorPos = cursorStr.c_str();
|
||||
const auto cursorStr = _CoordToString(cursor);
|
||||
const auto cursorPos = cursorStr.c_str();
|
||||
|
||||
TraceLoggingWrite(g_hConsoleVtRendererTraceProvider,
|
||||
"VtEngine_TraceMoveCursor",
|
||||
TraceLoggingString(lastText),
|
||||
TraceLoggingString(cursorPos),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
|
||||
TraceLoggingWrite(g_hConsoleVtRendererTraceProvider,
|
||||
"VtEngine_TraceMoveCursor",
|
||||
TraceLoggingString(lastText),
|
||||
TraceLoggingString(cursorPos),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
|
||||
}
|
||||
#else
|
||||
UNREFERENCED_PARAMETER(lastTextPos);
|
||||
UNREFERENCED_PARAMETER(cursor);
|
||||
|
@ -248,11 +251,14 @@ void RenderTracing::TraceMoveCursor(const COORD lastTextPos, const COORD cursor)
|
|||
void RenderTracing::TraceWrapped() const
|
||||
{
|
||||
#ifndef UNIT_TESTING
|
||||
const auto* const msg = "Wrapped instead of \\r\\n";
|
||||
TraceLoggingWrite(g_hConsoleVtRendererTraceProvider,
|
||||
"VtEngine_TraceWrapped",
|
||||
TraceLoggingString(msg),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
|
||||
if (TraceLoggingProviderEnabled(g_hConsoleVtRendererTraceProvider, WINEVENT_LEVEL_VERBOSE, 0))
|
||||
{
|
||||
const auto* const msg = "Wrapped instead of \\r\\n";
|
||||
TraceLoggingWrite(g_hConsoleVtRendererTraceProvider,
|
||||
"VtEngine_TraceWrapped",
|
||||
TraceLoggingString(msg),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
|
||||
}
|
||||
#else
|
||||
#endif UNIT_TESTING
|
||||
}
|
||||
|
@ -260,12 +266,15 @@ void RenderTracing::TraceWrapped() const
|
|||
void RenderTracing::TracePaintCursor(const COORD coordCursor) const
|
||||
{
|
||||
#ifndef UNIT_TESTING
|
||||
const auto cursorPosString = _CoordToString(coordCursor);
|
||||
const auto cursorPos = cursorPosString.c_str();
|
||||
TraceLoggingWrite(g_hConsoleVtRendererTraceProvider,
|
||||
"VtEngine_TracePaintCursor",
|
||||
TraceLoggingString(cursorPos),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
|
||||
if (TraceLoggingProviderEnabled(g_hConsoleVtRendererTraceProvider, WINEVENT_LEVEL_VERBOSE, 0))
|
||||
{
|
||||
const auto cursorPosString = _CoordToString(coordCursor);
|
||||
const auto cursorPos = cursorPosString.c_str();
|
||||
TraceLoggingWrite(g_hConsoleVtRendererTraceProvider,
|
||||
"VtEngine_TracePaintCursor",
|
||||
TraceLoggingString(cursorPos),
|
||||
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
|
||||
}
|
||||
#else
|
||||
UNREFERENCED_PARAMETER(coordCursor);
|
||||
#endif UNIT_TESTING
|
||||
|
|
|
@ -106,6 +106,8 @@ namespace Microsoft::Console::Render
|
|||
void BeginResizeRequest();
|
||||
void EndResizeRequest();
|
||||
|
||||
void SetResizeQuirk(const bool resizeQuirk);
|
||||
|
||||
protected:
|
||||
wil::unique_hfile _hFile;
|
||||
std::string _buffer;
|
||||
|
@ -151,6 +153,8 @@ namespace Microsoft::Console::Render
|
|||
|
||||
bool _delayedEolWrap{ false };
|
||||
|
||||
bool _resizeQuirk{ false };
|
||||
|
||||
[[nodiscard]] HRESULT _Write(std::string_view const str) noexcept;
|
||||
[[nodiscard]] HRESULT _WriteFormattedString(const std::string* const pFormat, ...) noexcept;
|
||||
[[nodiscard]] HRESULT _Flush() noexcept;
|
||||
|
|
|
@ -83,15 +83,17 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken,
|
|||
RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeConhostSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT));
|
||||
|
||||
// GH4061: Ensure that the path to executable in the format is escaped so C:\Program.exe cannot collide with C:\Program Files
|
||||
const wchar_t* pwszFormat = L"\"%s\" --headless %s--width %hu --height %hu --signal 0x%x --server 0x%x";
|
||||
const wchar_t* pwszFormat = L"\"%s\" --headless %s%s--width %hu --height %hu --signal 0x%x --server 0x%x";
|
||||
// This is plenty of space to hold the formatted string
|
||||
wchar_t cmd[MAX_PATH]{};
|
||||
const BOOL bInheritCursor = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR) == PSEUDOCONSOLE_INHERIT_CURSOR;
|
||||
const BOOL bResizeQuirk = (dwFlags & PSEUDOCONSOLE_RESIZE_QUIRK) == PSEUDOCONSOLE_RESIZE_QUIRK;
|
||||
swprintf_s(cmd,
|
||||
MAX_PATH,
|
||||
pwszFormat,
|
||||
_ConsoleHostPath(),
|
||||
bInheritCursor ? L"--inheritcursor " : L"",
|
||||
bResizeQuirk ? L"--resizeQuirk " : L"",
|
||||
size.X,
|
||||
size.Y,
|
||||
signalPipeConhostSide.get(),
|
||||
|
|
|
@ -19,6 +19,11 @@ typedef struct _PseudoConsole
|
|||
// the signal pipe.
|
||||
#define PTY_SIGNAL_RESIZE_WINDOW (8u)
|
||||
|
||||
// CreatePseudoConsole Flags
|
||||
// The other flag (PSEUDOCONSOLE_INHERIT_CURSOR) is actually defined in consoleapi.h in the OS repo
|
||||
// #define PSEUDOCONSOLE_INHERIT_CURSOR (0x1)
|
||||
#define PSEUDOCONSOLE_RESIZE_QUIRK (0x2)
|
||||
|
||||
// Implementations of the various PseudoConsole functions.
|
||||
HRESULT _CreatePseudoConsole(const HANDLE hToken,
|
||||
const COORD size,
|
||||
|
|
Loading…
Reference in a new issue