Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manually copy trailing attributes on a resize #12637

Merged
25 commits merged into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1f6210d
I think this is a fix for #32
zadjii-msft Mar 2, 2022
966fa7b
another comment
zadjii-msft Mar 2, 2022
65638cc
cleanup, an edge case too
zadjii-msft Mar 2, 2022
4388502
wat
zadjii-msft Mar 2, 2022
dba1e98
start working on a test
zadjii-msft Mar 7, 2022
4e37693
okay I think the test is ready
zadjii-msft Mar 7, 2022
37837ca
Fix persisting attrs below the last line of the buffer too
zadjii-msft Mar 7, 2022
3e3d345
annother test
zadjii-msft Mar 7, 2022
de062ab
another test
zadjii-msft Mar 7, 2022
6843fc7
account for line rendition too
zadjii-msft Mar 7, 2022
e62fa78
spel
zadjii-msft Mar 8, 2022
cd8b21a
Merge remote-tracking branch 'origin/main' into dev/migrie/b/32-attem…
zadjii-msft Mar 15, 2022
59b16d3
nits because I can't type on this space keyboard
zadjii-msft Mar 15, 2022
2004916
try using iterators. Performance seems very bad
zadjii-msft Mar 15, 2022
fd1eda2
this seems like it fixes it but the core UTs are broken. Are they fin…
zadjii-msft Mar 15, 2022
b38b0e8
dustin was the murderer
zadjii-msft Mar 15, 2022
998916d
guess what, this did work
zadjii-msft Mar 15, 2022
97e7f95
Revert "dustin was the murderer"
zadjii-msft Mar 16, 2022
56e5679
this was the file I meant to check in
zadjii-msft Mar 16, 2022
c82a51b
Merge branch 'dev/migrie/b/32-attempt-3' into dev/migrie/b/32-attempt-2
zadjii-msft Mar 16, 2022
61b0697
https://i.kym-cdn.com/entries/icons/facebook/000/006/077/so_good.jpg
zadjii-msft Mar 16, 2022
07e854f
Update build/pipelines/templates/build-console-steps.yml
zadjii-msft Mar 16, 2022
8fa3a79
Merge remote-tracking branch 'origin/main' into dev/migrie/b/32-attem…
zadjii-msft Mar 17, 2022
447e309
typo
zadjii-msft Mar 17, 2022
58e6b9a
Merge remote-tracking branch 'origin/main' into dev/migrie/b/32-attem…
DHowett Mar 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 91 additions & 2 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2197,7 +2197,8 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
bool foundOldVisible = 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++)
short iOldRow = 0;
for (; iOldRow < cOldRowsTotal; iOldRow++)
{
// Fetch the row and its "right" which is the last printable character.
const ROW& row = oldBuffer.GetRowByOffset(iOldRow);
Expand Down Expand Up @@ -2241,7 +2242,8 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
// 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++)
short iOldCol = 0;
for (; iOldCol < iRight; iOldCol++)
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
{
if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y)
{
Expand All @@ -2265,6 +2267,48 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
CATCH_RETURN();
}

// GH#32: Copy the attributes from the rest of the row into this new buffer.
// From where we are in the old buffer, to the end of the row, copy the
// remaining attributes.
// - if the old buffer is smaller than the new buffer, then just copy
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// what we have. as it was. The last attr in the row will be extended
// to the end of the row in the new buffer.
// - if the old buffer is BIGGER, than we might have wrapped onto a new
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// line. Use the cursor's position's Y so that we know where the new
// row is, and start writing st the cursor position. Again, the attr
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// in the last column of the old row will be extended to the end of the
// row that the text was flowed onto.
// - if we the text in the old buffer didn't actually fill the whole
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
// line in the new buffer, then we didn't wrap. That's fine. just
// copy attributes from the old row till the end of the new row, and
// move on.
const auto newRowY = newCursor.GetPosition().Y;
auto& newRow = newBuffer.GetRowByOffset(newRowY);
auto newAttrColumn = newCursor.GetPosition().X;
const auto newWidth = newBuffer.GetLineWidth(newRowY);
// Stop when we get to the end of the buffer width, or the new position
// for inserting an attr would be past the right of the new buffer.
for (short copyAttrCol = iOldCol;
copyAttrCol < cOldColsTotal && newAttrColumn < newWidth;
copyAttrCol++)
{
try
{
// TODO: MSFT: 19446208 - this should just use an iterator and the inserter...
const auto textAttr = row.GetAttrRow().GetAttrByColumn(copyAttrCol);
if (!newRow.GetAttrRow().SetAttrToEnd(newAttrColumn, textAttr))
{
break;
}
}
CATCH_LOG(); // Not worth dying over.

newAttrColumn++;
}
// FOR DISCUSSION: Maybe also try to leave a
// default-attributes-to-the-end attr here, for the case where the line
// had color, then flowed onto the subsequent row.
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved

// 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).
Expand Down Expand Up @@ -2349,6 +2393,51 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer,
}
}
}

