diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 0c6cad8ff57..dbd584c218e 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -1356,24 +1356,22 @@ void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool if (screenInfo.IsCursorInMargins(oldCursorPosition)) { // Cursor is at the top of the viewport - const COORD bufferSize = screenInfo.GetBufferSize().Dimensions(); - // Rectangle to cut out of the existing buffer + // Rectangle to cut out of the existing buffer. This is inclusive. + // It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width. SMALL_RECT srScroll; srScroll.Left = 0; - srScroll.Right = bufferSize.X; + srScroll.Right = SHORT_MAX; srScroll.Top = viewport.Top; - srScroll.Bottom = viewport.Bottom - 1; + srScroll.Bottom = viewport.Bottom; // Paste coordinate for cut text above COORD coordDestination; coordDestination.X = 0; coordDestination.Y = viewport.Top + 1; - SMALL_RECT srClip = viewport; - Status = NTSTATUS_FROM_HRESULT(ServiceLocator::LocateGlobals().api.ScrollConsoleScreenBufferWImpl(screenInfo, srScroll, coordDestination, - srClip, + srScroll, UNICODE_SPACE, screenInfo.GetAttributes().GetLegacyAttributes())); } @@ -2033,13 +2031,13 @@ void DoSrvPrivateModifyLinesImpl(const unsigned int count, const bool insert) const auto cursorPosition = textBuffer.GetCursor().GetPosition(); if (screenInfo.IsCursorInMargins(cursorPosition)) { - const auto screenEdges = screenInfo.GetBufferSize().ToInclusive(); - // Rectangle to cut out of the existing buffer + // Rectangle to cut out of the existing buffer. This is inclusive. + // It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width. SMALL_RECT srScroll; srScroll.Left = 0; - srScroll.Right = screenEdges.Right - screenEdges.Left; + srScroll.Right = SHORT_MAX; srScroll.Top = cursorPosition.Y; - srScroll.Bottom = screenEdges.Bottom; + srScroll.Bottom = screenInfo.GetViewport().BottomInclusive(); // Paste coordinate for cut text above COORD coordDestination; coordDestination.X = 0; @@ -2052,9 +2050,6 @@ void DoSrvPrivateModifyLinesImpl(const unsigned int count, const bool insert) coordDestination.Y = (cursorPosition.Y) - gsl::narrow(count); } - SMALL_RECT srClip = screenEdges; - srClip.Top = cursorPosition.Y; - // Here we previously called to ScrollConsoleScreenBufferWImpl to // perform the scrolling operation. However, that function only accepts // a WORD for the fill attributes. That means we'd lose 256/RGB fidelity @@ -2068,7 +2063,7 @@ void DoSrvPrivateModifyLinesImpl(const unsigned int count, const bool insert) auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); ScrollRegion(screenInfo, srScroll, - srClip, + srScroll, coordDestination, UNICODE_SPACE, screenInfo.GetAttributes()); diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 38d4faf2684..ff3d34988f9 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -161,6 +161,10 @@ class ScreenBufferTests TEST_METHOD(DontResetColorsAboveVirtualBottom); + TEST_METHOD(ScrollOperations); + TEST_METHOD(InsertChars); + TEST_METHOD(DeleteChars); + TEST_METHOD(ScrollUpInMargins); TEST_METHOD(ScrollDownInMargins); TEST_METHOD(InsertLinesInMargins); @@ -3057,6 +3061,499 @@ void ScreenBufferTests::DontResetColorsAboveVirtualBottom() } } +template +void _FillLine(COORD position, T fillContent, TextAttribute fillAttr) +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& row = si.GetTextBuffer().GetRowByOffset(position.Y); + row.WriteCells({ fillContent, fillAttr }, position.X, false); +} + +template +void _FillLine(int line, T fillContent, TextAttribute fillAttr) +{ + _FillLine({ 0, gsl::narrow(line) }, fillContent, fillAttr); +} + +template +void _FillLines(int startLine, int endLine, T fillContent, TextAttribute fillAttr) +{ + for (auto line = startLine; line < endLine; ++line) + { + _FillLine(line, fillContent, fillAttr); + } +} + +template +bool _ValidateLineContains(COORD position, T expectedContent, TextAttribute expectedAttr) +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto actual = si.GetCellLineDataAt(position); + auto expected = OutputCellIterator{ expectedContent, expectedAttr }; + while (actual && expected) + { + if (actual->Chars() != expected->Chars() || actual->TextAttr() != expected->TextAttr()) + { + return false; + } + ++actual; + ++expected; + } + return true; +}; + +template +bool _ValidateLineContains(int line, T expectedContent, TextAttribute expectedAttr) +{ + return _ValidateLineContains({ 0, gsl::narrow(line) }, expectedContent, expectedAttr); +} + +template +auto _ValidateLinesContain(int startLine, int endLine, T expectedContent, TextAttribute expectedAttr) +{ + for (auto line = startLine; line < endLine; ++line) + { + if (!_ValidateLineContains(line, expectedContent, expectedAttr)) + { + return false; + } + } + return true; +}; + +void ScreenBufferTests::ScrollOperations() +{ + enum ScrollType : int + { + ScrollUp, + ScrollDown, + InsertLine, + DeleteLine, + ReverseIndex + }; + enum ScrollDirection : int + { + Up, + Down + }; + + ScrollType scrollType; + ScrollDirection scrollDirection; + int scrollMagnitude; + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:scrollType", L"{0, 1, 2, 3, 4}") + TEST_METHOD_PROPERTY(L"Data:scrollMagnitude", L"{1, 2, 5}") + END_TEST_METHOD_PROPERTIES() + + VERIFY_SUCCEEDED(TestData::TryGetValue(L"scrollType", (int&)scrollType)); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"scrollMagnitude", scrollMagnitude)); + + std::wstringstream escapeSequence; + switch (scrollType) + { + case ScrollUp: + Log::Comment(L"Testing scroll up (SU)."); + escapeSequence << "\x1b[" << scrollMagnitude << "S"; + scrollDirection = Up; + break; + case ScrollDown: + Log::Comment(L"Testing scroll down (SD)."); + escapeSequence << "\x1b[" << scrollMagnitude << "T"; + scrollDirection = Down; + break; + case InsertLine: + Log::Comment(L"Testing insert line (IL)."); + escapeSequence << "\x1b[" << scrollMagnitude << "L"; + scrollDirection = Down; + break; + case DeleteLine: + Log::Comment(L"Testing delete line (DL)."); + escapeSequence << "\x1b[" << scrollMagnitude << "M"; + scrollDirection = Up; + break; + case ReverseIndex: + Log::Comment(L"Testing reverse index (RI)."); + for (auto i = 0; i < scrollMagnitude; ++i) + { + escapeSequence << "\x1bM"; + } + scrollDirection = Down; + break; + default: + VERIFY_FAIL(); + return; + } + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + const auto& cursor = si.GetTextBuffer().GetCursor(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const auto bufferWidth = si.GetBufferSize().Width(); + const auto bufferHeight = si.GetBufferSize().Height(); + + // Move the viewport down a few lines, and only cover part of the buffer width. + si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 10 }), true); + const auto viewportStart = si.GetViewport().Top(); + const auto viewportEnd = si.GetViewport().BottomExclusive(); + + // Fill the entire buffer with Zs. Blue on Green. + const auto bufferChar = L'Z'; + const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; + _FillLines(0, bufferHeight, bufferChar, bufferAttr); + + // Fill the viewport with a range of letters to see if they move. Red on Blue. + const auto viewportAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + auto viewportChar = L'A'; + auto viewportLine = viewportStart; + while (viewportLine < viewportEnd) + { + _FillLine(viewportLine++, viewportChar++, viewportAttr); + } + + // Set the background color so that it will be used to fill the revealed area. + si.SetAttributes({ BACKGROUND_RED }); + + // Place the cursor in the center. + auto cursorPos = COORD{ bufferWidth / 2, (viewportStart + viewportEnd) / 2 }; + // Unless this is reverse index, which has to be be at the top of the viewport. + if (scrollType == ReverseIndex) + { + cursorPos.Y = viewportStart; + } + + Log::Comment(L"Set the cursor position and perform the operation."); + VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true)); + stateMachine.ProcessString(escapeSequence.str()); + + Log::Comment(L"Verify cursor didn't move."); + VERIFY_ARE_EQUAL(cursorPos, cursor.GetPosition()); + + Log::Comment(L"Field of Zs outside viewport should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, bufferChar, bufferAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, bufferChar, bufferAttr)); + + // Depending on the direction of scrolling, lines are either deleted or inserted. + const auto deletedLines = scrollDirection == Up ? scrollMagnitude : 0; + const auto insertedLines = scrollDirection == Down ? scrollMagnitude : 0; + + // Insert and delete operations only scroll the viewport below the cursor position. + const auto scrollStart = (scrollType == InsertLine || scrollType == DeleteLine) ? cursorPos.Y : viewportStart; + + // Reset the viewport character and line number for the verification loop. + viewportChar = L'A'; + viewportLine = viewportStart; + + Log::Comment(L"Lines above the scrolled area should remain unchanged."); + while (viewportLine < scrollStart) + { + VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr)); + } + + Log::Comment(L"Scrolled area should have moved up/down by given magnitude."); + viewportChar += gsl::narrow(deletedLines); // Characters dropped when deleting + viewportLine += gsl::narrow(insertedLines); // Lines skipped when inserting + while (viewportLine < viewportEnd - deletedLines) + { + VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr)); + } + + Log::Comment(L"The revealed area should now be blank, with default buffer attributes."); + const auto revealedStart = scrollDirection == Up ? viewportEnd - deletedLines : scrollStart; + const auto revealedEnd = revealedStart + scrollMagnitude; + VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', si.GetAttributes())); +} + +void ScreenBufferTests::InsertChars() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + // Set the buffer width to 40, with a centered viewport of 20. + const auto bufferWidth = 40; + const auto bufferHeight = si.GetBufferSize().Height(); + const auto viewportStart = 10; + const auto viewportEnd = viewportStart + 20; + VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false)); + si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true); + + Log::Comment( + L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. " + L"Then insert 5 spaces at the cursor. Watch spaces get inserted, text slides right " + L"out of the viewport, pushing some of the Qs out of the buffer."); + + const auto insertLine = SHORT{ 10 }; + auto insertPos = SHORT{ 20 }; + + // Place the cursor in the center of the line. + VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); + + // Save the cursor position. It shouldn't move for the rest of the test. + const auto& cursor = si.GetTextBuffer().GetCursor(); + auto expectedCursor = cursor.GetPosition(); + + // Fill the entire line with Qs. Blue on Green. + const auto bufferChar = L'Q'; + const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; + _FillLine(insertLine, bufferChar, bufferAttr); + + // Fill the viewport range with text. Red on Blue. + const auto textChars = L"ABCDEFGHIJKLMNOPQRST"; + const auto textAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + _FillLine({ viewportStart, insertLine }, textChars, textAttr); + + // Set the background color so that it will be used to fill the revealed area. + si.SetAttributes({ BACKGROUND_RED }); + + // Insert 5 spaces at the cursor position. + // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ + // After: QQQQQQQQQQABCDEFGHIJ KLMNOPQRSTQQQQQ + Log::Comment(L"Inserting 5 spaces in the middle of the line."); + auto before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); + stateMachine.ProcessString(L"\x1b[5@"); + auto after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); + Log::Comment(before.c_str(), L"Before"); + Log::Comment(after.c_str(), L" After"); + + // Verify cursor didn't move. + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); + + // Verify the updated structure of the line. + VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs left of the viewport should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJ", textAttr), + L"First half of the alphabet should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", si.GetAttributes()), + L"Spaces should be inserted with the current attributes at the cursor position."); + VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNOPQRST", textAttr), + L"Second half of the alphabet should have moved to the right by the number of spaces inserted."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd + 5, insertLine }, L"QQQQQ", bufferAttr), + L"Field of Qs right of the viewport should be moved right, half pushed outside the buffer."); + + Log::Comment( + L"Test 2: Inserting at the exact end of the line. Same line structure. " + L"Move cursor to right edge of window and insert > 1 space. " + L"Only 1 should be inserted, everything else unchanged."); + + // Move cursor to right edge. + insertPos = bufferWidth - 1; + VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); + expectedCursor = cursor.GetPosition(); + + // Fill the entire line with Qs. Blue on Green. + _FillLine(insertLine, bufferChar, bufferAttr); + + // Fill the viewport range with text. Red on Blue. + _FillLine({ viewportStart, insertLine }, textChars, textAttr); + + // Set the background color so that it will be used to fill the revealed area. + si.SetAttributes({ BACKGROUND_RED }); + + // Insert 5 spaces at the right edge. Only 1 should be inserted. + // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ + // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ + Log::Comment(L"Inserting 5 spaces at the right edge of the buffer."); + before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); + stateMachine.ProcessString(L"\x1b[5@"); + after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); + Log::Comment(before.c_str(), L"Before"); + Log::Comment(after.c_str(), L" After"); + + // Verify cursor didn't move. + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); + + // Verify the updated structure of the line. + VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs left of the viewport should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr), + L"Entire viewport range should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged except for the last spot."); + VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", si.GetAttributes()), + L"One space should be inserted with the current attributes at the cursor postion."); + + Log::Comment( + L"Test 3: Inserting at the exact beginning of the line. Same line structure. " + L"Move cursor to left edge of buffer and insert > buffer width of space. " + L"The whole row should be replaced with spaces."); + + // Move cursor to left edge. + VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, insertLine }, true)); + expectedCursor = cursor.GetPosition(); + + // Fill the entire line with Qs. Blue on Green. + _FillLine(insertLine, bufferChar, bufferAttr); + + // Fill the viewport range with text. Red on Blue. + _FillLine({ viewportStart, insertLine }, textChars, textAttr); + + // Insert greater than the buffer width at the left edge. The entire line should be erased. + // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ + // After: + Log::Comment(L"Inserting 100 spaces at the left edge of the buffer."); + before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); + stateMachine.ProcessString(L"\x1b[100@"); + after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText(); + Log::Comment(before.c_str(), L"Before"); + Log::Comment(after.c_str(), L" After"); + + // Verify cursor didn't move. + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation."); + + // Verify the updated structure of the line. + VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', si.GetAttributes()), + L"A whole line of spaces was inserted at the start, erasing the line."); +} + +void ScreenBufferTests::DeleteChars() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + // Set the buffer width to 40, with a centered viewport of 20. + const auto bufferWidth = 40; + const auto bufferHeight = si.GetBufferSize().Height(); + const auto viewportStart = 10; + const auto viewportEnd = viewportStart + 20; + VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false)); + si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true); + + Log::Comment( + L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. " + L"Then delete 5 characters at the cursor. Watch the rest of the line slide left, " + L"replacing the deleted characters, with spaces inserted at the end of the line."); + + const auto deleteLine = SHORT{ 10 }; + auto deletePos = SHORT{ 20 }; + + // Place the cursor in the center of the line. + VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); + + // Save the cursor position. It shouldn't move for the rest of the test. + const auto& cursor = si.GetTextBuffer().GetCursor(); + auto expectedCursor = cursor.GetPosition(); + + // Fill the entire line with Qs. Blue on Green. + const auto bufferChar = L'Q'; + const auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN }; + _FillLine(deleteLine, bufferChar, bufferAttr); + + // Fill the viewport range with text. Red on Blue. + const auto textChars = L"ABCDEFGHIJKLMNOPQRST"; + const auto textAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE }; + _FillLine({ viewportStart, deleteLine }, textChars, textAttr); + + // Set the background color so that it will be used to fill the revealed area. + si.SetAttributes({ BACKGROUND_RED }); + + // Delete 5 characters at the cursor position. + // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ + // After: QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ + Log::Comment(L"Deleting 5 characters in the middle of the line."); + auto before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); + stateMachine.ProcessString(L"\x1b[5P"); + auto after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); + Log::Comment(before.c_str(), L"Before"); + Log::Comment(after.c_str(), L" After"); + + // Verify cursor didn't move. + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); + + // Verify the updated structure of the line. + VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs left of the viewport should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJ", textAttr), + L"First half of the alphabet should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L"PQRST", textAttr), + L"Only half of the second part of the alphabet remains."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should be moved left."); + VERIFY_IS_TRUE(_ValidateLineContains({ bufferWidth - 5, deleteLine }, L" ", si.GetAttributes()), + L"The rest of the line should be replaced with spaces with the current attributes."); + + Log::Comment( + L"Test 2: Deleting at the exact end of the line. Same line structure. " + L"Move cursor to right edge of window and delete > 1 character. " + L"Only 1 should be deleted, everything else unchanged."); + + // Move cursor to right edge. + deletePos = bufferWidth - 1; + VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); + expectedCursor = cursor.GetPosition(); + + // Fill the entire line with Qs. Blue on Green. + _FillLine(deleteLine, bufferChar, bufferAttr); + + // Fill the viewport range with text. Red on Blue. + _FillLine({ viewportStart, deleteLine }, textChars, textAttr); + + // Set the background color so that it will be used to fill the revealed area. + si.SetAttributes({ BACKGROUND_RED }); + + // Delete 5 characters at the right edge. Only 1 should be deleted. + // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ + // After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ + Log::Comment(L"Deleting 5 characters at the right edge of the buffer."); + before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); + stateMachine.ProcessString(L"\x1b[5P"); + after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); + Log::Comment(before.c_str(), L"Before"); + Log::Comment(after.c_str(), L" After"); + + // Verify cursor didn't move. + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); + + // Verify the updated structure of the line. + VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr), + L"Field of Qs left of the viewport should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr), + L"Entire viewport range should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQ", bufferAttr), + L"Field of Qs right of the viewport should remain unchanged except for the last spot."); + VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L" ", si.GetAttributes()), + L"One character should be erased with the current attributes at the cursor postion."); + + Log::Comment( + L"Test 3: Deleting at the exact beginning of the line. Same line structure. " + L"Move cursor to left edge of buffer and delete > buffer width of characters. " + L"The whole row should be replaced with spaces."); + + // Move cursor to left edge. + VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, deleteLine }, true)); + expectedCursor = cursor.GetPosition(); + + // Fill the entire line with Qs. Blue on Green. + _FillLine(deleteLine, bufferChar, bufferAttr); + + // Fill the viewport range with text. Red on Blue. + _FillLine({ viewportStart, deleteLine }, textChars, textAttr); + + // Delete greater than the buffer width at the left edge. The entire line should be erased. + // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ + // After: + Log::Comment(L"Deleting 100 characters at the left edge of the buffer."); + before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); + stateMachine.ProcessString(L"\x1b[100P"); + after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText(); + Log::Comment(before.c_str(), L"Before"); + Log::Comment(after.c_str(), L" After"); + + // Verify cursor didn't move. + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation."); + + // Verify the updated structure of the line. + VERIFY_IS_TRUE(_ValidateLineContains(deleteLine, L' ', si.GetAttributes()), + L"A whole line of spaces was inserted from the right, erasing the line."); +} + void _CommonScrollingSetup() { // Used for testing MSFT:20204600 diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 4b853910dd0..1e6089ed22d 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -506,7 +506,7 @@ bool AdaptDispatch::_InsertDeleteHelper(_In_ unsigned int const uiCount, const b SHORT sDistance; RETURN_IF_FALSE(SUCCEEDED(UIntToShort(uiCount, &sDistance))); - // get current cursor, viewport + // get current cursor, attributes CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); // Make sure to reset the viewport (with MoveToBottom )to where it was @@ -515,11 +515,11 @@ bool AdaptDispatch::_InsertDeleteHelper(_In_ unsigned int const uiCount, const b RETURN_IF_FALSE(_conApi->GetConsoleScreenBufferInfoEx(&csbiex)); const auto cursor = csbiex.dwCursorPosition; - const auto viewport = Viewport::FromExclusive(csbiex.srWindow); - // Rectangle to cut out of the existing buffer + // Rectangle to cut out of the existing buffer. This is inclusive. + // It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width. SMALL_RECT srScroll; srScroll.Left = cursor.X; - srScroll.Right = viewport.RightExclusive(); + srScroll.Right = SHORT_MAX; srScroll.Top = cursor.Y; srScroll.Bottom = srScroll.Top; @@ -541,85 +541,16 @@ bool AdaptDispatch::_InsertDeleteHelper(_In_ unsigned int const uiCount, const b } else { - // for delete, we need to add to the scroll region to move it off toward the right. - fSuccess = SUCCEEDED(ShortAdd(srScroll.Left, sDistance, &srScroll.Left)); + // Delete scrolls the affected region to the left, relying on the clipping rect to actually delete the characters. + fSuccess = SUCCEEDED(ShortSub(coordDestination.X, sDistance, &coordDestination.X)); } if (fSuccess) { - if (srScroll.Left >= viewport.RightExclusive() || - coordDestination.X >= viewport.RightExclusive()) - { - DWORD const nLength = viewport.RightExclusive() - cursor.X; - size_t written = 0; - - // if the select/scroll region is off screen to the right or the destination is off screen to the right, fill instead of scrolling. - fSuccess = !!_conApi->FillConsoleOutputCharacterW(ciFill.Char.UnicodeChar, - nLength, - cursor, - written); - - if (fSuccess) - { - written = 0; - fSuccess = !!_conApi->FillConsoleOutputAttribute(ciFill.Attributes, - nLength, - cursor, - written); - } - } - else - { - // clip inside the viewport. - fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScroll, - &csbiex.srWindow, - coordDestination, - &ciFill); - - if (fSuccess && !fIsInsert) - { - // See MSFT:19888564 - // We've now shifted a number of the characters to the left. - // If the number of chars we've shifted doesn't fill the - // entire region we deleted, then artifacts of the - // previous contents of the row can get left behind. - // - // Example: (this is tested by DeleteCharsNearEndOfLineSimpleFirstCase) - // start with the following buffer contents, and the cursor on the "D" - // [ABCDEFG ] - // ^ - // When you DCH(3) here, we are trying to delete the D, E and F. - // We do that by shifting the contents of the line after the deleted - // characters to the left. HOWEVER, there are only 2 chars left to move. - // So (before the fix) the buffer end up like this: - // [ABCG F ] - // ^ - // The G and " " have moved, but the F did not get overwritten. - // - // Fill the remaining space after the characters we - // shifted with spaces (empty cells). - const short scrolledChars = viewport.RightExclusive() - srScroll.Left; - const short shiftedRightPos = cursor.X + scrolledChars; - if (shiftedRightPos < srScroll.Left) - { - size_t written = 0; - const short spacesToFill = viewport.RightInclusive() - (shiftedRightPos); - const COORD fillPos{ shiftedRightPos, cursor.Y }; - fSuccess = !!_conApi->FillConsoleOutputCharacterW(ciFill.Char.UnicodeChar, - spacesToFill, - fillPos, - written); - if (fSuccess) - { - written = 0; - fSuccess = !!_conApi->FillConsoleOutputAttribute(ciFill.Attributes, - spacesToFill, - fillPos, - written); - } - } - } - } + fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScroll, + &srScroll, + coordDestination, + &ciFill); } return fSuccess; @@ -1025,7 +956,13 @@ bool AdaptDispatch::_ScrollMovement(const ScrollDirection sdDirection, _In_ unsi if (fSuccess) { - SMALL_RECT srScreen = csbiex.srWindow; + // Rectangle to cut out of the existing buffer. This is inclusive. + // It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width. + SMALL_RECT srScreen; + srScreen.Left = 0; + srScreen.Right = SHORT_MAX; + srScreen.Top = csbiex.srWindow.Top; + srScreen.Bottom = csbiex.srWindow.Bottom - 1; // srWindow is exclusive, hence the - 1 // Paste coordinate for cut text above COORD coordDestination; diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index bc65cae83ee..b2a6f4bb88c 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -47,12 +47,6 @@ enum class CursorDirection : unsigned int PREVLINE = 5 }; -enum class ScrollDirection : unsigned int -{ - UP = 0, - DOWN = 1 -}; - enum class AbsolutePosition : unsigned int { CursorHorizontal = 0, @@ -395,114 +389,10 @@ class TestGetSet final : public ConGetSet return _fPrivateWriteConsoleControlInputResult; } - bool _IsInsideClip(const SMALL_RECT* const pClipRectangle, const SHORT iRow, const SHORT iCol) - { - if (pClipRectangle == nullptr) - { - return true; - } - else - { - return iRow >= pClipRectangle->Top && iRow < pClipRectangle->Bottom && iCol >= pClipRectangle->Left && iCol < pClipRectangle->Right; - } - } - - BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle, _In_opt_ const SMALL_RECT* pClipRectangle, _In_ COORD dwDestinationOrigin, const CHAR_INFO* pFill) override + BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* /*pScrollRectangle*/, _In_opt_ const SMALL_RECT* /*pClipRectangle*/, _In_ COORD /*dwDestinationOrigin*/, const CHAR_INFO* /*pFill*/) override { Log::Comment(L"ScrollConsoleScreenBufferW MOCK called..."); - if (_fScrollConsoleScreenBufferWResult) - { - if (pClipRectangle != nullptr) - { - Log::Comment(NoThrowString().Format( - L"\tScrolling Rectangle (T: %d, B: %d, L: %d, R: %d) " - L"into new top-left coordinate (X: %d, Y:%d) with Fill ('%c', 0x%x) " - L"clipping to (T: %d, B: %d, L: %d, R: %d)...", - pScrollRectangle->Top, - pScrollRectangle->Bottom, - pScrollRectangle->Left, - pScrollRectangle->Right, - dwDestinationOrigin.X, - dwDestinationOrigin.Y, - pFill->Char.UnicodeChar, - pFill->Attributes, - pClipRectangle->Top, - pClipRectangle->Bottom, - pClipRectangle->Left, - pClipRectangle->Right)); - } - else - { - Log::Comment(NoThrowString().Format( - L"\tScrolling Rectangle (T: %d, B: %d, L: %d, R: %d) " - L"into new top-left coordinate (X: %d, Y:%d) with Fill ('%c', 0x%x) ", - pScrollRectangle->Top, - pScrollRectangle->Bottom, - pScrollRectangle->Left, - pScrollRectangle->Right, - dwDestinationOrigin.X, - dwDestinationOrigin.Y, - pFill->Char.UnicodeChar, - pFill->Attributes)); - } - - // allocate buffer space to hold scrolling rectangle - SHORT width = pScrollRectangle->Right - pScrollRectangle->Left; - SHORT height = pScrollRectangle->Bottom - pScrollRectangle->Top + 1; - size_t const cch = width * height; - CHAR_INFO* const ciBuffer = new CHAR_INFO[cch]; - size_t cciFilled = 0; - - Log::Comment(NoThrowString().Format(L"\tCopy buffer size is %zu chars", cch)); - - for (SHORT iCharY = pScrollRectangle->Top; iCharY <= pScrollRectangle->Bottom; iCharY++) - { - // back up space and fill it with the fill. - for (SHORT iCharX = pScrollRectangle->Left; iCharX < pScrollRectangle->Right; iCharX++) - { - COORD coordTarget; - coordTarget.X = (SHORT)iCharX; - coordTarget.Y = iCharY; - - CHAR_INFO* const pciStored = _GetCharAt(coordTarget.Y, coordTarget.X); - - // back up to buffer - ciBuffer[cciFilled] = *pciStored; - cciFilled++; - - // fill with fill - if (_IsInsideClip(pClipRectangle, coordTarget.Y, coordTarget.X)) - { - *pciStored = *pFill; - } - } - } - Log::Comment(NoThrowString().Format(L"\tCopied a total %zu chars", cciFilled)); - Log::Comment(L"\tCopying chars back"); - for (SHORT iCharY = pScrollRectangle->Top; iCharY <= pScrollRectangle->Bottom; iCharY++) - { - // back up space and fill it with the fill. - for (SHORT iCharX = pScrollRectangle->Left; iCharX < pScrollRectangle->Right; iCharX++) - { - COORD coordTarget; - coordTarget.X = dwDestinationOrigin.X + (iCharX - pScrollRectangle->Left); - coordTarget.Y = dwDestinationOrigin.Y + (iCharY - pScrollRectangle->Top); - - CHAR_INFO* const pciStored = _GetCharAt(coordTarget.Y, coordTarget.X); - - if (_IsInsideClip(pClipRectangle, coordTarget.Y, coordTarget.X) && _IsInsideClip(pClipRectangle, iCharY, iCharX)) - { - size_t index = (width) * (iCharY - pScrollRectangle->Top) + (iCharX - pScrollRectangle->Left); - CHAR_INFO charFromBuffer = ciBuffer[index]; - *pciStored = charFromBuffer; - } - } - } - - delete[] ciBuffer; - } - return _fScrollConsoleScreenBufferWResult; } @@ -978,55 +868,6 @@ class TestGetSet final : public ConGetSet } } - void InsertString(COORD coordTarget, PWSTR pwszText, WORD wAttr) - { - Log::Comment(NoThrowString().Format(L"Writing string '%s' to target (X: %d, Y:%d) with color/attr 0x%x", pwszText, coordTarget.X, coordTarget.Y, wAttr)); - - size_t cchModified = 0; - - if (pwszText != nullptr) - { - size_t cch; - if (SUCCEEDED(StringCchLengthW(pwszText, STRSAFE_MAX_LENGTH, &cch))) - { - COORD coordInsertPoint = coordTarget; - - for (size_t i = 0; i < cch; i++) - { - CHAR_INFO* const pci = _GetCharAt(coordInsertPoint.Y, coordInsertPoint.X); - pci->Char.UnicodeChar = pwszText[i]; - pci->Attributes = wAttr; - - _IncrementCoordPos(&coordInsertPoint); - cchModified++; - } - } - } - - Log::Comment(NoThrowString().Format(L"Wrote %zu characters into buffer.", cchModified)); - } - - void FillRectangle(SMALL_RECT srRect, wchar_t wch, WORD wAttr) - { - Log::Comment(NoThrowString().Format(L"Filling area (L: %d, R: %d, T: %d, B: %d) with '%c' in attr 0x%x", srRect.Left, srRect.Right, srRect.Top, srRect.Bottom, wch, wAttr)); - - size_t cchModified = 0; - - for (SHORT iRow = srRect.Top; iRow < srRect.Bottom; iRow++) - { - for (SHORT iCol = srRect.Left; iCol < srRect.Right; iCol++) - { - CHAR_INFO* const pci = _GetCharAt(iRow, iCol); - pci->Char.UnicodeChar = wch; - pci->Attributes = wAttr; - - cchModified++; - } - } - - Log::Comment(NoThrowString().Format(L"Filled %zu characters.", cchModified)); - } - void ValidateInputEvent(_In_ PCWSTR pwszExpectedResponse) { size_t const cchResponse = wcslen(pwszExpectedResponse); @@ -1053,132 +894,6 @@ class TestGetSet final : public ConGetSet } } - bool ValidateString(COORD const coordTarget, PCWSTR pwszText, WORD const wAttr) - { - Log::Comment(NoThrowString().Format(L"Validating that the string %s is written starting at (X: %d, Y: %d) with the color/attr 0x%x", pwszText, coordTarget.X, coordTarget.Y, wAttr)); - - bool fSuccess = true; - - if (pwszText != nullptr) - { - size_t cch; - fSuccess = SUCCEEDED(StringCchLengthW(pwszText, STRSAFE_MAX_LENGTH, &cch)); - - if (fSuccess) - { - COORD coordGetPos = coordTarget; - - for (size_t i = 0; i < cch; i++) - { - const CHAR_INFO* const pci = _GetCharAt(coordGetPos.Y, coordGetPos.X); - - const wchar_t wchActual = pci->Char.UnicodeChar; - const wchar_t wchExpected = pwszText[i]; - - fSuccess = wchExpected == wchActual; - - if (!fSuccess) - { - Log::Comment(NoThrowString().Format(L"ValidateString failed char comparison at (X: %d, Y: %d). Expected: '%c' Actual: '%c'", coordGetPos.X, coordGetPos.Y, wchExpected, wchActual)); - break; - } - - const WORD wAttrActual = pci->Attributes; - const WORD wAttrExpected = wAttr; - - if (!fSuccess) - { - Log::Comment(NoThrowString().Format(L"ValidateString failed attr comparison at (X: %d, Y: %d). Expected: '0x%x' Actual: '0x%x'", coordGetPos.X, coordGetPos.Y, wAttrExpected, wAttrActual)); - break; - } - - _IncrementCoordPos(&coordGetPos); - } - } - } - - return fSuccess; - } - - bool ValidateRectangleContains(SMALL_RECT srRect, wchar_t wchExpected, WORD wAttrExpected) - { - Log::Comment(NoThrowString().Format(L"Validating that the area inside (L: %d, R: %d, T: %d, B: %d) char '%c' and attr 0x%x", srRect.Left, srRect.Right, srRect.Top, srRect.Bottom, wchExpected, wAttrExpected)); - - bool fStateValid = true; - - for (SHORT iRow = srRect.Top; iRow < srRect.Bottom; iRow++) - { - Log::Comment(NoThrowString().Format(L"Validating row(y=) %d", iRow)); - for (SHORT iCol = srRect.Left; iCol < srRect.Right; iCol++) - { - CHAR_INFO* const pci = _GetCharAt(iRow, iCol); - - fStateValid = pci->Char.UnicodeChar == wchExpected; - if (!fStateValid) - { - Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected: '%c'. Actual: '%c'", iCol, iRow, wchExpected, pci->Char.UnicodeChar)); - break; - } - - fStateValid = pci->Attributes == wAttrExpected; - if (!fStateValid) - { - Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected Attr: 0x%x. Actual Attr: 0x%x", iCol, iRow, wAttrExpected, pci->Attributes)); - } - } - - if (!fStateValid) - { - break; - } - } - - return fStateValid; - } - - bool ValidateRectangleContains(SMALL_RECT srRect, wchar_t wchExpected, WORD wAttrExpected, SMALL_RECT srExcept) - { - bool fStateValid = true; - - Log::Comment(NoThrowString().Format(L"Validating that the area inside (L: %d, R: %d, T: %d, B: %d) but outside (L: %d, R: %d, T: %d, B: %d) contains char '%c' and attr 0x%x", srRect.Left, srRect.Right, srRect.Top, srRect.Bottom, srExcept.Left, srExcept.Right, srExcept.Top, srExcept.Bottom, wchExpected, wAttrExpected)); - - for (SHORT iRow = srRect.Top; iRow < srRect.Bottom; iRow++) - { - for (SHORT iCol = srRect.Left; iCol < srRect.Right; iCol++) - { - if (iRow >= srExcept.Top && iRow < srExcept.Bottom && iCol >= srExcept.Left && iCol < srExcept.Right) - { - // if in exception range, skip comparison. - continue; - } - else - { - CHAR_INFO* const pci = _GetCharAt(iRow, iCol); - - fStateValid = pci->Char.UnicodeChar == wchExpected; - if (!fStateValid) - { - Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected: '%c'. Actual: '%c'", iCol, iRow, wchExpected, pci->Char.UnicodeChar)); - break; - } - - fStateValid = pci->Attributes == wAttrExpected; - if (!fStateValid) - { - Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected Attr: 0x%x. Actual Attr: 0x%x", iCol, iRow, wAttrExpected, pci->Attributes)); - } - } - } - - if (!fStateValid) - { - break; - } - } - - return fStateValid; - } - bool ValidateEraseBufferState(SMALL_RECT* rgsrRegions, size_t cRegions, wchar_t wchExpectedInRegions, WORD wAttrExpectedInRegions) { bool fStateValid = true; @@ -1274,20 +989,6 @@ class TestGetSet final : public ConGetSet return pchar; } - void _PrepForScroll(ScrollDirection const dir, int const distance) - { - _fExpectedWindowAbsolute = FALSE; - _srExpectedConsoleWindow.Top = (SHORT)distance; - _srExpectedConsoleWindow.Bottom = (SHORT)distance; - _srExpectedConsoleWindow.Left = 0; - _srExpectedConsoleWindow.Right = 0; - if (dir == ScrollDirection::UP) - { - _srExpectedConsoleWindow.Top *= -1; - _srExpectedConsoleWindow.Bottom *= -1; - } - } - void _SetMarginsHelper(SMALL_RECT* rect, SHORT top, SHORT bottom) { rect->Top = top; @@ -1939,315 +1640,6 @@ class AdapterTest VERIFY_IS_FALSE(_pDispatch->CursorVisibility(fEnd)); } - TEST_METHOD(InsertCharacterTests) - { - Log::Comment(L"Starting test..."); - - Log::Comment(L"Test 1: The big one. Fill the buffer with Qs. Fill the window with Rs. Write a line of ABCDE at the cursor. Then insert 5 spaces at the cursor. Watch spaces get inserted, ABCDE slide right eating up the Rs in the viewport but not modifying the Qs outside."); - - // place the cursor in the center. - _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); - - // Save the cursor position. It shouldn't move for the rest of the test. - COORD coordCursorExpected = _testGetSet->_coordCursorPos; - - // Fill the entire buffer with Qs. Blue on Green. - WCHAR const wchOuterBuffer = 'Q'; - WORD const wAttrOuterBuffer = FOREGROUND_BLUE | BACKGROUND_GREEN; - SMALL_RECT srOuterBuffer; - srOuterBuffer.Top = 0; - srOuterBuffer.Left = 0; - srOuterBuffer.Bottom = _testGetSet->_coordBufferSize.Y; - srOuterBuffer.Right = _testGetSet->_coordBufferSize.X; - _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); - - // Fill the viewport with Rs. Red on Blue. - WCHAR const wchViewport = 'R'; - WORD const wAttrViewport = FOREGROUND_RED | BACKGROUND_BLUE; - SMALL_RECT srViewport = _testGetSet->_srViewport; - _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); - - // fill some of the text right of the cursor so we can verify it moved it and didn't overwrite it. - // change the color too so we can make sure that it's fine - - WORD const wAttrTestText = FOREGROUND_GREEN; - PWSTR const pwszTestText = L"ABCDE"; - size_t cchTestText = wcslen(pwszTestText); - SMALL_RECT srTestText; - srTestText.Top = _testGetSet->_coordCursorPos.Y; - srTestText.Bottom = srTestText.Top + 1; - srTestText.Left = _testGetSet->_coordCursorPos.X; - srTestText.Right = srTestText.Left + (SHORT)cchTestText; - _testGetSet->InsertString(_testGetSet->_coordCursorPos, pwszTestText, wAttrTestText); - - WCHAR const wchInsertExpected = L' '; - WORD const wAttrInsertExpected = _testGetSet->_wAttribute; - size_t const cchInsertSize = 5; - SMALL_RECT srInsertExpected; - srInsertExpected.Top = _testGetSet->_coordCursorPos.Y; - srInsertExpected.Bottom = srInsertExpected.Top + 1; - srInsertExpected.Left = _testGetSet->_coordCursorPos.X; - srInsertExpected.Right = srInsertExpected.Left + (SHORT)cchInsertSize; - - // the text we inserted is going to move right by the insert size, so adjust that rectangle right. - srTestText.Left += cchInsertSize; - srTestText.Right += cchInsertSize; - - // insert out 5 spots. this should clear them out with spaces and the default fill from the original cursor position - VERIFY_IS_TRUE(_pDispatch->InsertCharacter(cchInsertSize), L"Verify insert call was sucessful."); - - // the combined area of the letters + the spaces will be 10 characters wide: - SMALL_RECT srModifiedSpace; - srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; - srModifiedSpace.Bottom = srModifiedSpace.Top + 1; - srModifiedSpace.Left = _testGetSet->_coordCursorPos.X; - srModifiedSpace.Right = srModifiedSpace.Left + (SHORT)cchInsertSize + (SHORT)cchTestText; - - // verify cursor didn't move - VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); - - // e.g. we had this in the buffer: QQQRRRRRRABCDERRRRRRRQQQ with the cursor on the A. - // now we should have this buffer: QQQRRRRRR ABCDERRQQQ with the cursor on the first space. - - // Verify the field of Qs didn't change outside the viewport. - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); - - // Verify the field of Rs within the viewport not including the inserted range and the ABCDE shifted right. (10 characters) - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); - - // Verify the 5 spaces inserted from the cursor. - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srInsertExpected, wchInsertExpected, wAttrInsertExpected), L"Spaces should be inserted with the proper attributes at the cursor."); - - // Verify the ABCDE sequence was shifted right. - COORD coordTestText; - coordTestText.X = srTestText.Left; - coordTestText.Y = srTestText.Top; - VERIFY_IS_TRUE(_testGetSet->ValidateString(coordTestText, pwszTestText, wAttrTestText), L"Inserted string should have moved to the right by the number of spaces inserted, attributes and text preserved."); - - // Test case needed for exact end of line (and full line) insert/delete lengths - Log::Comment(L"Test 2: Inserting at the exact end of the line. Same field of Qs and Rs. Move cursor to right edge of window and insert > 1 space. Only 1 should be inserted, everything else unchanged."); - - _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); - _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); - - // move cursor to right edge - _testGetSet->_coordCursorPos.X = _testGetSet->_srViewport.Right - 1; - coordCursorExpected = _testGetSet->_coordCursorPos; - - // the rectangle where the space should be is exactly the size of the cursor. - srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; - srModifiedSpace.Bottom = srModifiedSpace.Top + 1; - srModifiedSpace.Left = _testGetSet->_coordCursorPos.X; - srModifiedSpace.Right = srModifiedSpace.Left + 1; - - // insert out 5 spots. this should clear them out with spaces and the default fill from the original cursor position - VERIFY_IS_TRUE(_pDispatch->InsertCharacter(cchInsertSize), L"Verify insert call was sucessful."); - - // cursor didn't move - VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); - - // Qs are the same outside the viewport - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); - - // Entire viewport is Rs except the one space spot - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); - - // The 5 inserted spaces at the right edge resulted in 1 space at the right edge - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srModifiedSpace, wchInsertExpected, wAttrInsertExpected), L"A space was inserted at the cursor position. All extra spaces were discarded as they hit the right boundary."); - - Log::Comment(L"Test 3: Inserting at the exact beginning of the line. Same field of Qs and Rs. Move cursor to left edge of window and insert > screen width of space. The whole row should be spaces but nothing outside the viewport should be changed."); - - _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); - _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); - - // move cursor to left edge - _testGetSet->_coordCursorPos.X = _testGetSet->_srViewport.Left; - coordCursorExpected = _testGetSet->_coordCursorPos; - - // the rectangle of spaces should be the entire line at the cursor. - srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; - srModifiedSpace.Bottom = srModifiedSpace.Top + 1; - srModifiedSpace.Left = _testGetSet->_srViewport.Left; - srModifiedSpace.Right = _testGetSet->_srViewport.Right; - - // insert greater than the entire viewport (the entire buffer width) at the cursor position - VERIFY_IS_TRUE(_pDispatch->InsertCharacter(_testGetSet->_coordBufferSize.X), L"Verify insert call was successful."); - - // cursor didn't move - VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); - - // Qs are the same outside the viewport - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); - - // Entire viewport is Rs except the one space spot - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); - - // The inserted spaces at the left edge resulted in an entire line of spaces bounded by the viewport - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srModifiedSpace, wchInsertExpected, wAttrInsertExpected), L"A whole line of spaces was inserted at the cursor position. All extra spaces were discarded as they hit the right boundary."); - } - - TEST_METHOD(DeleteCharacterTests) - { - Log::Comment(L"Starting test..."); - - Log::Comment(L"Test 1: The big one. Fill the buffer with Qs. Fill the window with Rs. Write a line of ABCDE at the cursor. Then insert 5 spaces at the cursor. Watch spaces get inserted, ABCDE slide right eating up the Rs in the viewport but not modifying the Qs outside."); - - // place the cursor in the center. - _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); - - // Save the cursor position. It shouldn't move for the rest of the test. - COORD coordCursorExpected = _testGetSet->_coordCursorPos; - - // Fill the entire buffer with Qs. Blue on Green. - WCHAR const wchOuterBuffer = 'Q'; - WORD const wAttrOuterBuffer = FOREGROUND_BLUE | BACKGROUND_GREEN; - SMALL_RECT srOuterBuffer; - srOuterBuffer.Top = 0; - srOuterBuffer.Left = 0; - srOuterBuffer.Bottom = _testGetSet->_coordBufferSize.Y; - srOuterBuffer.Right = _testGetSet->_coordBufferSize.X; - _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); - - // Fill the viewport with Rs. Red on Blue. - WCHAR const wchViewport = 'R'; - WORD const wAttrViewport = FOREGROUND_RED | BACKGROUND_BLUE; - SMALL_RECT srViewport = _testGetSet->_srViewport; - _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); - - // fill some of the text right of the cursor so we can verify it moved it and wasn't deleted - // change the color too so we can make sure that it's fine - WORD const wAttrTestText = FOREGROUND_GREEN; - PWSTR const pwszTestText = L"ABCDE"; - size_t cchTestText = wcslen(pwszTestText); - SMALL_RECT srTestText; - srTestText.Top = _testGetSet->_coordCursorPos.Y; - srTestText.Bottom = srTestText.Top + 1; - srTestText.Left = _testGetSet->_coordCursorPos.X; - srTestText.Right = srTestText.Left + (SHORT)cchTestText; - _testGetSet->InsertString(_testGetSet->_coordCursorPos, pwszTestText, wAttrTestText); - - // We're going to delete "in" from the right edge, so set up that rectangle. - WCHAR const wchDeleteExpected = L' '; - WORD const wAttrDeleteExpected = _testGetSet->_wAttribute; - size_t const cchDeleteSize = 5; - SMALL_RECT srDeleteExpected; - srDeleteExpected.Top = _testGetSet->_coordCursorPos.Y; - srDeleteExpected.Bottom = srDeleteExpected.Top + 1; - srDeleteExpected.Right = _testGetSet->_srViewport.Right; - srDeleteExpected.Left = srDeleteExpected.Right - cchDeleteSize; - - // We want the ABCDE to shift left when we delete and onto the cursor. So move the cursor left 5 and adjust the srTestText rectangle left 5 to the new - // final destination of where they will be after the delete operation occurs. - _testGetSet->_coordCursorPos.X -= cchDeleteSize; - coordCursorExpected = _testGetSet->_coordCursorPos; - srTestText.Left -= cchDeleteSize; - srTestText.Right -= cchDeleteSize; - - // delete out 5 spots. this should shift the ABCDE text left by 5 and insert 5 spaces at the end of the line - VERIFY_IS_TRUE(_pDispatch->DeleteCharacter(cchDeleteSize), L"Verify delete call was sucessful."); - - // we're going to have ABCDERRRRRRRRRRRRR QQQQQQQ - // since this is a bit more complicated than the insert case, make this the "special" region and exempt it from the bulk "R" check - // we'll check the inside of this rect in 3 pieces, for the ABCDE, then for the inner Rs, then for the 5 spaces after. - SMALL_RECT srSpecialSpace; - srSpecialSpace.Top = _testGetSet->_coordCursorPos.Y; - srSpecialSpace.Bottom = srSpecialSpace.Top + 1; - srSpecialSpace.Left = _testGetSet->_coordCursorPos.X; - srSpecialSpace.Right = _testGetSet->_srViewport.Right; - - SMALL_RECT srGap; // gap space is the Rs between ABCDE and the spaces shifted in from the right - srGap.Left = srTestText.Right; - srGap.Right = srDeleteExpected.Left; - srGap.Top = _testGetSet->_coordCursorPos.Y; - srGap.Bottom = srGap.Top + 1; - - // verify cursor didn't move - VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); - - // e.g. we had this in the buffer: QQQRRRRRR-RRRRABCDERRQQQ with the cursor on the -. - // now we should have this buffer: QQQRRRRRRABCDERR QQQ with the cursor on the A. - - // Verify the field of Qs didn't change outside the viewport. - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); - - // Verify the field of Rs within the viewport not including the special range of the ABCDE, the spaces shifted in from the right, and the Rs between them that went along for the ride. - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srSpecialSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); - - // Verify the 5 spaces shifted in from the right edge due to the delete - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srDeleteExpected, wchDeleteExpected, wAttrDeleteExpected), L"Spaces should be inserted with the proper attributes from the right end of this line (viewport edge.)"); - - // Verify the ABCDE sequence was shifted left by 5 toward the cursor. - COORD coordTestText; - coordTestText.X = srTestText.Left; - coordTestText.Y = srTestText.Top; - VERIFY_IS_TRUE(_testGetSet->ValidateString(coordTestText, pwszTestText, wAttrTestText), L"Inserted string should have moved to the left by the number of deletes, attributes and text preserved."); - - // Verify the field of Rs between the ABCDE and the spaces shifted in from the right - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srGap, wchViewport, wAttrViewport), L"Viewport Rs should be preserved/shifted left in between the ABCDE and the spaces that came in from the right edge."); - - // Test case needed for exact end of line (and full line) insert/delete lengths - Log::Comment(L"Test 2: Deleting at the exact end of the line. Same field of Qs and Rs. Move cursor to right edge of window and delete > 1 space. Only 1 should be inserted from the right edge (delete inserts from the right), everything else unchanged."); - - _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); - _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); - - // move cursor to right edge - _testGetSet->_coordCursorPos.X = _testGetSet->_srViewport.Right - 1; - coordCursorExpected = _testGetSet->_coordCursorPos; - - // the rectangle where the space should be is exactly the size of the cursor. - SMALL_RECT srModifiedSpace; - srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; - srModifiedSpace.Bottom = srModifiedSpace.Top + 1; - srModifiedSpace.Left = _testGetSet->_coordCursorPos.X; - srModifiedSpace.Right = srModifiedSpace.Left + 1; - - // delete out 5 spots. this should clear them out with spaces and the default fill from the original cursor position - VERIFY_IS_TRUE(_pDispatch->DeleteCharacter(cchDeleteSize), L"Verify delete call was sucessful."); - - // cursor didn't move - VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from delete operation."); - - // Qs are the same outside the viewport - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); - - // Entire viewport is Rs except the one space spot - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); - - // The 5 deleted spaces at the right edge resulted in 1 space at the right edge - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srModifiedSpace, wchDeleteExpected, wAttrDeleteExpected), L"A space was inserted at the cursor position. All extra spaces deleted in from the right continued to cover that one space."); - - Log::Comment(L"Test 3: Deleting at the exact beginning of the line. Same field of Qs and Rs. Move cursor to left edge of window and delete > screen width of space. The whole row should be spaces but nothing outside the viewport should be changed."); - - _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); - _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); - - // move cursor to left edge - _testGetSet->_coordCursorPos.X = _testGetSet->_srViewport.Left; - coordCursorExpected = _testGetSet->_coordCursorPos; - - // the rectangle of spaces should be the entire line at the cursor. - srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; - srModifiedSpace.Bottom = srModifiedSpace.Top + 1; - srModifiedSpace.Left = _testGetSet->_srViewport.Left; - srModifiedSpace.Right = _testGetSet->_srViewport.Right; - - // delete greater than the entire viewport (the entire buffer width) at the cursor position - VERIFY_IS_TRUE(_pDispatch->DeleteCharacter(_testGetSet->_coordBufferSize.X), L"Verify delete call was successful."); - - // cursor didn't move - VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); - - // Qs are the same outside the viewport - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); - - // Entire viewport is Rs except the one space spot - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); - - // The inserted spaces at the left edge resulted in an entire line of spaces bounded by the viewport - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srModifiedSpace, wchDeleteExpected, wAttrDeleteExpected), L"A whole line of spaces was inserted from the right (the cursor position was deleted enough times.) Extra deletes just covered up some of the spaces that were shifted in."); - } - // Ensures that EraseScrollback (^[[3J) deletes any content from the buffer // above the viewport, and moves the contents of the buffer in the // viewport to 0,0. This emulates the xterm behavior of clearing any @@ -2982,129 +2374,6 @@ class AdapterTest VERIFY_IS_FALSE(_pDispatch->DeviceAttributes()); } - TEST_METHOD(ScrollTest) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:uiDirection", L"{0, 1}") // These values align with the ScrollDirection enum class to try all the directions. - TEST_METHOD_PROPERTY(L"Data:uiMagnitude", L"{1, 2, 5}") // These values align with the ScrollDirection enum class to try all the directions. - END_TEST_METHOD_PROPERTIES() - - Log::Comment(L"Starting test..."); - - // Used to switch between the various function options. - typedef bool (AdaptDispatch::*ScrollFunc)(const unsigned int); - ScrollFunc scrollFunc = nullptr; - - // Modify variables based on directionality of this test - ScrollDirection direction; - unsigned int dir; - VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiDirection", dir)); - direction = (ScrollDirection)dir; - unsigned int uiMagnitude; - VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiMagnitude", uiMagnitude)); - SHORT sMagnitude = (SHORT)uiMagnitude; - - switch (direction) - { - case ScrollDirection::UP: - Log::Comment(L"Testing up direction."); - scrollFunc = &AdaptDispatch::ScrollUp; - break; - case ScrollDirection::DOWN: - Log::Comment(L"Testing down direction."); - scrollFunc = &AdaptDispatch::ScrollDown; - break; - } - Log::Comment(NoThrowString().Format(L"Scrolling by %d lines", uiMagnitude)); - if (scrollFunc == nullptr) - { - VERIFY_FAIL(); - return; - } - - // place the cursor in the center. - _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); - - // Save the cursor position. It shouldn't move for the rest of the test. - COORD coordCursorExpected = _testGetSet->_coordCursorPos; - - // Fill the entire buffer with Qs. Blue on Green. - WCHAR const wchOuterBuffer = 'Q'; - WORD const wAttrOuterBuffer = FOREGROUND_BLUE | BACKGROUND_GREEN; - SMALL_RECT srOuterBuffer; - srOuterBuffer.Top = 0; - srOuterBuffer.Left = 0; - srOuterBuffer.Bottom = _testGetSet->_coordBufferSize.Y; - srOuterBuffer.Right = _testGetSet->_coordBufferSize.X; - _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); - - // Fill the viewport with Rs. Red on Blue. - WCHAR const wchViewport = 'R'; - WORD const wAttrViewport = FOREGROUND_RED | BACKGROUND_BLUE; - SMALL_RECT srViewport = _testGetSet->_srViewport; - _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); - - // Add some characters to see if they moved. - // change the color too so we can make sure that it's fine - - WORD const wAttrTestText = FOREGROUND_GREEN; - PWSTR const pwszTestText = L"ABCDE"; // Text is written at y=34, moves to y=33 - size_t cchTestText = wcslen(pwszTestText); - SMALL_RECT srTestText; - srTestText.Top = _testGetSet->_coordCursorPos.Y; - srTestText.Bottom = srTestText.Top + 1; - srTestText.Left = _testGetSet->_coordCursorPos.X; - srTestText.Right = srTestText.Left + (SHORT)cchTestText; - _testGetSet->InsertString(_testGetSet->_coordCursorPos, pwszTestText, wAttrTestText); - - //Scroll Up one line - VERIFY_IS_TRUE((_pDispatch->*(scrollFunc))(sMagnitude), L"Verify Scroll call was sucessful."); - - // verify cursor didn't move - VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); - - // Verify the field of Qs didn't change outside the viewport. - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), - L"Field of Qs outside viewport should remain unchanged."); - - // Okay, this part get confusing. These change depending on the direction of the test. - // direction InViewport Outside - // UP Bottom Line Top minus One - // DOWN Top Line Bottom plus One - const bool fScrollUp = (direction == ScrollDirection::UP); - SMALL_RECT srInViewport = srViewport; - srInViewport.Top = (fScrollUp) ? (srViewport.Bottom - sMagnitude) : (srViewport.Top); - srInViewport.Bottom = srInViewport.Top + sMagnitude; - WCHAR const wchInViewport = ' '; - WORD const wAttrInViewport = _testGetSet->_wAttribute; - - // Verify the bottom line is now empty - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srInViewport, wchInViewport, wAttrInViewport), - L"InViewport line(s) should now be blank, with default buffer attributes"); - - SMALL_RECT srOutside = srViewport; - srOutside.Top = (fScrollUp) ? (srViewport.Top - sMagnitude) : (srViewport.Bottom); - srOutside.Bottom = srOutside.Top + sMagnitude; - WCHAR const wchOutside = wchOuterBuffer; - WORD const wAttrOutside = wAttrOuterBuffer; - - // Verify the line above the viewport is unchanged - VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOutside, wchOutside, wAttrOutside), - L"Line(s) above the viewport is unchanged"); - - // Verify that the line where the ABCDE is now wchViewport - COORD coordTestText; - PWSTR const pwszNewTestText = L"RRRRR"; - coordTestText.X = srTestText.Left; - coordTestText.Y = srTestText.Top; - VERIFY_IS_TRUE(_testGetSet->ValidateString(coordTestText, pwszNewTestText, wAttrViewport), L"Contents of viewport should have shifted to where the string used to be."); - - // Verify that the line above/below the ABCDE now has the ABCDE - coordTestText.X = srTestText.Left; - coordTestText.Y = (fScrollUp) ? (srTestText.Top - sMagnitude) : (srTestText.Top + sMagnitude); - VERIFY_IS_TRUE(_testGetSet->ValidateString(coordTestText, pwszTestText, wAttrTestText), L"String should have moved up/down by given magnitude."); - } - TEST_METHOD(CursorKeysModeTest) { Log::Comment(L"Starting test...");