mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-11 08:41:06 +00:00
Compare commits
8 Commits
dev/lhecke
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06bc6a199d | ||
|
|
240916d56e | ||
|
|
180f1b3dfd | ||
|
|
065f3610c9 | ||
|
|
c0249ef26e | ||
|
|
1a2654d291 | ||
|
|
9aec69467c | ||
|
|
b5c8c854cc |
@@ -1541,3 +1541,225 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Function Description:
|
||||
// - Reflow the contents from the old buffer into the new buffer. The new buffer
|
||||
// can have different dimensions than the old buffer. If it does, then this
|
||||
// function will attempt to maintain the logical contents of the old buffer,
|
||||
// by continuing wrapped lines onto the next line in the new buffer.
|
||||
// Arguments:
|
||||
// - oldBuffer - the text buffer to copy the contents FROM
|
||||
// - newBuffer - the text buffer to copy the contents TO
|
||||
// Return Value:
|
||||
// - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT.
|
||||
HRESULT TextBuffer::ReflowBuffer(TextBuffer& oldBuffer, TextBuffer& newBuffer)
|
||||
{
|
||||
Cursor& oldCursor = oldBuffer.GetCursor();
|
||||
Cursor& newCursor = newBuffer.GetCursor();
|
||||
// skip any drawing updates that might occur as we manipulate the new buffer
|
||||
oldCursor.StartDeferDrawing();
|
||||
newCursor.StartDeferDrawing();
|
||||
|
||||
// We need to save the old cursor position so that we can
|
||||
// place the new cursor back on the equivalent character in
|
||||
// the new buffer.
|
||||
COORD cOldCursorPos = oldCursor.GetPosition();
|
||||
COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter();
|
||||
|
||||
short const cOldRowsTotal = cOldLastChar.Y + 1;
|
||||
short const cOldColsTotal = oldBuffer.GetSize().Width();
|
||||
|
||||
COORD cNewCursorPos = { 0 };
|
||||
bool fFoundCursorPos = 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++)
|
||||
{
|
||||
// Fetch the row and its "right" which is the last printable character.
|
||||
const ROW& row = oldBuffer.GetRowByOffset(iOldRow);
|
||||
const CharRow& charRow = row.GetCharRow();
|
||||
short iRight = static_cast<short>(charRow.MeasureRight());
|
||||
|
||||
// There is a special case here. If the row has a "wrap"
|
||||
// flag on it, but the right isn't equal to the width (one
|
||||
// index past the final valid index in the row) then there
|
||||
// were a bunch trailing of spaces in the row.
|
||||
// (But the measuring functions for each row Left/Right do
|
||||
// not count spaces as "displayable" so they're not
|
||||
// included.)
|
||||
// As such, adjust the "right" to be the width of the row
|
||||
// to capture all these spaces
|
||||
if (charRow.WasWrapForced())
|
||||
{
|
||||
iRight = cOldColsTotal;
|
||||
|
||||
// And a combined special case.
|
||||
// If we wrapped off the end of the row by adding a
|
||||
// piece of padding because of a double byte LEADING
|
||||
// character, then remove one from the "right" to
|
||||
// leave this padding out of the copy process.
|
||||
if (charRow.WasDoubleBytePadded())
|
||||
{
|
||||
iRight--;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through every character in the current row (up to
|
||||
// the "right" boundary, which is one past the final valid
|
||||
// character)
|
||||
for (short iOldCol = 0; iOldCol < iRight; iOldCol++)
|
||||
{
|
||||
if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
|
||||
{
|
||||
cNewCursorPos = newCursor.GetPosition();
|
||||
fFoundCursorPos = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
|
||||
const auto glyph = row.GetCharRow().GlyphAt(iOldCol);
|
||||
const auto dbcsAttr = row.GetCharRow().DbcsAttrAt(iOldCol);
|
||||
const auto textAttr = row.GetAttrRow().GetAttrByColumn(iOldCol);
|
||||
|
||||
if (!newBuffer.InsertCharacter(glyph, dbcsAttr, textAttr))
|
||||
{
|
||||
hr = E_OUTOFMEMORY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CATCH_RETURN();
|
||||
}
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
// If we didn't have a full row to copy, insert a new
|
||||
// line into the new buffer.
|
||||
// Only do so if we were not forced to wrap. If we did
|
||||
// force a word wrap, then the existing line break was
|
||||
// only because we ran out of space.
|
||||
if (iRight < cOldColsTotal && !charRow.WasWrapForced())
|
||||
{
|
||||
if (iRight == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
|
||||
{
|
||||
cNewCursorPos = newCursor.GetPosition();
|
||||
fFoundCursorPos = true;
|
||||
}
|
||||
// Only do this if it's not the final line in the buffer.
|
||||
// On the final line, we want the cursor to sit
|
||||
// where it is done printing for the cursor
|
||||
// adjustment to follow.
|
||||
if (iOldRow < cOldRowsTotal - 1)
|
||||
{
|
||||
hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we are on the final line of the buffer, we have one more check.
|
||||
// We got into this code path because we are at the right most column of a row in the old buffer
|
||||
// that had a hard return (no wrap was forced).
|
||||
// However, as we're inserting, the old row might have just barely fit into the new buffer and
|
||||
// caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below.
|
||||
// We need to preserve the memory of the hard return at this point by inserting one additional
|
||||
// hard newline, otherwise we've lost that information.
|
||||
// We only do this when the cursor has just barely poured over onto the next line so the hard return
|
||||
// isn't covered by the soft one.
|
||||
// e.g.
|
||||
// The old line was:
|
||||
// |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a.
|
||||
// The cursor was here ^
|
||||
// And the new line will be:
|
||||
// |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end
|
||||
// | |
|
||||
// ^ and the cursor is now there.
|
||||
// If we leave it like this, we've lost the newline information.
|
||||
// So we insert one more newline so a continued reflow of this buffer by resizing larger will
|
||||
// continue to look as the original output intended with the newline data.
|
||||
// After this fix, it looks like this:
|
||||
// |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline)
|
||||
// | |
|
||||
// ^ and the cursor is now here.
|
||||
const COORD coordNewCursor = newCursor.GetPosition();
|
||||
if (coordNewCursor.X == 0 && coordNewCursor.Y > 0)
|
||||
{
|
||||
if (newBuffer.GetRowByOffset(coordNewCursor.Y - 1).GetCharRow().WasWrapForced())
|
||||
{
|
||||
hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
// Finish copying remaining parameters from the old text buffer to the new one
|
||||
newBuffer.CopyProperties(oldBuffer);
|
||||
|
||||
// If we found where to put the cursor while placing characters into the buffer,
|
||||
// just put the cursor there. Otherwise we have to advance manually.
|
||||
if (fFoundCursorPos)
|
||||
{
|
||||
newCursor.SetPosition(cNewCursorPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Advance the cursor to the same offset as before
|
||||
// get the number of newlines and spaces between the old end of text and the old cursor,
|
||||
// then advance that many newlines and chars
|
||||
int iNewlines = cOldCursorPos.Y - cOldLastChar.Y;
|
||||
int iIncrements = cOldCursorPos.X - cOldLastChar.X;
|
||||
const COORD cNewLastChar = newBuffer.GetLastNonSpaceCharacter();
|
||||
|
||||
// If the last row of the new buffer wrapped, there's going to be one less newline needed,
|
||||
// because the cursor is already on the next line
|
||||
if (newBuffer.GetRowByOffset(cNewLastChar.Y).GetCharRow().WasWrapForced())
|
||||
{
|
||||
iNewlines = std::max(iNewlines - 1, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// if this buffer didn't wrap, but the old one DID, then the d(columns) of the
|
||||
// old buffer will be one more than in this buffer, so new need one LESS.
|
||||
if (oldBuffer.GetRowByOffset(cOldLastChar.Y).GetCharRow().WasWrapForced())
|
||||
{
|
||||
iNewlines = std::max(iNewlines - 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
for (int r = 0; r < iNewlines; r++)
|
||||
{
|
||||
if (!newBuffer.NewlineCursor())
|
||||
{
|
||||
hr = E_OUTOFMEMORY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
for (int c = 0; c < iIncrements - 1; c++)
|
||||
{
|
||||
if (!newBuffer.IncrementCursor())
|
||||
{
|
||||
hr = E_OUTOFMEMORY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
// Save old cursor size before we delete it
|
||||
ULONG const ulSize = oldCursor.GetSize();
|
||||
|
||||
// Set size back to real size as it will be taking over the rendering duties.
|
||||
newCursor.SetSize(ulSize);
|
||||
}
|
||||
|
||||
newCursor.EndDeferDrawing();
|
||||
oldCursor.EndDeferDrawing();
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,8 @@ public:
|
||||
const std::wstring_view fontFaceName,
|
||||
const COLORREF backgroundColor);
|
||||
|
||||
static HRESULT ReflowBuffer(TextBuffer& oldBuffer, TextBuffer& newBuffer);
|
||||
|
||||
private:
|
||||
std::deque<ROW> _storage;
|
||||
Cursor _cursor;
|
||||
|
||||
@@ -28,6 +28,11 @@ namespace Microsoft::Terminal::Core
|
||||
class Terminal;
|
||||
}
|
||||
|
||||
// fwdecl unittest classes
|
||||
#ifdef UNIT_TESTING
|
||||
class ConptyRoundtripTests;
|
||||
#endif
|
||||
|
||||
class Microsoft::Terminal::Core::Terminal final :
|
||||
public Microsoft::Terminal::Core::ITerminalApi,
|
||||
public Microsoft::Terminal::Core::ITerminalInput,
|
||||
@@ -245,4 +250,8 @@ private:
|
||||
SMALL_RECT _GetSelectionRow(const SHORT row, const COORD higherCoord, const COORD lowerCoord) const;
|
||||
void _ExpandSelectionRow(SMALL_RECT& selectionRow) const;
|
||||
#pragma endregion
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
friend class ::ConptyRoundtripTests;
|
||||
#endif
|
||||
};
|
||||
|
||||
416
src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
Normal file
416
src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
Normal file
@@ -0,0 +1,416 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
#include <wextestclass.h>
|
||||
#include "../../inc/consoletaeftemplates.hpp"
|
||||
#include "../../types/inc/Viewport.hpp"
|
||||
#include "../../types/inc/convert.hpp"
|
||||
|
||||
#include "../renderer/inc/DummyRenderTarget.hpp"
|
||||
#include "../../renderer/base/Renderer.hpp"
|
||||
#include "../../renderer/vt/Xterm256Engine.hpp"
|
||||
#include "../../renderer/vt/XtermEngine.hpp"
|
||||
#include "../../renderer/vt/WinTelnetEngine.hpp"
|
||||
|
||||
class InputBuffer; // This for some reason needs to be fwd-decl'd
|
||||
#include "../host/inputBuffer.hpp"
|
||||
#include "../host/readDataCooked.hpp"
|
||||
#include "test/CommonState.hpp"
|
||||
|
||||
#include "../cascadia/TerminalCore/Terminal.hpp"
|
||||
|
||||
using namespace WEX::Common;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
using namespace Microsoft::Console::Types;
|
||||
using namespace Microsoft::Console::Interactivity;
|
||||
using namespace Microsoft::Console::VirtualTerminal;
|
||||
|
||||
using namespace Microsoft::Console;
|
||||
using namespace Microsoft::Console::Render;
|
||||
using namespace Microsoft::Console::Types;
|
||||
|
||||
using namespace Microsoft::Terminal::Core;
|
||||
|
||||
class ConptyRoundtripTests
|
||||
{
|
||||
TEST_CLASS(ConptyRoundtripTests);
|
||||
|
||||
TEST_CLASS_SETUP(ClassSetup)
|
||||
{
|
||||
m_state = new CommonState();
|
||||
|
||||
m_state->InitEvents();
|
||||
m_state->PrepareGlobalFont();
|
||||
m_state->PrepareGlobalScreenBuffer();
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_CLASS_CLEANUP(ClassCleanup)
|
||||
{
|
||||
m_state->CleanupGlobalScreenBuffer();
|
||||
m_state->CleanupGlobalFont();
|
||||
m_state->CleanupGlobalInputBuffer();
|
||||
|
||||
delete m_state;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD_SETUP(MethodSetup)
|
||||
{
|
||||
// STEP 1: Set up the Terminal
|
||||
term = std::make_unique<Terminal>();
|
||||
term->Create({ CommonState::s_csBufferWidth, CommonState::s_csBufferHeight }, 0, 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);
|
||||
auto& currentBuffer = gci.GetActiveOutputBuffer();
|
||||
// Make sure a test hasn't left us in the alt buffer on accident
|
||||
VERIFY_IS_FALSE(currentBuffer._IsAltBuffer());
|
||||
VERIFY_SUCCEEDED(currentBuffer.SetViewportOrigin(true, { 0, 0 }, true));
|
||||
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), currentBuffer.GetTextBuffer().GetCursor().GetPosition());
|
||||
|
||||
g.pRender = new Renderer(&gci.renderData, nullptr, 0, nullptr);
|
||||
|
||||
// Set up an xterm-256 renderer for conpty
|
||||
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
|
||||
Viewport initialViewport = currentBuffer.GetViewport();
|
||||
|
||||
_pVtRenderEngine = std::make_unique<Xterm256Engine>(std::move(hFile),
|
||||
gci,
|
||||
initialViewport,
|
||||
gci.GetColorTable(),
|
||||
static_cast<WORD>(gci.GetColorTableSize()));
|
||||
auto pfn = std::bind(&ConptyRoundtripTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2);
|
||||
_pVtRenderEngine->SetTestCallback(pfn);
|
||||
|
||||
g.pRender->AddRenderEngine(_pVtRenderEngine.get());
|
||||
gci.GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get());
|
||||
|
||||
expectedOutput.clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD_CLEANUP(MethodCleanup)
|
||||
{
|
||||
m_state->CleanupNewTextBufferInfo();
|
||||
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
delete g.pRender;
|
||||
|
||||
VERIFY_ARE_EQUAL(0u, expectedOutput.size(), L"Tests should drain all the output they push into the expected output buffer.");
|
||||
|
||||
term = nullptr;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD(ConptyOutputTestCanary);
|
||||
TEST_METHOD(SimpleWriteOutputTest);
|
||||
TEST_METHOD(WriteTwoLinesUsesNewline);
|
||||
TEST_METHOD(WriteAFewSimpleLines);
|
||||
TEST_METHOD(WriteWrappedLine);
|
||||
|
||||
private:
|
||||
bool _writeCallback(const char* const pch, size_t const cch);
|
||||
void _flushFirstFrame();
|
||||
std::deque<std::string> expectedOutput;
|
||||
std::unique_ptr<Microsoft::Console::Render::VtEngine> _pVtRenderEngine;
|
||||
CommonState* m_state;
|
||||
|
||||
DummyRenderTarget emptyRT;
|
||||
std::unique_ptr<Terminal> term;
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
// Write the string back to our Terminal
|
||||
const auto converted = ConvertToW(CP_UTF8, actualString);
|
||||
term->Write(converted);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::_flushFirstFrame()
|
||||
{
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
auto& renderer = *g.pRender;
|
||||
|
||||
expectedOutput.push_back("\x1b[2J");
|
||||
expectedOutput.push_back("\x1b[m");
|
||||
expectedOutput.push_back("\x1b[H"); // Go Home
|
||||
expectedOutput.push_back("\x1b[?25h");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::ConptyOutputTestCanary()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"This is a simple test to make sure that everything is working as expected."));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
_flushFirstFrame();
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::SimpleWriteOutputTest()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Write some simple output, and make sure it gets rendered largely "
|
||||
L"unmodified to the terminal"));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
auto& renderer = *g.pRender;
|
||||
auto& gci = g.getConsoleInformation();
|
||||
auto& si = gci.GetActiveOutputBuffer();
|
||||
auto& hostSm = si.GetStateMachine();
|
||||
auto& termTb = *term->_buffer;
|
||||
|
||||
_flushFirstFrame();
|
||||
|
||||
expectedOutput.push_back("Hello World");
|
||||
hostSm.ProcessString(L"Hello World");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
|
||||
auto iter = termTb.GetCellDataAt({ 0, 0 });
|
||||
VERIFY_ARE_EQUAL(L"H", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"e", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"l", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"l", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"o", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"W", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"o", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"r", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"l", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"d", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::WriteTwoLinesUsesNewline()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Write two lines of outout. We should use \r\n to move the cursor"));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
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;
|
||||
|
||||
_flushFirstFrame();
|
||||
|
||||
hostSm.ProcessString(L"AAA");
|
||||
hostSm.ProcessString(L"\x1b[2;1H");
|
||||
hostSm.ProcessString(L"BBB");
|
||||
|
||||
auto verifyData = [](TextBuffer& tb) {
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 0 });
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
}
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 1 });
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
}
|
||||
};
|
||||
|
||||
verifyData(hostTb);
|
||||
|
||||
expectedOutput.push_back("AAA");
|
||||
expectedOutput.push_back("\r\n");
|
||||
expectedOutput.push_back("BBB");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
|
||||
verifyData(termTb);
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::WriteAFewSimpleLines()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Write more lines of outout. We should use \r\n to move the cursor"));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
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;
|
||||
|
||||
_flushFirstFrame();
|
||||
|
||||
hostSm.ProcessString(L"AAA\n");
|
||||
hostSm.ProcessString(L"BBB\n");
|
||||
hostSm.ProcessString(L"\n");
|
||||
hostSm.ProcessString(L"CCC");
|
||||
auto verifyData = [](TextBuffer& tb) {
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 0 });
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
}
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 1 });
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
}
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 2 });
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
||||
}
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 3 });
|
||||
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
||||
}
|
||||
};
|
||||
|
||||
verifyData(hostTb);
|
||||
|
||||
expectedOutput.push_back("AAA");
|
||||
expectedOutput.push_back("\r\n");
|
||||
expectedOutput.push_back("BBB");
|
||||
expectedOutput.push_back("\r\n");
|
||||
// Here, we're going to emit 3 spaces. The region that got invalidated was a
|
||||
// rectangle from 0,0 to 3,3, so the vt renderer will try to render the
|
||||
// region in between BBB and CCC as well, because it got included in the
|
||||
// rectangle Or() operation.
|
||||
// This behavior should not be seen as binding - if a future optimization
|
||||
// breaks this test, it wouldn't be the worst.
|
||||
expectedOutput.push_back(" ");
|
||||
expectedOutput.push_back("\r\n");
|
||||
expectedOutput.push_back("CCC");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
|
||||
verifyData(termTb);
|
||||
}
|
||||
|
||||
void _verifySpanOfText(const wchar_t* const expectedChar, TextBufferCellIterator& iter, const int start, const int end)
|
||||
{
|
||||
for (int x = start; x < end; x++)
|
||||
{
|
||||
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
|
||||
if (iter->Chars() != expectedChar)
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x));
|
||||
}
|
||||
VERIFY_ARE_EQUAL(expectedChar, (iter++)->Chars());
|
||||
}
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Successfully validated %d characters were '%s'", end - start, expectedChar));
|
||||
}
|
||||
|
||||
void ConptyRoundtripTests::WriteWrappedLine()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Write more lines of outout. We should use \r\n to move the cursor"));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
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;
|
||||
const auto view = si.GetViewport();
|
||||
|
||||
_flushFirstFrame();
|
||||
|
||||
const std::wstring aWString(view.Width() - 1, L'A');
|
||||
const std::wstring bWString(view.Width() + 1, L'B');
|
||||
|
||||
hostSm.ProcessString(aWString);
|
||||
hostSm.ProcessString(L"\n");
|
||||
hostSm.ProcessString(bWString);
|
||||
hostSm.ProcessString(L"\n");
|
||||
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Ensure the buffer contains what we'd expect"));
|
||||
auto verifyData = [&view](TextBuffer& tb) {
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 0 });
|
||||
_verifySpanOfText(L"A", iter, 0, view.Width() - 1);
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars(), L"The last char of the line should be a space");
|
||||
}
|
||||
{
|
||||
// Every char in this line should be 'B'
|
||||
auto iter = tb.GetCellDataAt({ 0, 1 });
|
||||
_verifySpanOfText(L"B", iter, 0, view.Width());
|
||||
}
|
||||
{
|
||||
// Only the first char should be 'B', the rest should be blank
|
||||
auto iter = tb.GetCellDataAt({ 0, 2 });
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
_verifySpanOfText(L" ", iter, 1, view.Width());
|
||||
}
|
||||
};
|
||||
|
||||
verifyData(hostTb);
|
||||
|
||||
std::string aLine(view.Width() - 1, 'A');
|
||||
aLine += ' ';
|
||||
std::string bLine(view.Width(), 'B');
|
||||
|
||||
// First, the line of 'A's with a space at the end
|
||||
expectedOutput.push_back(aLine);
|
||||
expectedOutput.push_back("\r\n");
|
||||
// Then, the line of all 'B's
|
||||
expectedOutput.push_back(bLine);
|
||||
// No trailing newline here. Instead, onto the next line, another 'B'
|
||||
expectedOutput.push_back("B");
|
||||
// Followed by us using ECH to clear the rest of the spaces in the line.
|
||||
expectedOutput.push_back("\x1b[K");
|
||||
// and finally a newline.
|
||||
expectedOutput.push_back("\r\n");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
|
||||
// TODO:GH#780 - this test will fail until we implement WriteCharsLegacy2ElectricBoogaloo
|
||||
// verifyData(termTb);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="TerminalApiTest.cpp" />
|
||||
<ClCompile Include="ConptyRoundtripTests.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\buffer\out\lib\bufferout.vcxproj">
|
||||
@@ -38,6 +39,42 @@
|
||||
<ProjectReference Include="..\TerminalCore\lib\TerminalCore-lib.vcxproj">
|
||||
<Project>{ca5cad1a-abcd-429c-b551-8562ec954746}</Project>
|
||||
</ProjectReference>
|
||||
|
||||
<!-- The following are all Console Host (host.lib) dependencies. We're
|
||||
including them for the ConptyRoundtripTests, which instantiate a console
|
||||
host, then user the output from conpty to dump directly into a Terminal,
|
||||
and make sure the buffer contents align. -->
|
||||
|
||||
<ProjectReference Include="..\..\renderer\vt\ut_lib\vt.unittest.vcxproj">
|
||||
<Project>{990F2657-8580-4828-943F-5DD657D11843}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\renderer\base\lib\base.vcxproj">
|
||||
<Project>{af0a096a-8b3a-4949-81ef-7df8f0fee91f}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="$(OpenConsoleDir)\src\host\ut_lib\host.unittest.vcxproj">
|
||||
<Project>{06ec74cb-9a12-429c-b551-8562ec954746}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\propslib\propslib.vcxproj">
|
||||
<Project>{345fd5a4-b32b-4f29-bd1c-b033bd2c35cc}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\interactivity\base\lib\InteractivityBase.vcxproj">
|
||||
<Project>{06ec74cb-9a12-429c-b551-8562ec964846}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\interactivity\win32\lib\win32.LIB.vcxproj">
|
||||
<Project>{06ec74cb-9a12-429c-b551-8532ec964726}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\tsf\tsf.vcxproj">
|
||||
<Project>{2fd12fbb-1ddb-46d8-b818-1023c624caca}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\server\lib\server.vcxproj">
|
||||
<Project>{18d09a24-8240-42d6-8cb6-236eee820262}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\terminal\adapter\lib\adapter.vcxproj">
|
||||
<Project>{dcf55140-ef6a-4736-a403-957e4f7430bb}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\internal\internal.vcxproj">
|
||||
<Project>{ef3e32a7-5ff6-42b4-b6e2-96cd7d033f00}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="MockTermSettings.h" />
|
||||
@@ -45,7 +82,7 @@
|
||||
</ItemGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettings\Generated Files";%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettings\Generated Files";$(OpenConsoleDir)\src\host;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
@@ -55,4 +92,4 @@
|
||||
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
|
||||
<Import Project="$(SolutionDir)src\common.build.post.props" />
|
||||
<Import Project="$(SolutionDir)src\common.build.tests.props" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -15,6 +15,10 @@ Author(s):
|
||||
|
||||
#pragma once
|
||||
|
||||
// This includes a lot of common headers needed by both the host and the propsheet
|
||||
// including: windows.h, winuser, ntstatus, assert, and the DDK
|
||||
#include "HostAndPropsheetIncludes.h"
|
||||
|
||||
// This includes support libraries from the CRT, STL, WIL, and GSL
|
||||
#include "LibraryIncludes.h"
|
||||
|
||||
@@ -30,4 +34,4 @@ Author(s):
|
||||
#ifdef CON_BUILD_PUBLIC
|
||||
#define CON_USERPRIVAPI_INDIRECT
|
||||
#define CON_DPIAPI_INDIRECT
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1425,224 +1425,21 @@ bool SCREEN_INFORMATION::IsMaximizedY() const
|
||||
// Save cursor's relative height versus the viewport
|
||||
SHORT const sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top();
|
||||
|
||||
Cursor& oldCursor = _textBuffer->GetCursor();
|
||||
Cursor& newCursor = newTextBuffer->GetCursor();
|
||||
// skip any drawing updates that might occur as we manipulate the new buffer
|
||||
oldCursor.StartDeferDrawing();
|
||||
newCursor.StartDeferDrawing();
|
||||
HRESULT hr = TextBuffer::ReflowBuffer(*_textBuffer.get(), *newTextBuffer.get());
|
||||
|
||||
// We need to save the old cursor position so that we can
|
||||
// place the new cursor back on the equivalent character in
|
||||
// the new buffer.
|
||||
COORD cOldCursorPos = oldCursor.GetPosition();
|
||||
COORD cOldLastChar = _textBuffer->GetLastNonSpaceCharacter();
|
||||
|
||||
short const cOldRowsTotal = cOldLastChar.Y + 1;
|
||||
short const cOldColsTotal = GetBufferSize().Width();
|
||||
|
||||
COORD cNewCursorPos = { 0 };
|
||||
bool fFoundCursorPos = false;
|
||||
|
||||
NTSTATUS status = STATUS_SUCCESS;
|
||||
// Loop through all the rows of the old buffer and reprint them into the new buffer
|
||||
for (short iOldRow = 0; iOldRow < cOldRowsTotal; iOldRow++)
|
||||
{
|
||||
// Fetch the row and its "right" which is the last printable character.
|
||||
const ROW& Row = _textBuffer->GetRowByOffset(iOldRow);
|
||||
const CharRow& charRow = Row.GetCharRow();
|
||||
short iRight = static_cast<short>(charRow.MeasureRight());
|
||||
|
||||
// There is a special case here. If the row has a "wrap"
|
||||
// flag on it, but the right isn't equal to the width (one
|
||||
// index past the final valid index in the row) then there
|
||||
// were a bunch trailing of spaces in the row.
|
||||
// (But the measuring functions for each row Left/Right do
|
||||
// not count spaces as "displayable" so they're not
|
||||
// included.)
|
||||
// As such, adjust the "right" to be the width of the row
|
||||
// to capture all these spaces
|
||||
if (charRow.WasWrapForced())
|
||||
{
|
||||
iRight = cOldColsTotal;
|
||||
|
||||
// And a combined special case.
|
||||
// If we wrapped off the end of the row by adding a
|
||||
// piece of padding because of a double byte LEADING
|
||||
// character, then remove one from the "right" to
|
||||
// leave this padding out of the copy process.
|
||||
if (charRow.WasDoubleBytePadded())
|
||||
{
|
||||
iRight--;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through every character in the current row (up to
|
||||
// the "right" boundary, which is one past the final valid
|
||||
// character)
|
||||
for (short iOldCol = 0; iOldCol < iRight; iOldCol++)
|
||||
{
|
||||
if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
|
||||
{
|
||||
cNewCursorPos = newCursor.GetPosition();
|
||||
fFoundCursorPos = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
|
||||
const auto glyph = Row.GetCharRow().GlyphAt(iOldCol);
|
||||
const auto dbcsAttr = Row.GetCharRow().DbcsAttrAt(iOldCol);
|
||||
const auto textAttr = Row.GetAttrRow().GetAttrByColumn(iOldCol);
|
||||
|
||||
if (!newTextBuffer->InsertCharacter(glyph, dbcsAttr, textAttr))
|
||||
{
|
||||
status = STATUS_NO_MEMORY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
||||
}
|
||||
}
|
||||
if (NT_SUCCESS(status))
|
||||
{
|
||||
// If we didn't have a full row to copy, insert a new
|
||||
// line into the new buffer.
|
||||
// Only do so if we were not forced to wrap. If we did
|
||||
// force a word wrap, then the existing line break was
|
||||
// only because we ran out of space.
|
||||
if (iRight < cOldColsTotal && !charRow.WasWrapForced())
|
||||
{
|
||||
if (iRight == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
|
||||
{
|
||||
cNewCursorPos = newCursor.GetPosition();
|
||||
fFoundCursorPos = true;
|
||||
}
|
||||
// Only do this if it's not the final line in the buffer.
|
||||
// On the final line, we want the cursor to sit
|
||||
// where it is done printing for the cursor
|
||||
// adjustment to follow.
|
||||
if (iOldRow < cOldRowsTotal - 1)
|
||||
{
|
||||
status = newTextBuffer->NewlineCursor() ? status : STATUS_NO_MEMORY;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we are on the final line of the buffer, we have one more check.
|
||||
// We got into this code path because we are at the right most column of a row in the old buffer
|
||||
// that had a hard return (no wrap was forced).
|
||||
// However, as we're inserting, the old row might have just barely fit into the new buffer and
|
||||
// caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below.
|
||||
// We need to preserve the memory of the hard return at this point by inserting one additional
|
||||
// hard newline, otherwise we've lost that information.
|
||||
// We only do this when the cursor has just barely poured over onto the next line so the hard return
|
||||
// isn't covered by the soft one.
|
||||
// e.g.
|
||||
// The old line was:
|
||||
// |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a.
|
||||
// The cursor was here ^
|
||||
// And the new line will be:
|
||||
// |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end
|
||||
// | |
|
||||
// ^ and the cursor is now there.
|
||||
// If we leave it like this, we've lost the newline information.
|
||||
// So we insert one more newline so a continued reflow of this buffer by resizing larger will
|
||||
// continue to look as the original output intended with the newline data.
|
||||
// After this fix, it looks like this:
|
||||
// |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline)
|
||||
// | |
|
||||
// ^ and the cursor is now here.
|
||||
const COORD coordNewCursor = newCursor.GetPosition();
|
||||
if (coordNewCursor.X == 0 && coordNewCursor.Y > 0)
|
||||
{
|
||||
if (newTextBuffer->GetRowByOffset(coordNewCursor.Y - 1).GetCharRow().WasWrapForced())
|
||||
{
|
||||
status = newTextBuffer->NewlineCursor() ? status : STATUS_NO_MEMORY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (NT_SUCCESS(status))
|
||||
{
|
||||
// Finish copying remaining parameters from the old text buffer to the new one
|
||||
newTextBuffer->CopyProperties(*_textBuffer);
|
||||
|
||||
// If we found where to put the cursor while placing characters into the buffer,
|
||||
// just put the cursor there. Otherwise we have to advance manually.
|
||||
if (fFoundCursorPos)
|
||||
{
|
||||
newCursor.SetPosition(cNewCursorPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Advance the cursor to the same offset as before
|
||||
// get the number of newlines and spaces between the old end of text and the old cursor,
|
||||
// then advance that many newlines and chars
|
||||
int iNewlines = cOldCursorPos.Y - cOldLastChar.Y;
|
||||
int iIncrements = cOldCursorPos.X - cOldLastChar.X;
|
||||
const COORD cNewLastChar = newTextBuffer->GetLastNonSpaceCharacter();
|
||||
|
||||
// If the last row of the new buffer wrapped, there's going to be one less newline needed,
|
||||
// because the cursor is already on the next line
|
||||
if (newTextBuffer->GetRowByOffset(cNewLastChar.Y).GetCharRow().WasWrapForced())
|
||||
{
|
||||
iNewlines = std::max(iNewlines - 1, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// if this buffer didn't wrap, but the old one DID, then the d(columns) of the
|
||||
// old buffer will be one more than in this buffer, so new need one LESS.
|
||||
if (_textBuffer->GetRowByOffset(cOldLastChar.Y).GetCharRow().WasWrapForced())
|
||||
{
|
||||
iNewlines = std::max(iNewlines - 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
for (int r = 0; r < iNewlines; r++)
|
||||
{
|
||||
if (!newTextBuffer->NewlineCursor())
|
||||
{
|
||||
status = STATUS_NO_MEMORY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (NT_SUCCESS(status))
|
||||
{
|
||||
for (int c = 0; c < iIncrements - 1; c++)
|
||||
{
|
||||
if (!newTextBuffer->IncrementCursor())
|
||||
{
|
||||
status = STATUS_NO_MEMORY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (NT_SUCCESS(status))
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
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();
|
||||
COORD coordCursorHeightDiff = { 0 };
|
||||
coordCursorHeightDiff.Y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore;
|
||||
LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, true));
|
||||
|
||||
// Save old cursor size before we delete it
|
||||
ULONG const ulSize = oldCursor.GetSize();
|
||||
|
||||
_textBuffer.swap(newTextBuffer);
|
||||
|
||||
// Set size back to real size as it will be taking over the rendering duties.
|
||||
newCursor.SetSize(ulSize);
|
||||
newCursor.EndDeferDrawing();
|
||||
}
|
||||
oldCursor.EndDeferDrawing();
|
||||
|
||||
return status;
|
||||
return NTSTATUS_FROM_HRESULT(hr);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -307,5 +307,7 @@ private:
|
||||
friend class TextBufferIteratorTests;
|
||||
friend class ScreenBufferTests;
|
||||
friend class CommonState;
|
||||
friend class ConptyOutputTests;
|
||||
friend class ConptyRoundtripTests;
|
||||
#endif
|
||||
};
|
||||
|
||||
361
src/host/ut_host/ConptyOutputTests.cpp
Normal file
361
src/host/ut_host/ConptyOutputTests.cpp
Normal file
@@ -0,0 +1,361 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#include "precomp.h"
|
||||
#include <wextestclass.h>
|
||||
#include "../../inc/consoletaeftemplates.hpp"
|
||||
#include "../../types/inc/Viewport.hpp"
|
||||
|
||||
#include "../../renderer/base/Renderer.hpp"
|
||||
#include "../../renderer/vt/Xterm256Engine.hpp"
|
||||
#include "../../renderer/vt/XtermEngine.hpp"
|
||||
#include "../../renderer/vt/WinTelnetEngine.hpp"
|
||||
#include "../Settings.hpp"
|
||||
|
||||
#include "CommonState.hpp"
|
||||
|
||||
using namespace WEX::Common;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::TestExecution;
|
||||
using namespace Microsoft::Console::Types;
|
||||
using namespace Microsoft::Console::Interactivity;
|
||||
using namespace Microsoft::Console::VirtualTerminal;
|
||||
|
||||
using namespace Microsoft::Console;
|
||||
using namespace Microsoft::Console::Render;
|
||||
using namespace Microsoft::Console::Types;
|
||||
|
||||
class ConptyOutputTests
|
||||
{
|
||||
TEST_CLASS(ConptyOutputTests);
|
||||
|
||||
TEST_CLASS_SETUP(ClassSetup)
|
||||
{
|
||||
m_state = new CommonState();
|
||||
|
||||
m_state->InitEvents();
|
||||
m_state->PrepareGlobalFont();
|
||||
m_state->PrepareGlobalScreenBuffer();
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_CLASS_CLEANUP(ClassCleanup)
|
||||
{
|
||||
m_state->CleanupGlobalScreenBuffer();
|
||||
m_state->CleanupGlobalFont();
|
||||
m_state->CleanupGlobalInputBuffer();
|
||||
|
||||
delete m_state;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD_SETUP(MethodSetup)
|
||||
{
|
||||
// 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);
|
||||
auto& currentBuffer = gci.GetActiveOutputBuffer();
|
||||
// Make sure a test hasn't left us in the alt buffer on accident
|
||||
VERIFY_IS_FALSE(currentBuffer._IsAltBuffer());
|
||||
VERIFY_SUCCEEDED(currentBuffer.SetViewportOrigin(true, { 0, 0 }, true));
|
||||
VERIFY_ARE_EQUAL(COORD({ 0, 0 }), currentBuffer.GetTextBuffer().GetCursor().GetPosition());
|
||||
|
||||
g.pRender = new Renderer(&gci.renderData, nullptr, 0, nullptr);
|
||||
|
||||
// Set up an xterm-256 renderer for conpty
|
||||
wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
|
||||
Viewport initialViewport = currentBuffer.GetViewport();
|
||||
|
||||
_pVtRenderEngine = std::make_unique<Xterm256Engine>(std::move(hFile),
|
||||
gci,
|
||||
initialViewport,
|
||||
gci.GetColorTable(),
|
||||
static_cast<WORD>(gci.GetColorTableSize()));
|
||||
auto pfn = std::bind(&ConptyOutputTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2);
|
||||
_pVtRenderEngine->SetTestCallback(pfn);
|
||||
|
||||
g.pRender->AddRenderEngine(_pVtRenderEngine.get());
|
||||
gci.GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get());
|
||||
|
||||
expectedOutput.clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD_CLEANUP(MethodCleanup)
|
||||
{
|
||||
m_state->CleanupNewTextBufferInfo();
|
||||
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
delete g.pRender;
|
||||
|
||||
VERIFY_ARE_EQUAL(0u, expectedOutput.size(), L"Tests should drain all the output they push into the expected output buffer.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD(ConptyOutputTestCanary);
|
||||
TEST_METHOD(SimpleWriteOutputTest);
|
||||
TEST_METHOD(WriteTwoLinesUsesNewline);
|
||||
TEST_METHOD(WriteAFewSimpleLines);
|
||||
TEST_METHOD(WriteWrappedLine);
|
||||
|
||||
private:
|
||||
bool _writeCallback(const char* const pch, size_t const cch);
|
||||
void _flushFirstFrame();
|
||||
std::deque<std::string> expectedOutput;
|
||||
std::unique_ptr<Microsoft::Console::Render::VtEngine> _pVtRenderEngine;
|
||||
CommonState* m_state;
|
||||
};
|
||||
|
||||
bool ConptyOutputTests::_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();
|
||||
|
||||
Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", first.c_str()));
|
||||
Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", actualString.c_str()));
|
||||
|
||||
// try
|
||||
// {
|
||||
VERIFY_ARE_EQUAL(first.length(), cch);
|
||||
VERIFY_ARE_EQUAL(first, actualString);
|
||||
// }
|
||||
// catch (...)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConptyOutputTests::_flushFirstFrame()
|
||||
{
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
auto& renderer = *g.pRender;
|
||||
|
||||
expectedOutput.push_back("\x1b[2J");
|
||||
expectedOutput.push_back("\x1b[m");
|
||||
expectedOutput.push_back("\x1b[H"); // Go Home
|
||||
expectedOutput.push_back("\x1b[?25h");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
}
|
||||
|
||||
void ConptyOutputTests::ConptyOutputTestCanary()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"This is a simple test to make sure that everything is working as expected."));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
_flushFirstFrame();
|
||||
}
|
||||
|
||||
void ConptyOutputTests::SimpleWriteOutputTest()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Write some simple output, and make sure it gets rendered largely "
|
||||
L"unmodified to the terminal"));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
auto& renderer = *g.pRender;
|
||||
auto& gci = g.getConsoleInformation();
|
||||
auto& si = gci.GetActiveOutputBuffer();
|
||||
auto& sm = si.GetStateMachine();
|
||||
|
||||
_flushFirstFrame();
|
||||
|
||||
expectedOutput.push_back("Hello World");
|
||||
sm.ProcessString(L"Hello World");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
}
|
||||
|
||||
void ConptyOutputTests::WriteTwoLinesUsesNewline()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Write two lines of outout. We should use \r\n to move the cursor"));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
auto& renderer = *g.pRender;
|
||||
auto& gci = g.getConsoleInformation();
|
||||
auto& si = gci.GetActiveOutputBuffer();
|
||||
auto& sm = si.GetStateMachine();
|
||||
auto& tb = si.GetTextBuffer();
|
||||
|
||||
_flushFirstFrame();
|
||||
|
||||
sm.ProcessString(L"AAA");
|
||||
sm.ProcessString(L"\x1b[2;1H");
|
||||
sm.ProcessString(L"BBB");
|
||||
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 0 });
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
}
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 1 });
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
}
|
||||
|
||||
expectedOutput.push_back("AAA");
|
||||
expectedOutput.push_back("\r\n");
|
||||
expectedOutput.push_back("BBB");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
}
|
||||
|
||||
void ConptyOutputTests::WriteAFewSimpleLines()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Write more lines of outout. We should use \r\n to move the cursor"));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
auto& renderer = *g.pRender;
|
||||
auto& gci = g.getConsoleInformation();
|
||||
auto& si = gci.GetActiveOutputBuffer();
|
||||
auto& sm = si.GetStateMachine();
|
||||
auto& tb = si.GetTextBuffer();
|
||||
|
||||
_flushFirstFrame();
|
||||
|
||||
sm.ProcessString(L"AAA\n");
|
||||
sm.ProcessString(L"BBB\n");
|
||||
sm.ProcessString(L"\n");
|
||||
sm.ProcessString(L"CCC");
|
||||
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 0 });
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"A", (iter++)->Chars());
|
||||
}
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 1 });
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
}
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 2 });
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars());
|
||||
}
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 3 });
|
||||
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
||||
VERIFY_ARE_EQUAL(L"C", (iter++)->Chars());
|
||||
}
|
||||
|
||||
expectedOutput.push_back("AAA");
|
||||
expectedOutput.push_back("\r\n");
|
||||
expectedOutput.push_back("BBB");
|
||||
expectedOutput.push_back("\r\n");
|
||||
// Here, we're going to emit 3 spaces. The region that got invalidated was a
|
||||
// rectangle from 0,0 to 3,3, so the vt renderer will try to render the
|
||||
// region in between BBB and CCC as well, because it got included in the
|
||||
// rectangle Or() operation.
|
||||
// This behavior should not be seen as binding - if a future optimization
|
||||
// breaks this test, it wouldn't be the worst.
|
||||
expectedOutput.push_back(" ");
|
||||
expectedOutput.push_back("\r\n");
|
||||
expectedOutput.push_back("CCC");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
}
|
||||
|
||||
void _verifySpanOfText(const wchar_t* const expectedChar, TextBufferCellIterator& iter, const int start, const int end)
|
||||
{
|
||||
for (int x = start; x < end; x++)
|
||||
{
|
||||
SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
|
||||
if (iter->Chars() != expectedChar)
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x));
|
||||
}
|
||||
VERIFY_ARE_EQUAL(expectedChar, (iter++)->Chars());
|
||||
}
|
||||
}
|
||||
|
||||
void ConptyOutputTests::WriteWrappedLine()
|
||||
{
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Write more lines of outout. We should use \r\n to move the cursor"));
|
||||
VERIFY_IS_NOT_NULL(_pVtRenderEngine.get());
|
||||
|
||||
auto& g = ServiceLocator::LocateGlobals();
|
||||
auto& renderer = *g.pRender;
|
||||
auto& gci = g.getConsoleInformation();
|
||||
auto& si = gci.GetActiveOutputBuffer();
|
||||
auto& sm = si.GetStateMachine();
|
||||
auto& tb = si.GetTextBuffer();
|
||||
const auto view = si.GetViewport();
|
||||
|
||||
_flushFirstFrame();
|
||||
|
||||
const std::wstring aWString(view.Width() - 1, L'A');
|
||||
const std::wstring bWString(view.Width() + 1, L'B');
|
||||
|
||||
sm.ProcessString(aWString);
|
||||
sm.ProcessString(L"\n");
|
||||
sm.ProcessString(bWString);
|
||||
sm.ProcessString(L"\n");
|
||||
|
||||
Log::Comment(NoThrowString().Format(
|
||||
L"Ensure the buffer contains what we'd expect"));
|
||||
{
|
||||
auto iter = tb.GetCellDataAt({ 0, 0 });
|
||||
_verifySpanOfText(L"A", iter, 0, view.Width() - 1);
|
||||
VERIFY_ARE_EQUAL(L" ", (iter++)->Chars(), L"The last char of the line should be a space");
|
||||
}
|
||||
{
|
||||
// Every char in this line should be 'B'
|
||||
auto iter = tb.GetCellDataAt({ 0, 1 });
|
||||
_verifySpanOfText(L"B", iter, 0, view.Width());
|
||||
}
|
||||
{
|
||||
// Only the first char should be 'B', the rest should be blank
|
||||
auto iter = tb.GetCellDataAt({ 0, 2 });
|
||||
VERIFY_ARE_EQUAL(L"B", (iter++)->Chars());
|
||||
_verifySpanOfText(L" ", iter, 1, view.Width());
|
||||
}
|
||||
|
||||
std::string aLine(view.Width() - 1, 'A');
|
||||
aLine += ' ';
|
||||
std::string bLine(view.Width(), 'B');
|
||||
|
||||
// First, the line of 'A's with a space at the end
|
||||
expectedOutput.push_back(aLine);
|
||||
expectedOutput.push_back("\r\n");
|
||||
// Then, the line of all 'B's
|
||||
expectedOutput.push_back(bLine);
|
||||
// No trailing newline here. Instead, onto the next line, another 'B'
|
||||
expectedOutput.push_back("B");
|
||||
// Followed by us using ECH to clear the rest of the spaces in the line.
|
||||
expectedOutput.push_back("\x1b[K");
|
||||
// and finally a newline.
|
||||
expectedOutput.push_back("\r\n");
|
||||
|
||||
VERIFY_SUCCEEDED(renderer.PaintFrame());
|
||||
}
|
||||
@@ -40,6 +40,7 @@
|
||||
<ClCompile Include="ViewportTests.cpp" />
|
||||
<ClCompile Include="VtIoTests.cpp" />
|
||||
<ClCompile Include="VtRendererTests.cpp" />
|
||||
<ClCompile Include="ConptyOutputTests.cpp" />
|
||||
<Clcompile Include="..\..\types\IInputEventStreams.cpp" />
|
||||
<ClCompile Include="..\precomp.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
|
||||
@@ -149,7 +149,7 @@ class TextBufferTests
|
||||
|
||||
void TextBufferTests::TestBufferCreate()
|
||||
{
|
||||
VERIFY_SUCCESS_NTSTATUS(m_state->GetTextBufferInfoInitResult());
|
||||
VERIFY_SUCCEEDED(m_state->GetTextBufferInfoInitResult());
|
||||
}
|
||||
|
||||
TextBuffer& TextBufferTests::GetTbi()
|
||||
|
||||
@@ -36,6 +36,7 @@ SOURCES = \
|
||||
InputBufferTests.cpp \
|
||||
VtIoTests.cpp \
|
||||
VtRendererTests.cpp \
|
||||
ConptyOutputTests.cpp \
|
||||
ViewportTests.cpp \
|
||||
ConsoleArgumentsTests.cpp \
|
||||
CommandLineTests.cpp \
|
||||
|
||||
@@ -38,7 +38,7 @@ public:
|
||||
|
||||
CommonState() :
|
||||
m_heap(GetProcessHeap()),
|
||||
m_ntstatusTextBufferInfo(STATUS_FAIL_CHECK),
|
||||
m_hrTextBufferInfo(E_FAIL),
|
||||
m_pFontInfo(nullptr),
|
||||
m_backupTextBufferInfo(),
|
||||
m_readHandle(nullptr)
|
||||
@@ -143,7 +143,7 @@ public:
|
||||
gci.SetCookedReadData(nullptr);
|
||||
}
|
||||
|
||||
void PrepareNewTextBufferInfo()
|
||||
void PrepareNewTextBufferInfo(const bool useDefaultAttributes = false)
|
||||
{
|
||||
CONSOLE_INFORMATION& gci = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation();
|
||||
COORD coordScreenBufferSize;
|
||||
@@ -152,26 +152,29 @@ public:
|
||||
|
||||
UINT uiCursorSize = 12;
|
||||
|
||||
auto initialAttributes = useDefaultAttributes ? gci.GetDefaultAttributes() :
|
||||
TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY };
|
||||
|
||||
m_backupTextBufferInfo.swap(gci.pCurrentScreenBuffer->_textBuffer);
|
||||
try
|
||||
{
|
||||
std::unique_ptr<TextBuffer> textBuffer = std::make_unique<TextBuffer>(coordScreenBufferSize,
|
||||
TextAttribute{ FOREGROUND_BLUE | FOREGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY },
|
||||
initialAttributes,
|
||||
uiCursorSize,
|
||||
gci.pCurrentScreenBuffer->GetRenderTarget());
|
||||
if (textBuffer.get() == nullptr)
|
||||
{
|
||||
m_ntstatusTextBufferInfo = STATUS_NO_MEMORY;
|
||||
m_hrTextBufferInfo = E_OUTOFMEMORY;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ntstatusTextBufferInfo = STATUS_SUCCESS;
|
||||
m_hrTextBufferInfo = S_OK;
|
||||
}
|
||||
gci.pCurrentScreenBuffer->_textBuffer.swap(textBuffer);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
m_ntstatusTextBufferInfo = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
|
||||
m_hrTextBufferInfo = wil::ResultFromCaughtException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,14 +224,14 @@ public:
|
||||
textBuffer.GetCursor().SetYPosition(cRowsToFill);
|
||||
}
|
||||
|
||||
[[nodiscard]] NTSTATUS GetTextBufferInfoInitResult()
|
||||
[[nodiscard]] HRESULT GetTextBufferInfoInitResult()
|
||||
{
|
||||
return m_ntstatusTextBufferInfo;
|
||||
return m_hrTextBufferInfo;
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE m_heap;
|
||||
NTSTATUS m_ntstatusTextBufferInfo;
|
||||
HRESULT m_hrTextBufferInfo;
|
||||
FontInfo* m_pFontInfo;
|
||||
std::unique_ptr<TextBuffer> m_backupTextBufferInfo;
|
||||
std::unique_ptr<INPUT_READ_HANDLE_DATA> m_readHandle;
|
||||
|
||||
@@ -151,8 +151,12 @@ Renderer::~Renderer()
|
||||
|
||||
void Renderer::_NotifyPaintFrame()
|
||||
{
|
||||
// The thread will provide throttling for us.
|
||||
_pThread->NotifyPaint();
|
||||
// If we're running in the unittests, we might not have a render thread.
|
||||
if (_pThread)
|
||||
{
|
||||
// The thread will provide throttling for us.
|
||||
_pThread->NotifyPaint();
|
||||
}
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
|
||||
@@ -125,5 +125,9 @@ namespace Microsoft::Console::Render
|
||||
// Helper functions to diagnose issues with painting and layout.
|
||||
// These are only actually effective/on in Debug builds when the flag is set using an attached debugger.
|
||||
bool _fDebug = false;
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
friend class ConptyOutputTests;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Microsoft::Console::Render
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
friend class VtRendererTest;
|
||||
friend class ConptyOutputTests;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
|
||||
_ColorTable(ColorTable),
|
||||
_cColorTable(cColorTable),
|
||||
_fUseAsciiOnly(fUseAsciiOnly),
|
||||
_previousLineWrapped(false),
|
||||
// _previousLineWrapped(false),
|
||||
_usingUnderLine(false),
|
||||
_needToDisableCursor(false),
|
||||
_lastCursorIsVisible(false),
|
||||
@@ -248,7 +248,13 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
|
||||
|
||||
// If the previous line wrapped, then the cursor is already at this
|
||||
// position, we just don't know it yet. Don't emit anything.
|
||||
if (_previousLineWrapped)
|
||||
bool previousLineWrapped = false;
|
||||
if (_wrappedRow.has_value())
|
||||
{
|
||||
previousLineWrapped = coord.Y == _wrappedRow.value() + 1;
|
||||
}
|
||||
|
||||
if (previousLineWrapped)
|
||||
{
|
||||
hr = S_OK;
|
||||
}
|
||||
@@ -298,6 +304,9 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
|
||||
_newBottomLine = false;
|
||||
}
|
||||
_deferredCursorPos = INVALID_COORDS;
|
||||
|
||||
_wrappedRow = std::nullopt;
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ namespace Microsoft::Console::Render
|
||||
|
||||
#ifdef UNIT_TESTING
|
||||
friend class VtRendererTest;
|
||||
friend class ConptyOutputTests;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
@@ -449,6 +449,21 @@ using namespace Microsoft::Console::Types;
|
||||
std::wstring wstr = std::wstring(unclusteredString.data(), cchActual);
|
||||
RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8(wstr));
|
||||
|
||||
// If we've written text to the last column of the viewport, then mark
|
||||
// that we've wrapped this line. The next time we attempt to move the
|
||||
// cursor, if we're trying to move it to the start of the next line,
|
||||
// we'll remember that this line was wrapped, and not manually break the
|
||||
// line.
|
||||
// Don't do this is the last character we're writing is a space - The last
|
||||
// char will always be a space, but if we see that, we shouldn't wrap.
|
||||
|
||||
// TODO: This seems to be off by one char. Resizing cmd.exe, the '.' at the
|
||||
// end of the initial message sometimes gets cut off weirdly.
|
||||
if ((_lastText.X + (totalWidth - numSpaces)) > _lastViewport.RightInclusive())
|
||||
{
|
||||
_wrappedRow = coord.Y;
|
||||
}
|
||||
|
||||
// Update our internal tracker of the cursor's position.
|
||||
// See MSFT:20266233
|
||||
// If the cursor is at the rightmost column of the terminal, and we write a
|
||||
|
||||
@@ -80,8 +80,18 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe,
|
||||
#ifdef UNIT_TESTING
|
||||
if (_usingTestCallback)
|
||||
{
|
||||
RETURN_LAST_ERROR_IF(!_pfnTestCallback(str.data(), str.size()));
|
||||
return S_OK;
|
||||
// Try to get the last error. If that wasn't set, then the test probably
|
||||
// doesn't set last error. No matter. We'll just return with E_FAIL
|
||||
// then. This is a unit test, we don't particularily care.
|
||||
const auto succeeded = _pfnTestCallback(str.data(), str.size());
|
||||
auto hr = E_FAIL;
|
||||
if (!succeeded)
|
||||
{
|
||||
const auto err = ::GetLastError();
|
||||
// If there wasn't an error in GLE, just use E_FAIL
|
||||
hr = SUCCEEDED_WIN32(err) ? hr : HRESULT_FROM_WIN32(err);
|
||||
}
|
||||
return succeeded ? S_OK : hr;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ Author(s):
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
// fwdecl unittest classes
|
||||
#ifdef UNIT_TESTING
|
||||
class ConptyRoundtripTests;
|
||||
#endif
|
||||
|
||||
namespace Microsoft::Console::Render
|
||||
{
|
||||
class VtEngine : public RenderEngineBase, public Microsoft::Console::ITerminalOutputConnection
|
||||
@@ -136,6 +141,8 @@ namespace Microsoft::Console::Render
|
||||
Microsoft::Console::VirtualTerminal::RenderTracing _trace;
|
||||
bool _inResizeRequest{ false };
|
||||
|
||||
std::optional<short> _wrappedRow{ std::nullopt };
|
||||
|
||||
[[nodiscard]] HRESULT _Write(std::string_view const str) noexcept;
|
||||
[[nodiscard]] HRESULT _WriteFormattedString(const std::string* const pFormat, ...) noexcept;
|
||||
[[nodiscard]] HRESULT _Flush() noexcept;
|
||||
@@ -220,6 +227,8 @@ namespace Microsoft::Console::Render
|
||||
bool _usingTestCallback;
|
||||
|
||||
friend class VtRendererTest;
|
||||
friend class ConptyOutputTests;
|
||||
friend class ::ConptyRoundtripTests;
|
||||
#endif
|
||||
|
||||
void SetTestCallback(_In_ std::function<bool(const char* const, size_t const)> pfn);
|
||||
|
||||
Reference in New Issue
Block a user