// Finish copying buffer attributes to remaining rows below the last
// printable character. This is to fix the `color 2f` scenario, where you
// change the buffer colors then resize and everything below the past
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// change the buffer colors then resize and everything below the past
// change the buffer colors then resize and everything below the last

Copy link
Member

Choose a reason for hiding this comment

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

this comment suggests we'll have to inbox this change to fix COLOR

Copy link
Member Author

Choose a reason for hiding this comment

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

yea, we would.

// printable char gets reset. See GH #12567
auto newRowY = newCursor.GetPosition().Y + 1;
const auto newHeight = newBuffer.GetSize().Height();
const auto oldHeight = oldBuffer.GetSize().Height();
for (;
iOldRow < oldHeight && newRowY < newHeight;
iOldRow++)
{
const ROW& row = oldBuffer.GetRowByOffset(iOldRow);

// Loop through every character in the current row (up to
// the "right" boundary, which is one past the final valid
// character)

auto& newRow = newBuffer.GetRowByOffset(newRowY);
const auto newWidth = newBuffer.GetLineWidth(newRowY);
const auto minWidth = std::min(oldBuffer.GetLineWidth(iOldRow), newWidth);
uint16_t newAttrColumn = 0u;
// Stop when we get to the end of the buffer width, or the new position
// for inserting an attr would be past the right of the new buffer.
for (short copyAttrCol = 0;
copyAttrCol < minWidth;
copyAttrCol++)
{
try
{
// TODO: MSFT: 19446208 - this should just use an iterator and the inserter.
// May even just be faster to just copy the ATTR_ROW to the new row, then ATTR_ROW::Resize.
const auto textAttr = row.GetAttrRow().GetAttrByColumn(copyAttrCol);
if (!newRow.GetAttrRow().SetAttrToEnd(newAttrColumn, textAttr))
{
break;
}
}
CATCH_LOG(); // Not worth dying over.

newAttrColumn++;
}
newRowY++;
}

if (SUCCEEDED(hr))
{
// Finish copying remaining parameters from the old text buffer to the new one
Expand Down
245 changes: 245 additions & 0 deletions src/host/ut_host/ScreenBufferTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include "../../inc/conattrs.hpp"
#include "../../types/inc/Viewport.hpp"

#include "../../cascadia/UnitTests_TerminalCore/TestUtils.h"

#include <sstream>

using namespace WEX::Common;
Expand All @@ -26,6 +28,7 @@ using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::Interactivity;
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::VirtualTerminal;
using namespace TerminalCoreUnitTests;

class ScreenBufferTests
{
Expand Down Expand Up @@ -222,6 +225,10 @@ class ScreenBufferTests
TEST_METHOD(RetainHorizontalOffsetWhenMovingToBottom);

TEST_METHOD(TestWriteConsoleVTQuirkMode);

TEST_METHOD(TestReflowEndOfLineColor);
TEST_METHOD(TestReflowSmallerLongLineWithColor);
TEST_METHOD(TestReflowBiggerLongLineWithColor);
};

void ScreenBufferTests::SingleAlternateBufferCreationTest()
Expand Down Expand Up @@ -6263,3 +6270,241 @@ void ScreenBufferTests::TestWriteConsoleVTQuirkMode()
verifyLastAttribute(vtWhiteOnBlack256Attribute);
}
}

