Skip to content

Commit

Permalink
Prevent horizontally scrolling wide chars erasing themselves (#14650)
Browse files Browse the repository at this point in the history
When the buffer contains wide characters that occupy more than one cell,
and those cells are scrolled horizontally by exactly one column, that
operation can result in the wide characters being completely erased.
This PR attempts to fix that bug, although it's not an ideal long term
solution.

Although not really to blame, it was PR #13626 that exposed this issue.

The root of the problem is that scrolling operations copy cells one by
one, but wide characters are written to the buffer two cells at a time.
So when you move a wide character one position to the left or right, it
can overwrite itself before it's finished copying, and the end result is
the whole character gets erased.

I've attempt to solve this by getting the affected operations to read
two cells in advance before they start writing, so there's no risk of
losing the source data before it's fully output. This may not work in
the long term, with characters wider than two cells, but it should at
least be good enough for now.

I've also changed the `TextBuffer::Write` call to a `WriteLine` call to
improve the handling of a wide character on the end of the line, where
moving it right by one column would place it half off screen. It should
just be dropped, but with the `Write` method, it would end up pushed
onto the following line.

## Validation Steps Performed

I've manually confirmed this fixes all the test cases described in
#14626, and also added some unit tests that replicate those scenarios.

Closes #14626
  • Loading branch information
j4james authored Jan 13, 2023
1 parent 45a36cf commit fb485a2
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 8 deletions.
10 changes: 7 additions & 3 deletions src/host/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,16 @@ static void _CopyRectangle(SCREEN_INFORMATION& screenInfo,
auto sourcePos = source.GetWalkOrigin(walkDirection);
auto targetPos = target.GetWalkOrigin(walkDirection);

// Note that we read two cells from the source before we start writing
// to the target, so a two-cell DBCS character can't accidentally delete
// itself when moving one cell horizontally.
auto next = OutputCell(*screenInfo.GetCellDataAt(sourcePos));
do
{
const auto data = OutputCell(*screenInfo.GetCellDataAt(sourcePos));
screenInfo.Write(OutputCellIterator({ &data, 1 }), targetPos);

const auto current = next;
source.WalkInBounds(sourcePos, walkDirection);
next = OutputCell(*screenInfo.GetCellDataAt(sourcePos));
screenInfo.GetTextBuffer().WriteLine(OutputCellIterator({ &current, 1 }), targetPos);
} while (target.WalkInBounds(targetPos, walkDirection));
}
}
Expand Down
50 changes: 50 additions & 0 deletions src/host/ut_host/ScreenBufferTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "input.h"
#include "getset.h"
#include "_stream.h" // For WriteCharsLegacy
#include "output.h" // For ScrollRegion

#include "../interactivity/inc/ServiceLocator.hpp"
#include "../../inc/conattrs.hpp"
Expand Down Expand Up @@ -183,6 +184,7 @@ class ScreenBufferTests
TEST_METHOD(ScrollOperations);
TEST_METHOD(InsertChars);
TEST_METHOD(DeleteChars);
TEST_METHOD(ScrollingWideCharsHorizontally);

TEST_METHOD(EraseScrollbackTests);
TEST_METHOD(EraseTests);
Expand Down Expand Up @@ -4088,6 +4090,54 @@ void ScreenBufferTests::DeleteChars()
L"A whole line of spaces was inserted from the right, erasing the line.");
}

void ScreenBufferTests::ScrollingWideCharsHorizontally()
{
// The point of this test is to make sure wide characters can be
// moved horizontally by one cell without erasing themselves.

auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);

const auto viewport = si.GetViewport();
const auto testRow = viewport.Top();

Log::Comment(L"Fill the test row with content containing wide chars");
const auto testChars = L"こんにちは World";
const auto testAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
_FillLine(testRow, testChars, testAttr);

Log::Comment(L"Position the cursor at the start of the test row");
VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, testRow }, true));

Log::Comment(L"Insert 1 cell at the start of the test row");
stateMachine.ProcessString(L"\033[@");
VERIFY_IS_TRUE(_ValidateLineContains({ 1, testRow }, testChars, testAttr));