void ScreenBufferTests::TestReflowEndOfLineColor()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:dx", L"{-1, 0, 1}")
// TEST_METHOD_PROPERTY(L"Data:dx", L"{1}")
TEST_METHOD_PROPERTY(L"Data:dy", L"{-1, 0, 1}")
// TEST_METHOD_PROPERTY(L"Data:dy", L"{0}")
END_TEST_METHOD_PROPERTIES();

INIT_TEST_PROPERTY(int, dx, L"The change in width of the buffer");
INIT_TEST_PROPERTY(int, dy, L"The change in height of the buffer");

auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();

gci.SetWrapText(true);

auto defaultAttrs = si.GetAttributes();
auto red = defaultAttrs;
red.SetIndexedBackground(TextColor::DARK_RED);
auto green = defaultAttrs;
green.SetIndexedBackground(TextColor::DARK_GREEN);
auto blue = defaultAttrs;
blue.SetIndexedBackground(TextColor::DARK_BLUE);
auto yellow = defaultAttrs;
yellow.SetIndexedBackground(TextColor::DARK_YELLOW);

Log::Comment(L"Make sure the viewport is at 0,0");
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));

Log::Comment(L"Fill buffer with some data");
stateMachine.ProcessString(L"\x1b[H");
stateMachine.ProcessString(L"\x1b[41m"); // Red BG
stateMachine.ProcessString(L"AAAAA"); // AAAAA
stateMachine.ProcessString(L"\x1b[42m"); // Green BG
stateMachine.ProcessString(L"\nBBBBB\n"); // BBBBB
stateMachine.ProcessString(L"\x1b[44m"); // Blue BG
stateMachine.ProcessString(L" CCC \n"); // " abc " (with spaces on either side)
stateMachine.ProcessString(L"\x1b[43m"); // yellow BG
stateMachine.ProcessString(L"\x1b[K"); // Erase line
stateMachine.ProcessString(L"\x1b[2;6H"); // move the cursor to the end of the BBBBB's

auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& /*viewport*/, const bool /*before*/) {
const short width = tb.GetSize().Width();
Log::Comment(NoThrowString().Format(L"Buffer width: %d", width));

auto iter0 = TestUtils::VerifyLineContains(tb, { 0, 0 }, L'A', red, 5u);
TestUtils::VerifyLineContains(iter0, L' ', defaultAttrs, static_cast<size_t>(width - 5));

auto iter1 = tb.GetCellLineDataAt({ 0, 1 });
TestUtils::VerifyLineContains(iter1, L'B', green, 5u);
TestUtils::VerifyLineContains(iter1, L' ', defaultAttrs, static_cast<size_t>(width - 5));

auto iter2 = tb.GetCellLineDataAt({ 0, 2 });
TestUtils::VerifyLineContains(iter2, L' ', blue, 1u);
TestUtils::VerifyLineContains(iter2, L'C', blue, 3u);
TestUtils::VerifyLineContains(iter2, L' ', blue, 1u);
TestUtils::VerifyLineContains(iter2, L' ', defaultAttrs, static_cast<size_t>(width - 5));

auto iter3 = tb.GetCellLineDataAt({ 0, 3 });
TestUtils::VerifyLineContains(iter3, L' ', yellow, static_cast<size_t>(width));
};

Log::Comment(L"========== Checking the buffer state (before) ==========");
verifyBuffer(si.GetTextBuffer(), til::rect{ si.GetViewport().ToInclusive() }, true);

Log::Comment(L"========== resize buffer ==========");
const til::point delta{ dx, dy };
const til::point oldSize{ si.GetBufferSize().Dimensions() };
const til::point newSize{ oldSize + delta };
VERIFY_SUCCEEDED(si.ResizeWithReflow(newSize.to_win32_coord()));

Log::Comment(L"========== Checking the buffer state (after) ==========");
verifyBuffer(si.GetTextBuffer(), til::rect{ si.GetViewport().ToInclusive() }, false);
}

void ScreenBufferTests::TestReflowSmallerLongLineWithColor()
{
Log::Comment(L"Reflow the buffer such that a long, line of text flows onto "
L"the next line. Check that the trailing attributes were copied"
L" to the new row.");

auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();

gci.SetWrapText(true);

auto defaultAttrs = si.GetAttributes();
auto red = defaultAttrs;
red.SetIndexedBackground(TextColor::DARK_RED);
auto green = defaultAttrs;
green.SetIndexedBackground(TextColor::DARK_GREEN);
auto blue = defaultAttrs;
blue.SetIndexedBackground(TextColor::DARK_BLUE);
auto yellow = defaultAttrs;
yellow.SetIndexedBackground(TextColor::DARK_YELLOW);

Log::Comment(L"Make sure the viewport is at 0,0");
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));

Log::Comment(L"Fill buffer with some data");
stateMachine.ProcessString(L"\x1b[H");
stateMachine.ProcessString(L"\x1b[41m"); // Red BG
stateMachine.ProcessString(L"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // 35 A's
stateMachine.ProcessString(L"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // 35 more, 70 total
stateMachine.ProcessString(L"\x1b[42m"); // Green BG
stateMachine.ProcessString(L" BBB \n"); // " BBB " with spaces on either side).
// Attributes fill 70 red, 5 green, the last 5 are {whatever it was before}

auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& /*viewport*/, const bool before) {
const short width = tb.GetSize().Width();
Log::Comment(NoThrowString().Format(L"Buffer width: %d", width));

if (before)
{
auto iter0 = TestUtils::VerifyLineContains(tb, { 0, 0 }, L'A', red, 70u);
TestUtils::VerifyLineContains(iter0, L' ', green, 1u);
TestUtils::VerifyLineContains(iter0, L'B', green, 3u);
TestUtils::VerifyLineContains(iter0, L' ', green, 1u);
TestUtils::VerifyLineContains(iter0, L' ', defaultAttrs, 5u);
}
else
{
auto iter0 = TestUtils::VerifyLineContains(tb, { 0, 0 }, L'A', red, 65u);

auto iter1 = tb.GetCellLineDataAt({ 0, 1 });
TestUtils::VerifyLineContains(iter1, L'A', red, 5u);
TestUtils::VerifyLineContains(iter1, L' ', green, 1u);
TestUtils::VerifyLineContains(iter1, L'B', green, 3u);
TestUtils::VerifyLineContains(iter1, L' ', green, 1u);

// We don't want to necessarily verify the contents of the rest of the
// line, but we will anyways. Right now, we expect the last attrs on the
// original row to fill the remainder of the row it flowed onto. We may
// want to change that in the future. If we do, this check can always be
// changed.
TestUtils::VerifyLineContains(iter1, L' ', defaultAttrs, static_cast<size_t>(width - 10));
}
};

Log::Comment(L"========== Checking the buffer state (before) ==========");
verifyBuffer(si.GetTextBuffer(), til::rect{ si.GetViewport().ToInclusive() }, true);

Log::Comment(L"========== resize buffer ==========");
const til::point delta{ -15, 0 };
const til::point oldSize{ si.GetBufferSize().Dimensions() };
const til::point newSize{ oldSize + delta };
VERIFY_SUCCEEDED(si.ResizeWithReflow(newSize.to_win32_coord()));

// Buffer is now 65 wide. 65 A's that wrapped onto the next row, where there
// are also 3 B's wrapped in spaces.
Log::Comment(L"========== Checking the buffer state (after) ==========");
verifyBuffer(si.GetTextBuffer(), til::rect{ si.GetViewport().ToInclusive() }, false);
}