Log::Comment(L"Delete 1 cell from the start of the test row");
stateMachine.ProcessString(L"\033[P");
VERIFY_IS_TRUE(_ValidateLineContains(testRow, testChars, testAttr));

Log::Comment(L"Copy the test row 1 cell to the right");
stateMachine.ProcessString(L"\033[1;1;1;;;1;2$v");
VERIFY_IS_TRUE(_ValidateLineContains({ 1, testRow }, testChars, testAttr));

Log::Comment(L"Copy the test row 1 cell to the left");
stateMachine.ProcessString(L"\033[1;2;1;;;1;1$v");
VERIFY_IS_TRUE(_ValidateLineContains(testRow, testChars, testAttr));

Log::Comment(L"Scroll the test row 1 cell to the right");
const auto testRect = til::inclusive_rect{ 0, testRow, viewport.Width() - 2, testRow };
ScrollRegion(si, testRect, std::nullopt, { 1, testRow }, L' ', testAttr);
VERIFY_IS_TRUE(_ValidateLineContains({ 1, testRow }, testChars, testAttr));

Log::Comment(L"Scroll the test row 1 cell to the left");
const auto testRect2 = til::inclusive_rect{ 1, testRow, viewport.Width() - 1, testRow };
ScrollRegion(si, testRect2, std::nullopt, { 0, testRow }, L' ', testAttr);
VERIFY_IS_TRUE(_ValidateLineContains(testRow, testChars, testAttr));
}

void ScreenBufferTests::EraseScrollbackTests()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
Expand Down
20 changes: 15 additions & 5 deletions src/terminal/adapter/adaptDispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -466,11 +466,16 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r
const auto walkDirection = Viewport::DetermineWalkDirection(source, target);
auto sourcePos = source.GetWalkOrigin(walkDirection);
auto targetPos = target.GetWalkOrigin(walkDirection);
// Note that we read two cells from the source before we start writing
// to the target, so a two-cell DBCS character can't accidentally delete
// itself when moving one cell horizontally.
auto next = OutputCell(*textBuffer.GetCellDataAt(sourcePos));
do
{
const auto data = OutputCell(*textBuffer.GetCellDataAt(sourcePos));
textBuffer.Write(OutputCellIterator({ &data, 1 }), targetPos);
const auto current = next;
source.WalkInBounds(sourcePos, walkDirection);
next = OutputCell(*textBuffer.GetCellDataAt(sourcePos));
textBuffer.WriteLine(OutputCellIterator({ &current, 1 }), targetPos);
} while (target.WalkInBounds(targetPos, walkDirection));
}

Expand Down Expand Up @@ -1017,16 +1022,21 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const
const auto walkDirection = Viewport::DetermineWalkDirection(srcView, dstView);
auto srcPos = srcView.GetWalkOrigin(walkDirection);
auto dstPos = dstView.GetWalkOrigin(walkDirection);
// Note that we read two cells from the source before we start writing
// to the target, so a two-cell DBCS character can't accidentally delete
// itself when moving one cell horizontally.
auto next = OutputCell(*textBuffer.GetCellDataAt(srcPos));
do
{
const auto current = next;
srcView.WalkInBounds(srcPos, walkDirection);
next = OutputCell(*textBuffer.GetCellDataAt(srcPos));
// If the source position is offscreen (which can occur on double
// width lines), then we shouldn't copy anything to the destination.
if (srcPos.x < textBuffer.GetLineWidth(srcPos.y))
{
const auto data = OutputCell(*textBuffer.GetCellDataAt(srcPos));
textBuffer.Write(OutputCellIterator({ &data, 1 }), dstPos);
textBuffer.WriteLine(OutputCellIterator({ &current, 1 }), dstPos);
}
srcView.WalkInBounds(srcPos, walkDirection);
} while (dstView.WalkInBounds(dstPos, walkDirection));
_api.NotifyAccessibilityChange(dstRect);
}
Expand Down

0 comments on commit fb485a2

Please sign in to comment.