void ScreenBufferTests::TestReflowBiggerLongLineWithColor()
{
Log::Comment(L"Reflow the buffer such that a wrapped line of text 'de-flows' onto the previous line.");

auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& stateMachine = si.GetStateMachine();

gci.SetWrapText(true);

auto defaultAttrs = si.GetAttributes();
auto red = defaultAttrs;
red.SetIndexedBackground(TextColor::DARK_RED);
auto green = defaultAttrs;
green.SetIndexedBackground(TextColor::DARK_GREEN);
auto blue = defaultAttrs;
blue.SetIndexedBackground(TextColor::DARK_BLUE);
auto yellow = defaultAttrs;
yellow.SetIndexedBackground(TextColor::DARK_YELLOW);

Log::Comment(L"Make sure the viewport is at 0,0");
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true));

Log::Comment(L"Fill buffer with some data");
stateMachine.ProcessString(L"\x1b[H");
stateMachine.ProcessString(L"\x1b[41m"); // Red BG
stateMachine.ProcessString(L"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // 40 A's
stateMachine.ProcessString(L"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // 40 more, to wrap
stateMachine.ProcessString(L"AAAAA"); // 5 more. 85 total.
stateMachine.ProcessString(L"\x1b[42m"); // Green BG
stateMachine.ProcessString(L" BBB \n"); // " BBB " with spaces on either side).
// Attributes fill 85 red, 5 green, the rest are {whatever it was before}

auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& /*viewport*/, const bool before) {
const short width = tb.GetSize().Width();
Log::Comment(NoThrowString().Format(L"Buffer width: %d", width));

if (before)
{
auto iter0 = TestUtils::VerifyLineContains(tb, { 0, 0 }, L'A', red, 80u);

auto iter1 = tb.GetCellLineDataAt({ 0, 1 });
TestUtils::VerifyLineContains(iter1, L'A', red, 5u);
TestUtils::VerifyLineContains(iter1, L' ', green, 1u);
TestUtils::VerifyLineContains(iter1, L'B', green, 3u);
TestUtils::VerifyLineContains(iter1, L' ', green, 1u);
TestUtils::VerifyLineContains(iter1, L' ', defaultAttrs, static_cast<size_t>(width - 10));
}
else
{
auto iter0 = TestUtils::VerifyLineContains(tb, { 0, 0 }, L'A', red, 85u);
TestUtils::VerifyLineContains(iter0, L' ', green, 1u);
TestUtils::VerifyLineContains(iter0, L'B', green, 3u);
TestUtils::VerifyLineContains(iter0, L' ', green, 1u);

// We don't want to necessarily verify the contents of the rest of
// the line, but we will anyways. Differently than
// TestReflowSmallerLongLineWithColor: In this test, the since we're
// de-flowing row 1 onto row 0, and the trailing spaces in row 1 are
// default attrs, then that's the attrs we'll use to finish filling
// up the 0th row in the new buffer. Again, we may want to change
// that in the future. If we do, this check can always be changed.
TestUtils::VerifyLineContains(iter0, L' ', defaultAttrs, static_cast<size_t>(width - 90));
}
};

Log::Comment(L"========== Checking the buffer state (before) ==========");
verifyBuffer(si.GetTextBuffer(), til::rect{ si.GetViewport().ToInclusive() }, true);

Log::Comment(L"========== resize buffer ==========");
const til::point delta{ 15, 0 };
const til::point oldSize{ si.GetBufferSize().Dimensions() };
const til::point newSize{ oldSize + delta };
VERIFY_SUCCEEDED(si.ResizeWithReflow(newSize.to_win32_coord()));

// Buffer is now 95 wide. 85 A's that de-flowed onto the first row, where
// there are also 3 B's wrapped in spaces, and finally 5 trailing spaces.
Log::Comment(L"========== Checking the buffer state (after) ==========");
verifyBuffer(si.GetTextBuffer(), til::rect{ si.GetViewport().ToInclusive() }, false);
}
Copy link
Member

Choose a reason for hiding this comment

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

Do we need a test for the "copied the attributes after the last printable character" case?

Copy link
Member Author

Choose a reason for hiding this comment

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

meh, I could write one

Copy link
Member

Choose a reason for hiding this comment

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

I'll sign, but I would like to see that ^