diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp index fb498d25112..c6ac011543e 100644 --- a/src/buffer/out/TextAttribute.cpp +++ b/src/buffer/out/TextAttribute.cpp @@ -266,3 +266,12 @@ bool TextAttribute::BackgroundIsDefault() const noexcept { return _background.IsDefault(); } + +// Routine Description: +// - Resets the meta and extended attributes, which is what the VT standard +// requires for most erasing and filling operations. +void TextAttribute::SetStandardErase() noexcept +{ + SetExtendedAttributes(ExtendedAttributes::Normal); + SetMetaAttributes(0); +} diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp index 213c908e8f2..ff0bce8073f 100644 --- a/src/buffer/out/TextAttribute.hpp +++ b/src/buffer/out/TextAttribute.hpp @@ -156,6 +156,8 @@ class TextAttribute final bool ForegroundIsDefault() const noexcept; bool BackgroundIsDefault() const noexcept; + void SetStandardErase() noexcept; + constexpr bool IsRgb() const noexcept { return _foreground.IsRgb() || _background.IsRgb(); diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index fdec52c363a..ff461409e27 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -539,17 +539,24 @@ bool TextBuffer::NewlineCursor() //Routine Description: // - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable. //Arguments: -// - +// - inVtMode - set to true in VT mode, so standard erase attributes are used for the new row. //Return Value: // - true if we successfully incremented the buffer. -bool TextBuffer::IncrementCircularBuffer() +bool TextBuffer::IncrementCircularBuffer(const bool inVtMode) { // FirstRow is at any given point in time the array index in the circular buffer that corresponds // to the logical position 0 in the window (cursor coordinates and all other coordinates). _renderTarget.TriggerCircling(); // First, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed. - const bool fSuccess = _storage.at(_firstRow).Reset(_currentAttributes); + auto fillAttributes = _currentAttributes; + if (inVtMode) + { + // The VT standard requires that the new row is initialized with + // the current background color, but with no meta attributes set. + fillAttributes.SetStandardErase(); + } + const bool fSuccess = _storage.at(_firstRow).Reset(fillAttributes); if (fSuccess) { // Now proceed to increment. diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index ced8f0904bd..58eb01e5a50 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -103,7 +103,7 @@ class TextBuffer final bool NewlineCursor(); // Scroll needs access to this to quickly rotate around the buffer. - bool IncrementCircularBuffer(); + bool IncrementCircularBuffer(const bool inVtMode = false); COORD GetLastNonSpaceCharacter() const; COORD GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const; diff --git a/src/host/_output.cpp b/src/host/_output.cpp index 8d9fad7640a..334a5dc342b 100644 --- a/src/host/_output.cpp +++ b/src/host/_output.cpp @@ -219,24 +219,6 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region) try { TextAttribute useThisAttr(attribute); - - // Here we're being a little clever - - // Because RGB color can't roundtrip the API, certain VT sequences will forget the RGB color - // because their first call to GetScreenBufferInfo returned a legacy attr. - // If they're calling this with the default attrs, they likely wanted to use the RGB default attrs. - // This could create a scenario where someone emitted RGB with VT, - // THEN used the API to FillConsoleOutput with the default attrs, and DIDN'T want the RGB color - // they had set. - if (screenBuffer.InVTMode()) - { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto bufferLegacy = gci.GenerateLegacyAttributes(screenBuffer.GetAttributes()); - if (bufferLegacy == attribute) - { - useThisAttr = TextAttribute(screenBuffer.GetAttributes()); - } - } - const OutputCellIterator it(useThisAttr, lengthToWrite); const auto done = screenBuffer.Write(it, startingCoordinate); diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index 94d4f7c74c3..9a492086490 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -72,7 +72,10 @@ using Microsoft::Console::VirtualTerminal::StateMachine; } } - const auto bufferAttributes = screenInfo.GetAttributes(); + // The VT standard requires the lines revealed when scrolling are filled + // with the current background color, but with no meta attributes set. + auto fillAttributes = screenInfo.GetAttributes(); + fillAttributes.SetStandardErase(); const auto relativeMargins = screenInfo.GetRelativeScrollMargins(); auto viewport = screenInfo.GetViewport(); @@ -142,7 +145,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine; try { - ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, bufferAttributes); + ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, fillAttributes); } CATCH_LOG(); @@ -191,7 +194,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine; try { - ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, bufferAttributes); + ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, fillAttributes); } CATCH_LOG(); diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 6d979633b56..3f869f8e128 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -836,32 +836,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont auto& buffer = context.GetActiveBuffer(); TextAttribute useThisAttr(fillAttribute); - - // Here we're being a little clever - similar to FillConsoleOutputAttributeImpl - // Because RGB/default color can't roundtrip the API, certain VT - // sequences will forget the RGB color because their first call to - // GetScreenBufferInfo returned a legacy attr. - // If they're calling this with the legacy attrs version of our current - // attributes, they likely wanted to use the full version of - // our current attributes, whether that be RGB or _default_ colored. - // This could create a scenario where someone emitted RGB with VT, - // THEN used the API to ScrollConsoleOutput with the legacy attrs, - // and DIDN'T want the RGB color. As in FillConsoleOutputAttribute, - // this scenario is highly unlikely, and we can reasonably do this - // on their behalf. - // see MSFT:19853701 - - if (buffer.InVTMode()) - { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto currentAttributes = buffer.GetAttributes(); - const auto bufferLegacy = gci.GenerateLegacyAttributes(currentAttributes); - if (bufferLegacy == fillAttribute) - { - useThisAttr = currentAttributes; - } - } - ScrollRegion(buffer, source, clip, target, fillCharacter, useThisAttr); return S_OK; @@ -1422,25 +1396,12 @@ void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool coordDestination.X = 0; coordDestination.Y = viewport.Top + 1; - // 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 for fill attributes. So instead, we'll just call - // ScrollRegion ourselves, with the same params that - // ScrollConsoleScreenBufferWImpl would have. - // See microsoft/terminal#832, #2702 for more context. - try - { - LockConsole(); - auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - ScrollRegion(screenInfo, - srScroll, - srScroll, - coordDestination, - UNICODE_SPACE, - screenInfo.GetAttributes()); - } - CATCH_LOG(); + // Note the revealed lines are filled with the standard erase attributes. + Status = NTSTATUS_FROM_HRESULT(DoSrvPrivateScrollRegion(screenInfo, + srScroll, + srScroll, + coordDestination, + true)); } } return Status; @@ -2149,25 +2110,12 @@ void DoSrvPrivateModifyLinesImpl(const unsigned int count, const bool insert) coordDestination.Y = (cursorPosition.Y) - gsl::narrow(count); } - // 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 - // for fill attributes. So instead, we'll just call ScrollRegion - // ourselves, with the same params that ScrollConsoleScreenBufferWImpl - // would have. - // See microsoft/terminal#832 for more context. - try - { - LockConsole(); - auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); - ScrollRegion(screenInfo, - srScroll, - srScroll, - coordDestination, - UNICODE_SPACE, - screenInfo.GetAttributes()); - } - CATCH_LOG(); + // Note the revealed lines are filled with the standard erase attributes. + LOG_IF_FAILED(DoSrvPrivateScrollRegion(screenInfo, + srScroll, + srScroll, + coordDestination, + true)); // The IL and DL controls are also expected to move the cursor to the left margin. // For now this is just column 0, since we don't yet support DECSLRM. @@ -2291,3 +2239,99 @@ void DoSrvPrivateMoveToBottom(SCREEN_INFORMATION& screenInfo) } CATCH_RETURN(); } + +// Routine Description: +// - A private API call for filling a region of the screen buffer. +// Arguments: +// - screenInfo - Reference to screen buffer info. +// - startPosition - The position to begin filling at. +// - fillLength - The number of characters to fill. +// - fillChar - Character to fill the target region with. +// - standardFillAttrs - If true, fill with the standard erase attributes. +// If false, fill with the default attributes. +// Return value: +// - S_OK or failure code from thrown exception +[[nodiscard]] HRESULT DoSrvPrivateFillRegion(SCREEN_INFORMATION& screenInfo, + const COORD startPosition, + const size_t fillLength, + const wchar_t fillChar, + const bool standardFillAttrs) noexcept +{ + try + { + if (fillLength == 0) + { + return S_OK; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // For most VT erasing operations, the standard requires that the + // erased area be filled with the current background color, but with + // no additional meta attributes set. For all other cases, we just + // fill with the default attributes. + auto fillAttrs = TextAttribute{}; + if (standardFillAttrs) + { + fillAttrs = screenInfo.GetAttributes(); + fillAttrs.SetStandardErase(); + } + + const auto fillData = OutputCellIterator{ fillChar, fillAttrs, fillLength }; + screenInfo.Write(fillData, startPosition, false); + + // Notify accessibility + auto endPosition = startPosition; + const auto bufferSize = screenInfo.GetBufferSize(); + bufferSize.MoveInBounds(fillLength - 1, endPosition); + screenInfo.NotifyAccessibilityEventing(startPosition.X, startPosition.Y, endPosition.X, endPosition.Y); + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - A private API call for moving a block of data in the screen buffer, +// optionally limiting the effects of the move to a clipping rectangle. +// Arguments: +// - screenInfo - Reference to screen buffer info. +// - scrollRect - Region to copy/move (source and size). +// - clipRect - Optional clip region to contain buffer change effects. +// - destinationOrigin - Upper left corner of target region. +// - standardFillAttrs - If true, fill with the standard erase attributes. +// If false, fill with the default attributes. +// Return value: +// - S_OK or failure code from thrown exception +[[nodiscard]] HRESULT DoSrvPrivateScrollRegion(SCREEN_INFORMATION& screenInfo, + const SMALL_RECT scrollRect, + const std::optional clipRect, + const COORD destinationOrigin, + const bool standardFillAttrs) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // For most VT scrolling operations, the standard requires that the + // erased area be filled with the current background color, but with + // no additional meta attributes set. For all other cases, we just + // fill with the default attributes. + auto fillAttrs = TextAttribute{}; + if (standardFillAttrs) + { + fillAttrs = screenInfo.GetAttributes(); + fillAttrs.SetStandardErase(); + } + + ScrollRegion(screenInfo, + scrollRect, + clipRect, + destinationOrigin, + UNICODE_SPACE, + fillAttrs); + return S_OK; + } + CATCH_RETURN(); +} diff --git a/src/host/getset.h b/src/host/getset.h index 349c3c6b87c..0971b435192 100644 --- a/src/host/getset.h +++ b/src/host/getset.h @@ -92,3 +92,15 @@ void DoSrvPrivateMoveToBottom(SCREEN_INFORMATION& screenInfo); [[nodiscard]] HRESULT DoSrvPrivateSetDefaultForegroundColor(const COLORREF value) noexcept; [[nodiscard]] HRESULT DoSrvPrivateSetDefaultBackgroundColor(const COLORREF value) noexcept; + +[[nodiscard]] HRESULT DoSrvPrivateFillRegion(SCREEN_INFORMATION& screenInfo, + const COORD startPosition, + const size_t fillLength, + const wchar_t fillChar, + const bool standardFillAttrs) noexcept; + +[[nodiscard]] HRESULT DoSrvPrivateScrollRegion(SCREEN_INFORMATION& screenInfo, + const SMALL_RECT scrollRect, + const std::optional clipRect, + const COORD destinationOrigin, + const bool standardFillAttrs) noexcept; diff --git a/src/host/output.cpp b/src/host/output.cpp index 8b4e0bbbbda..ef726c92b54 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -296,7 +296,8 @@ static void _ScrollScreen(SCREEN_INFORMATION& screenInfo, const Viewport& source bool StreamScrollRegion(SCREEN_INFORMATION& screenInfo) { // Rotate the circular buffer around and wipe out the previous final line. - bool fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer(); + const bool inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + bool fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer(inVtMode); if (fSuccess) { // Trigger a graphical update if we're active. diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index f45fc8b1deb..a13d1f1574a 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -157,42 +157,6 @@ BOOL ConhostInternalGetSet::SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* cons return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleCursorInfoImpl(_io.GetActiveOutputBuffer(), pConsoleCursorInfo->dwSize, visible)); } -// Routine Description: -// - Connects the FillConsoleOutputCharacter API call directly into our Driver Message servicing call inside Conhost.exe -// Arguments: -// - wch - Character to use for filling the buffer -// - nLength - The length of the fill run in characters (depending on mode, will wrap at the window edge so multiple lines are the sum of the total length) -// - dwWriteCoord - The first fill character's coordinate position in the buffer (writes continue rightward and possibly down from there) -// - numberOfCharsWritten - Pointer to memory location to hold the total number of characters written into the buffer -// Return Value: -// - TRUE if successful (see FillConsoleOutputCharacterWImpl). FALSE otherwise. -BOOL ConhostInternalGetSet::FillConsoleOutputCharacterW(const WCHAR wch, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfCharsWritten) noexcept -{ - return SUCCEEDED(ServiceLocator::LocateGlobals().api.FillConsoleOutputCharacterWImpl(_io.GetActiveOutputBuffer(), - wch, - nLength, - dwWriteCoord, - numberOfCharsWritten)); -} - -// Routine Description: -// - Connects the FillConsoleOutputAttribute API call directly into our Driver Message servicing call inside Conhost.exe -// Arguments: -// - wAttribute - Text attribute (colors/font style) for filling the buffer -// - nLength - The length of the fill run in characters (depending on mode, will wrap at the window edge so multiple lines are the sum of the total length) -// - dwWriteCoord - The first fill character's coordinate position in the buffer (writes continue rightward and possibly down from there) -// - numberOfCharsWritten - Pointer to memory location to hold the total number of text attributes written into the buffer -// Return Value: -// - TRUE if successful (see FillConsoleOutputAttributeImpl). FALSE otherwise. -BOOL ConhostInternalGetSet::FillConsoleOutputAttribute(const WORD wAttribute, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfAttrsWritten) noexcept -{ - return SUCCEEDED(ServiceLocator::LocateGlobals().api.FillConsoleOutputAttributeImpl(_io.GetActiveOutputBuffer(), - wAttribute, - nLength, - dwWriteCoord, - numberOfAttrsWritten)); -} - // Routine Description: // - Connects the SetConsoleTextAttribute API call directly into our Driver Message servicing call inside Conhost.exe // Sets BOTH the FG and the BG component of the attributes. @@ -318,28 +282,6 @@ BOOL ConhostInternalGetSet::PrivateWriteConsoleInputW(_Inout_ std::deque(*pClipRectangle) : std::nullopt, - pFill->Char.UnicodeChar, - pFill->Attributes)); -} - // Routine Description: // - Connects the SetConsoleWindowInfo API call directly into our Driver Message servicing call inside Conhost.exe // Arguments: @@ -812,3 +754,54 @@ BOOL ConhostInternalGetSet::PrivateSetDefaultBackground(const COLORREF value) co { return SUCCEEDED(DoSrvPrivateSetDefaultBackgroundColor(value)); } + +// Routine Description: +// - Connects the PrivateFillRegion call directly into our Driver Message servicing +// call inside Conhost.exe +// PrivateFillRegion is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on our public API surface. +// Arguments: +// - screenInfo - Reference to screen buffer info. +// - startPosition - The position to begin filling at. +// - fillLength - The number of characters to fill. +// - fillChar - Character to fill the target region with. +// - standardFillAttrs - If true, fill with the standard erase attributes. +// If false, fill with the default attributes. +// Return value: +// - TRUE if successful (see DoSrvPrivateScrollRegion). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateFillRegion(const COORD startPosition, + const size_t fillLength, + const wchar_t fillChar, + const bool standardFillAttrs) noexcept +{ + return SUCCEEDED(DoSrvPrivateFillRegion(_io.GetActiveOutputBuffer(), + startPosition, + fillLength, + fillChar, + standardFillAttrs)); +} + +// Routine Description: +// - Connects the PrivateScrollRegion call directly into our Driver Message servicing +// call inside Conhost.exe +// PrivateScrollRegion is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on our public API surface. +// Arguments: +// - scrollRect - Region to copy/move (source and size). +// - clipRect - Optional clip region to contain buffer change effects. +// - destinationOrigin - Upper left corner of target region. +// - standardFillAttrs - If true, fill with the standard erase attributes. +// If false, fill with the default attributes. +// Return value: +// - TRUE if successful (see DoSrvPrivateScrollRegion). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateScrollRegion(const SMALL_RECT scrollRect, + const std::optional clipRect, + const COORD destinationOrigin, + const bool standardFillAttrs) noexcept +{ + return SUCCEEDED(DoSrvPrivateScrollRegion(_io.GetActiveOutputBuffer(), + scrollRect, + clipRect, + destinationOrigin, + standardFillAttrs)); +} diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 5cfb7bf8bbd..4937ba20dc7 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -62,15 +62,6 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: BOOL GetConsoleCursorInfo(_In_ CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) const override; BOOL SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) override; - BOOL FillConsoleOutputCharacterW(const WCHAR wch, - const DWORD nLength, - const COORD dwWriteCoord, - size_t& numberOfCharsWritten) noexcept override; - BOOL FillConsoleOutputAttribute(const WORD wAttribute, - const DWORD nLength, - const COORD dwWriteCoord, - size_t& numberOfAttrsWritten) noexcept override; - BOOL SetConsoleTextAttribute(const WORD wAttr) override; BOOL PrivateSetLegacyAttributes(const WORD wAttr, @@ -94,11 +85,6 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: BOOL PrivateWriteConsoleInputW(_Inout_ std::deque>& events, _Out_ size_t& eventsWritten) override; - BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle, - _In_opt_ const SMALL_RECT* pClipRectangle, - _In_ COORD coordDestinationOrigin, - const CHAR_INFO* pFill) override; - BOOL SetConsoleWindowInfo(BOOL const bAbsolute, const SMALL_RECT* const lpConsoleWindow) override; @@ -163,6 +149,16 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: BOOL PrivateSetDefaultBackground(const COLORREF value) const noexcept override; + BOOL PrivateFillRegion(const COORD startPosition, + const size_t fillLength, + const wchar_t fillChar, + const bool standardFillAttrs) noexcept override; + + BOOL PrivateScrollRegion(const SMALL_RECT scrollRect, + const std::optional clipRect, + const COORD destinationOrigin, + const bool standardFillAttrs) noexcept override; + private: Microsoft::Console::IIoProvider& _io; }; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index ec384715803..d682e672e0a 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -2035,10 +2035,15 @@ const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const const FontInfo& existingFont = GetCurrentFont(); + // The buffer needs to be initialized with the standard erase attributes, + // i.e. the current background color, but with no meta attributes set. + auto initAttributes = GetAttributes(); + initAttributes.SetStandardErase(); + NTSTATUS Status = SCREEN_INFORMATION::CreateInstance(WindowSize, existingFont, WindowSize, - GetAttributes(), + initAttributes, *GetPopupAttributes(), CURSOR_SMALL_SIZE, ppsiNewScreenBuffer); @@ -2501,9 +2506,14 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport, _viewport.ConvertFromOrigin(&relativeCursor); RETURN_IF_FAILED(SetCursorPosition(relativeCursor, false)); - // Update all the rows in the current viewport with the currently active attributes. - OutputCellIterator it(GetAttributes()); - WriteRect(it, _viewport); + // Update all the rows in the current viewport with the standard erase attributes, + // i.e. the current background color, but with no meta attributes set. + auto fillAttributes = GetAttributes(); + fillAttributes.SetStandardErase(); + auto fillPosition = COORD{ 0, _viewport.Top() }; + auto fillLength = gsl::narrow_cast(_viewport.Height() * GetBufferSize().Width()); + auto fillData = OutputCellIterator{ fillAttributes, fillLength }; + Write(fillData, fillPosition, false); return S_OK; } @@ -2801,7 +2811,7 @@ void SCREEN_INFORMATION::UpdateBottom() } // Method Description: -// - Initialize the row with the cursor on it to the current text attributes. +// - Initialize the row with the cursor on it to the standard erase attributes. // This is executed when we move the cursor below the current viewport in // VT mode. When that happens in a real terminal, the line is brand new, // so it gets initialized for the first time with the current attributes. @@ -2818,7 +2828,11 @@ void SCREEN_INFORMATION::InitializeCursorRowAttributes() { const auto& cursor = _textBuffer->GetCursor(); ROW& row = _textBuffer->GetRowByOffset(cursor.GetPosition().Y); - row.GetAttrRow().SetAttrToEnd(0, GetAttributes()); + // The VT standard requires that the new row is initialized with + // the current background color, but with no meta attributes set. + auto fillAttributes = GetAttributes(); + fillAttributes.SetStandardErase(); + row.GetAttrRow().SetAttrToEnd(0, fillAttributes); } } diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 3281c08543c..60ea440fd60 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -116,6 +116,7 @@ class ScreenBufferTests TEST_METHOD(VtScrollMarginsNewlineColor); TEST_METHOD(VtNewlinePastViewport); + TEST_METHOD(VtNewlinePastEndOfBuffer); TEST_METHOD(VtSetColorTable); @@ -167,6 +168,9 @@ class ScreenBufferTests TEST_METHOD(InsertChars); TEST_METHOD(DeleteChars); + TEST_METHOD(EraseScrollbackTests); + TEST_METHOD(EraseTests); + TEST_METHOD(ScrollUpInMargins); TEST_METHOD(ScrollDownInMargins); TEST_METHOD(InsertLinesInMargins); @@ -1323,7 +1327,6 @@ void ScreenBufferTests::VtNewlinePastViewport() stateMachine.ProcessString(seq); const TextAttribute defaultAttrs{}; - const TextAttribute expectedTwo{ FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE }; Log::Comment(NoThrowString().Format( L"Move the cursor to the bottom of the viewport")); @@ -1335,8 +1338,94 @@ void ScreenBufferTests::VtNewlinePastViewport() cursor.SetPosition(COORD({ 0, initialViewport.BottomInclusive() })); - seq = L"\x1b[92;44m"; // bright-green on dark-blue + // Set the attributes that will be used to initialize new rows. + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut); + fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE); + si.SetAttributes(fillAttr); + // But note that the meta attributes are expected to be cleared. + auto expectedFillAttr = fillAttr; + expectedFillAttr.SetStandardErase(); + + seq = L"\n"; stateMachine.ProcessString(seq); + + const auto viewport = si.GetViewport(); + Log::Comment(NoThrowString().Format( + L"viewport=%s", + VerifyOutputTraits::ToString(viewport.ToInclusive()).GetBuffer())); + + VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); + + for (int y = viewport.Top(); y < viewport.BottomInclusive(); y++) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + const ROW& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + for (int x = 0; x < viewport.RightInclusive(); x++) + { + const auto& attr = attrs[x]; + VERIFY_ARE_EQUAL(defaultAttrs, attr); + } + } + + const ROW& row = tbi.GetRowByOffset(viewport.BottomInclusive()); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + for (int x = 0; x < viewport.RightInclusive(); x++) + { + const auto& attr = attrs[x]; + VERIFY_ARE_EQUAL(expectedFillAttr, attr); + } +} + +void ScreenBufferTests::VtNewlinePastEndOfBuffer() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + + // Make sure we're in VT mode + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({ 0, 0 }), true)); + cursor.SetPosition(COORD({ 0, 0 })); + + std::wstring seq = L"\x1b[m"; + stateMachine.ProcessString(seq); + seq = L"\x1b[2J"; + stateMachine.ProcessString(seq); + + const TextAttribute defaultAttrs{}; + + Log::Comment(L"Move the cursor to the bottom of the buffer"); + for (auto i = 0; i < si.GetBufferSize().Height(); i++) + { + stateMachine.ProcessString(L"\n"); + } + + const auto initialViewport = si.GetViewport(); + Log::Comment(NoThrowString().Format( + L"initialViewport=%s", + VerifyOutputTraits::ToString(initialViewport.ToInclusive()).GetBuffer())); + + cursor.SetPosition(COORD({ 0, initialViewport.BottomInclusive() })); + + // Set the attributes that will be used to initialize new rows. + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut); + fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE); + si.SetAttributes(fillAttr); + // But note that the meta attributes are expected to be cleared. + auto expectedFillAttr = fillAttr; + expectedFillAttr.SetStandardErase(); + seq = L"\n"; stateMachine.ProcessString(seq); @@ -1367,7 +1456,7 @@ void ScreenBufferTests::VtNewlinePastViewport() for (int x = 0; x < viewport.RightInclusive(); x++) { const auto& attr = attrs[x]; - VERIFY_ARE_EQUAL(expectedTwo, attr); + VERIFY_ARE_EQUAL(expectedFillAttr, attr); } } @@ -3139,13 +3228,13 @@ void _FillLines(int startLine, int endLine, T fillContent, TextAttribute fillAtt } } -template -bool _ValidateLineContains(COORD position, T expectedContent, TextAttribute expectedAttr) +template +bool _ValidateLineContains(COORD position, T&&... expectedContent) { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); auto actual = si.GetCellLineDataAt(position); - auto expected = OutputCellIterator{ expectedContent, expectedAttr }; + auto expected = OutputCellIterator{ std::forward(expectedContent)... }; while (actual && expected) { if (actual->Chars() != expected->Chars() || actual->TextAttr() != expected->TextAttr()) @@ -3269,8 +3358,14 @@ void ScreenBufferTests::ScrollOperations() _FillLine(viewportLine++, viewportChar++, viewportAttr); } - // Set the background color so that it will be used to fill the revealed area. - si.SetAttributes({ BACKGROUND_RED }); + // Set the attributes that will be used to fill the revealed area. + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut); + fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE); + si.SetAttributes(fillAttr); + // But note that the meta attributes are expected to be cleared. + auto expectedFillAttr = fillAttr; + expectedFillAttr.SetStandardErase(); // Place the cursor in the center. auto cursorPos = COORD{ bufferWidth / 2, (viewportStart + viewportEnd) / 2 }; @@ -3324,10 +3419,10 @@ void ScreenBufferTests::ScrollOperations() VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr)); } - Log::Comment(L"The revealed area should now be blank, with default buffer attributes."); + Log::Comment(L"The revealed area should now be blank, with standard erase attributes."); const auto revealedStart = scrollDirection == Up ? viewportEnd - deletedLines : scrollStart; const auto revealedEnd = revealedStart + scrollMagnitude; - VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', si.GetAttributes())); + VERIFY_IS_TRUE(_ValidateLinesContain(revealedStart, revealedEnd, L' ', expectedFillAttr)); } void ScreenBufferTests::InsertChars() @@ -3383,8 +3478,14 @@ void ScreenBufferTests::InsertChars() 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 }); + // Set the attributes that will be used to fill the revealed area. + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut); + fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE); + si.SetAttributes(fillAttr); + // But note that the meta attributes are expected to be cleared. + auto expectedFillAttr = fillAttr; + expectedFillAttr.SetStandardErase(); // Insert 5 spaces at the cursor position. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ @@ -3404,8 +3505,8 @@ void ScreenBufferTests::InsertChars() 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, insertLine }, L" ", expectedFillAttr), + L"Spaces should be inserted with standard erase 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), @@ -3427,9 +3528,6 @@ void ScreenBufferTests::InsertChars() // 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 @@ -3450,8 +3548,8 @@ void ScreenBufferTests::InsertChars() 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."); + VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr), + L"One space should be inserted with standard erase attributes at the cursor postion."); Log::Comment( L"Test 3: Inserting at the exact beginning of the line. Same line structure. " @@ -3482,7 +3580,7 @@ void ScreenBufferTests::InsertChars() 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()), + VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', expectedFillAttr), L"A whole line of spaces was inserted at the start, erasing the line."); } @@ -3539,8 +3637,14 @@ void ScreenBufferTests::DeleteChars() 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 }); + // Set the attributes that will be used to fill the revealed area. + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut); + fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE); + si.SetAttributes(fillAttr); + // But note that the meta attributes are expected to be cleared. + auto expectedFillAttr = fillAttr; + expectedFillAttr.SetStandardErase(); // Delete 5 characters at the cursor position. // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ @@ -3564,8 +3668,8 @@ void ScreenBufferTests::DeleteChars() 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."); + VERIFY_IS_TRUE(_ValidateLineContains({ bufferWidth - 5, deleteLine }, L" ", expectedFillAttr), + L"The rest of the line should be replaced with spaces with standard erase attributes."); Log::Comment( L"Test 2: Deleting at the exact end of the line. Same line structure. " @@ -3583,9 +3687,6 @@ void ScreenBufferTests::DeleteChars() // 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 @@ -3606,8 +3707,8 @@ void ScreenBufferTests::DeleteChars() 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."); + VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L" ", expectedFillAttr), + L"One character should be erased with standard erase attributes at the cursor postion."); Log::Comment( L"Test 3: Deleting at the exact beginning of the line. Same line structure. " @@ -3638,10 +3739,217 @@ void ScreenBufferTests::DeleteChars() 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()), + VERIFY_IS_TRUE(_ValidateLineContains(deleteLine, L' ', expectedFillAttr), L"A whole line of spaces was inserted from the right, erasing the line."); } +void ScreenBufferTests::EraseScrollbackTests() +{ + 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 viewport = si.GetViewport(); + + // 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 = viewport.Top(); + while (viewportLine < viewport.BottomExclusive()) + { + _FillLine(viewportLine++, viewportChar++, viewportAttr); + } + + // Set the colors to Green on Red. This should have no effect on the results. + si.SetAttributes({ FOREGROUND_GREEN | BACKGROUND_RED }); + + // Place the cursor in the center. + const short centerX = bufferWidth / 2; + const short centerY = (si.GetViewport().Top() + si.GetViewport().BottomExclusive()) / 2; + const auto cursorPos = COORD{ centerX, centerY }; + + Log::Comment(L"Set the cursor position and erase the scrollback."); + VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true)); + stateMachine.ProcessString(L"\x1b[3J"); + + // The viewport should move to the top of the buffer, while the cursor + // maintains the same relative position. + const auto expectedOffset = COORD{ 0, -viewport.Top() }; + const auto expectedViewport = Viewport::Offset(viewport, expectedOffset); + const auto expectedCursorPos = COORD{ cursorPos.X, cursorPos.Y + expectedOffset.Y }; + + Log::Comment(L"Verify expected viewport."); + VERIFY_ARE_EQUAL(expectedViewport, si.GetViewport()); + + Log::Comment(L"Verify expected cursor position."); + VERIFY_ARE_EQUAL(expectedCursorPos, cursor.GetPosition()); + + Log::Comment(L"Viewport contents should have moved to the new location."); + viewportChar = L'A'; + viewportLine = expectedViewport.Top(); + while (viewportLine < expectedViewport.BottomExclusive()) + { + VERIFY_IS_TRUE(_ValidateLineContains(viewportLine++, viewportChar++, viewportAttr)); + } + + Log::Comment(L"The rest of the buffer should be cleared with default attributes."); + VERIFY_IS_TRUE(_ValidateLinesContain(viewportLine, bufferHeight, L' ', TextAttribute{})); +} + +void ScreenBufferTests::EraseTests() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:eraseType", L"{0, 1, 2}") // corresponds to options in DispatchTypes::EraseType + TEST_METHOD_PROPERTY(L"Data:eraseScreen", L"{false, true}") // corresponds to Line (false) or Screen (true) + END_TEST_METHOD_PROPERTIES() + + DispatchTypes::EraseType eraseType; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseType", (int&)eraseType)); + bool eraseScreen; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"eraseScreen", eraseScreen)); + + std::wstringstream escapeSequence; + escapeSequence << "\x1b["; + + switch (eraseType) + { + case DispatchTypes::EraseType::ToEnd: + Log::Comment(L"Erasing line from cursor to end."); + escapeSequence << "0"; + break; + case DispatchTypes::EraseType::FromBeginning: + Log::Comment(L"Erasing line from beginning to cursor."); + escapeSequence << "1"; + break; + case DispatchTypes::EraseType::All: + Log::Comment(L"Erasing all."); + escapeSequence << "2"; + break; + default: + VERIFY_FAIL(L"Unsupported erase type."); + } + + if (!eraseScreen) + { + Log::Comment(L"Erasing just one line (the cursor's line)."); + escapeSequence << "K"; + } + else + { + Log::Comment(L"Erasing entire display (viewport). May be bounded by the cursor."); + escapeSequence << "J"; + } + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + 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); + + // 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); + + // Set the attributes that will be used to fill the erased area. + auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) }; + fillAttr.SetExtendedAttributes(ExtendedAttributes::CrossedOut); + fillAttr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE); + si.SetAttributes(fillAttr); + // But note that the meta attributes are expected to be cleared. + auto expectedFillAttr = fillAttr; + expectedFillAttr.SetStandardErase(); + + // Place the cursor in the center. + const short centerX = bufferWidth / 2; + const short centerY = (si.GetViewport().Top() + si.GetViewport().BottomExclusive()) / 2; + + Log::Comment(L"Set the cursor position and perform the operation."); + VERIFY_SUCCEEDED(si.SetCursorPosition({ centerX, centerY }, true)); + stateMachine.ProcessString(escapeSequence.str()); + + // Get cursor position and viewport range. + const auto cursorPos = si.GetTextBuffer().GetCursor().GetPosition(); + const auto viewportStart = si.GetViewport().Top(); + const auto viewportEnd = si.GetViewport().BottomExclusive(); + + Log::Comment(L"Lines outside the viewport should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLinesContain(0, viewportStart, bufferChar, bufferAttr)); + VERIFY_IS_TRUE(_ValidateLinesContain(viewportEnd, bufferHeight, bufferChar, bufferAttr)); + + // 1. Lines before cursor line + if (eraseScreen && eraseType != DispatchTypes::EraseType::ToEnd) + { + // For eraseScreen, if we're not erasing to the end, these rows will be cleared. + Log::Comment(L"Lines before the cursor line should be erased."); + VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, cursorPos.Y, L' ', expectedFillAttr)); + } + else + { + // Otherwise we'll be left with the original buffer content. + Log::Comment(L"Lines before the cursor line should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLinesContain(viewportStart, cursorPos.Y, bufferChar, bufferAttr)); + } + + // 2. Cursor Line + auto prefixPos = COORD{ 0, cursorPos.Y }; + auto suffixPos = cursorPos; + // When erasing from the beginning, the cursor column is included in the range. + suffixPos.X += (eraseType == DispatchTypes::EraseType::FromBeginning); + size_t prefixWidth = suffixPos.X; + size_t suffixWidth = bufferWidth - prefixWidth; + if (eraseType == DispatchTypes::EraseType::ToEnd) + { + Log::Comment(L"The start of the cursor line should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains(prefixPos, bufferChar, bufferAttr, prefixWidth)); + Log::Comment(L"The end of the cursor line should be erased."); + VERIFY_IS_TRUE(_ValidateLineContains(suffixPos, L' ', expectedFillAttr, suffixWidth)); + } + if (eraseType == DispatchTypes::EraseType::FromBeginning) + { + Log::Comment(L"The start of the cursor line should be erased."); + VERIFY_IS_TRUE(_ValidateLineContains(prefixPos, L' ', expectedFillAttr, prefixWidth)); + Log::Comment(L"The end of the cursor line should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLineContains(suffixPos, bufferChar, bufferAttr, suffixWidth)); + } + if (eraseType == DispatchTypes::EraseType::All) + { + Log::Comment(L"The entire cursor line should be erased."); + VERIFY_IS_TRUE(_ValidateLineContains(cursorPos.Y, L' ', expectedFillAttr)); + } + + // 3. Lines after cursor line + if (eraseScreen && eraseType != DispatchTypes::EraseType::FromBeginning) + { + // For eraseScreen, if we're not erasing from the beginning, these rows will be cleared. + Log::Comment(L"Lines after the cursor line should be erased."); + VERIFY_IS_TRUE(_ValidateLinesContain(cursorPos.Y + 1, viewportEnd, L' ', expectedFillAttr)); + } + else + { + // Otherwise we'll be left with the original buffer content. + Log::Comment(L"Lines after the cursor line should remain unchanged."); + VERIFY_IS_TRUE(_ValidateLinesContain(cursorPos.Y + 1, viewportEnd, bufferChar, bufferAttr)); + } +} + void _CommonScrollingSetup() { // Used for testing MSFT:20204600 @@ -4290,6 +4598,7 @@ void ScreenBufferTests::HardResetBuffer() VERIFY_IS_TRUE(isBufferClear()); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), viewport.Origin()); VERIFY_ARE_EQUAL(COORD({ 0, 0 }), cursor.GetPosition()); + VERIFY_ARE_EQUAL(TextAttribute{}, si.GetAttributes()); } void ScreenBufferTests::RestoreDownAltBufferWithTerminalScrolling() diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index d539eb8c551..9caa521250f 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -516,11 +516,6 @@ bool AdaptDispatch::_InsertDeleteHelper(_In_ unsigned int const uiCount, const b coordDestination.Y = cursor.Y; coordDestination.X = cursor.X; - // Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line) - CHAR_INFO ciFill; - ciFill.Attributes = csbiex.wAttributes; - ciFill.Char.UnicodeChar = L' '; - bool fSuccess = false; if (fIsInsert) { @@ -535,10 +530,8 @@ bool AdaptDispatch::_InsertDeleteHelper(_In_ unsigned int const uiCount, const b if (fSuccess) { - fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScroll, - &srScroll, - coordDestination, - &ciFill); + // Note the revealed characters are filled with the standard erase attributes. + fSuccess = !!_conApi->PrivateScrollRegion(srScroll, srScroll, coordDestination, true); } return fSuccess; @@ -567,54 +560,6 @@ bool AdaptDispatch::DeleteCharacter(_In_ unsigned int const uiCount) { return _InsertDeleteHelper(uiCount, false); } -// Routine Description: -// - Internal helper to erase a specific number of characters in one particular line of the buffer. -// Erased positions are replaced with spaces. -// Arguments: -// - coordStartPosition - The position to begin erasing at. -// - dwLength - the number of characters to erase. -// - wFillColor - The attributes to apply to the erased positions. -// Return Value: -// - True if handled successfully. False otherwise. -bool AdaptDispatch::_EraseSingleLineDistanceHelper(const COORD coordStartPosition, const DWORD dwLength, const WORD wFillColor) const -{ - WCHAR const wchSpace = static_cast(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20. - - size_t written = 0; - bool fSuccess = !!_conApi->FillConsoleOutputCharacterW(wchSpace, dwLength, coordStartPosition, written); - - if (fSuccess) - { - fSuccess = !!_conApi->FillConsoleOutputAttribute(wFillColor, dwLength, coordStartPosition, written); - } - - return fSuccess; -} - -bool AdaptDispatch::_EraseAreaHelper(const COORD coordStartPosition, const COORD coordLastPosition, const WORD wFillColor) -{ - WCHAR const wchSpace = static_cast(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20. - - size_t written = 0; - FAIL_FAST_IF(!(coordStartPosition.X < coordLastPosition.X)); - FAIL_FAST_IF(!(coordStartPosition.Y < coordLastPosition.Y)); - bool fSuccess = false; - for (short y = coordStartPosition.Y; y < coordLastPosition.Y; y++) - { - const COORD coordLine = { coordStartPosition.X, y }; - fSuccess = !!_conApi->FillConsoleOutputCharacterW(wchSpace, coordLastPosition.X - coordStartPosition.X, coordLine, written); - if (fSuccess) - { - fSuccess = !!_conApi->FillConsoleOutputAttribute(wFillColor, coordLastPosition.X - coordStartPosition.X, coordLine, written); - } - - if (!fSuccess) - { - break; - } - } - return fSuccess; -} // Routine Description: // - Internal helper to erase one particular line of the buffer. Either from beginning to the cursor, from the cursor to the end, or the entire line. @@ -626,7 +571,7 @@ bool AdaptDispatch::_EraseAreaHelper(const COORD coordStartPosition, const COORD // - This is not aware of circular buffer. Line 0 is always the top visible line if you scrolled the whole way up the window. // Return Value: // - True if handled successfully. False otherwise. -bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId, const WORD wFillColor) const +bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId) const { COORD coordStartPosition = { 0 }; coordStartPosition.Y = sLineId; @@ -637,7 +582,7 @@ bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* c { case DispatchTypes::EraseType::FromBeginning: case DispatchTypes::EraseType::All: - coordStartPosition.X = pcsbiex->srWindow.Left; // from beginning and the whole line start from the left viewport edge. + coordStartPosition.X = 0; // from beginning and the whole line start from the left most edge of the buffer. break; case DispatchTypes::EraseType::ToEnd: coordStartPosition.X = pcsbiex->dwCursorPosition.X; // from the current cursor position (including it) @@ -651,16 +596,17 @@ bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* c { case DispatchTypes::EraseType::FromBeginning: // +1 because if cursor were at the left edge, the length would be 0 and we want to paint at least the 1 character the cursor is on. - nLength = (pcsbiex->dwCursorPosition.X - pcsbiex->srWindow.Left) + 1; + nLength = pcsbiex->dwCursorPosition.X + 1; break; case DispatchTypes::EraseType::ToEnd: case DispatchTypes::EraseType::All: - // Remember the .Right value is 1 farther than the right most displayed character in the viewport. Therefore no +1. - nLength = pcsbiex->srWindow.Right - coordStartPosition.X; + // Remember the .X value is 1 farther than the right most column in the buffer. Therefore no +1. + nLength = pcsbiex->dwSize.X - coordStartPosition.X; break; } - return _EraseSingleLineDistanceHelper(coordStartPosition, nLength, wFillColor); + // Note that the region is filled with the standard erase attributes. + return !!_conApi->PrivateFillRegion(coordStartPosition, nLength, L' ', true); } // Routine Description: @@ -682,12 +628,13 @@ bool AdaptDispatch::EraseCharacters(_In_ unsigned int const uiNumChars) { const COORD coordStartPosition = csbiex.dwCursorPosition; - const SHORT sRemainingSpaces = csbiex.srWindow.Right - coordStartPosition.X; + const SHORT sRemainingSpaces = csbiex.dwSize.X - coordStartPosition.X; const unsigned short usActualRemaining = (sRemainingSpaces < 0) ? 0 : sRemainingSpaces; // erase at max the number of characters remaining in the line from the current position. const DWORD dwEraseLength = (uiNumChars <= usActualRemaining) ? uiNumChars : usActualRemaining; - fSuccess = _EraseSingleLineDistanceHelper(coordStartPosition, dwEraseLength, csbiex.wAttributes); + // Note that the region is filled with the standard erase attributes. + fSuccess = !!_conApi->PrivateFillRegion(coordStartPosition, dwEraseLength, L' ', true); } return fSuccess; } @@ -741,7 +688,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // For beginning and all, erase all complete lines before (above vertically) from the cursor position. for (SHORT sStartLine = csbiex.srWindow.Top; sStartLine < csbiex.dwCursorPosition.Y; sStartLine++) { - fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine, csbiex.wAttributes); + fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine); if (!fSuccess) { @@ -753,7 +700,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) if (fSuccess) { // 2. Cursor Line - fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y, csbiex.wAttributes); + fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y); } if (fSuccess) @@ -765,7 +712,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // Remember that the viewport bottom value is 1 beyond the viewable area of the viewport. for (SHORT sStartLine = csbiex.dwCursorPosition.Y + 1; sStartLine < csbiex.srWindow.Bottom; sStartLine++) { - fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine, csbiex.wAttributes); + fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine); if (!fSuccess) { @@ -793,7 +740,7 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) if (fSuccess) { - fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y, csbiex.wAttributes); + fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y); } return fSuccess; @@ -963,11 +910,8 @@ bool AdaptDispatch::_ScrollMovement(const ScrollDirection sdDirection, _In_ unsi coordDestination.X = srScreen.Left; coordDestination.Y = srScreen.Top + sDistance * (sdDirection == ScrollDirection::Up ? -1 : 1); - // Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line) - CHAR_INFO ciFill; - ciFill.Attributes = csbiex.wAttributes; - ciFill.Char.UnicodeChar = L' '; - fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScreen, &srScreen, coordDestination, &ciFill); + // Note the revealed lines are filled with the standard erase attributes. + fSuccess = !!_conApi->PrivateScrollRegion(srScreen, srScreen, coordDestination, true); } } @@ -1578,64 +1522,47 @@ bool AdaptDispatch::_EraseScrollback() if (fSuccess) { const SMALL_RECT Screen = csbiex.srWindow; - const short sWidth = Screen.Right - Screen.Left; const short sHeight = Screen.Bottom - Screen.Top; - FAIL_FAST_IF(!(sWidth > 0 && sHeight > 0)); + FAIL_FAST_IF(!(sHeight > 0)); const COORD Cursor = csbiex.dwCursorPosition; // Rectangle to cut out of the existing buffer + // It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width. SMALL_RECT srScroll = Screen; + srScroll.Left = 0; + srScroll.Right = SHORT_MAX; // Paste coordinate for cut text above COORD coordDestination; coordDestination.X = 0; coordDestination.Y = 0; - // Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line) - CHAR_INFO ciFill; - ciFill.Attributes = csbiex.wAttributes; - ciFill.Char.UnicodeChar = static_cast(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20. - fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScroll, nullptr, coordDestination, &ciFill); + // Typically a scroll operation should fill with standard erase attributes, but in + // this case we need to use the default attributes, hence standardFillAttrs is false. + fSuccess = !!_conApi->PrivateScrollRegion(srScroll, std::nullopt, coordDestination, false); if (fSuccess) { - // Clear everything after the viewport. This is two regions: - // A. below the viewport - // B. to the right of the viewport. - - // First clear section A + // Clear everything after the viewport. const DWORD dwTotalAreaBelow = csbiex.dwSize.X * (csbiex.dwSize.Y - sHeight); const COORD coordBelowStartPosition = { 0, sHeight }; - // We don't use the _EraseAreaHelper here because _EraseSingleLineDistanceHelper does it all in one operation - fSuccess = _EraseSingleLineDistanceHelper(coordBelowStartPosition, dwTotalAreaBelow, csbiex.wAttributes); + // Again we need to use the default attributes, hence standardFillAttrs is false. + fSuccess = _conApi->PrivateFillRegion(coordBelowStartPosition, dwTotalAreaBelow, L' ', false); if (fSuccess) { - // If there is a section B, clear it. - const COORD coordBottomRight = { csbiex.dwSize.X, coordBelowStartPosition.Y }; - const COORD coordRightStartPosition = { sWidth, 0 }; - if (coordBottomRight.X > coordRightStartPosition.X) - { - // We use the Area helper here because the Line helper would - // erase the parts of the screen we want to keep too - fSuccess = _EraseAreaHelper(coordRightStartPosition, coordBottomRight, csbiex.wAttributes); - } + // Move the viewport (CAN'T be done in one call with SetConsoleScreenBufferInfoEx, because legacy) + SMALL_RECT srNewViewport; + srNewViewport.Left = Screen.Left; + srNewViewport.Top = 0; + // SetConsoleWindowInfo uses an inclusive rect, while GetConsoleScreenBufferInfo is exclusive + srNewViewport.Right = Screen.Right - 1; + srNewViewport.Bottom = sHeight - 1; + fSuccess = !!_conApi->SetConsoleWindowInfo(true, &srNewViewport); if (fSuccess) { - // Move the viewport (CAN'T be done in one call with SetConsoleScreenBufferInfoEx, because legacy) - SMALL_RECT srNewViewport; - srNewViewport.Left = 0; - srNewViewport.Top = 0; - // SetConsoleWindowInfo uses an inclusive rect, while GetConsoleScreenBufferInfo is exclusive - srNewViewport.Right = sWidth - 1; - srNewViewport.Bottom = sHeight - 1; - fSuccess = !!_conApi->SetConsoleWindowInfo(true, &srNewViewport); - - if (fSuccess) - { - // Move the cursor to the same relative location. - const COORD newCursor = { Cursor.X - Screen.Left, Cursor.Y - Screen.Top }; - fSuccess = !!_conApi->SetConsoleCursorPosition(newCursor); - } + // Move the cursor to the same relative location. + const COORD newCursor = { Cursor.X, Cursor.Y - Screen.Top }; + fSuccess = !!_conApi->SetConsoleCursorPosition(newCursor); } } } diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 255dd2c2e6e..e4696ebe63d 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -123,10 +123,8 @@ namespace Microsoft::Console::VirtualTerminal bool _CursorMovement(const CursorDirection dir, _In_ unsigned int const uiDistance) const; bool _CursorMovePosition(_In_opt_ const unsigned int* const puiRow, _In_opt_ const unsigned int* const puiCol) const; - bool _EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId, const WORD wFillColor) const; + bool _EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId) const; void _SetGraphicsOptionHelper(const DispatchTypes::GraphicsOptions opt, _Inout_ WORD* const pAttr); - bool _EraseAreaHelper(const COORD coordStartPosition, const COORD coordLastPosition, const WORD wFillColor); - bool _EraseSingleLineDistanceHelper(const COORD coordStartPosition, const DWORD dwLength, const WORD wFillColor) const; bool _EraseScrollback(); bool _EraseAll(); void _SetGraphicsOptionHelper(const DispatchTypes::GraphicsOptions opt, _Inout_ WORD* const pAttr) const; diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp index b337fe2c7d5..c99a747fdf6 100644 --- a/src/terminal/adapter/conGetSet.hpp +++ b/src/terminal/adapter/conGetSet.hpp @@ -31,14 +31,6 @@ namespace Microsoft::Console::VirtualTerminal virtual BOOL SetConsoleScreenBufferInfoEx(const CONSOLE_SCREEN_BUFFER_INFOEX* const pConsoleScreenBufferInfoEx) = 0; virtual BOOL SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) = 0; virtual BOOL SetConsoleCursorPosition(const COORD coordCursorPosition) = 0; - virtual BOOL FillConsoleOutputCharacterW(const WCHAR wch, - const DWORD nLength, - const COORD dwWriteCoord, - size_t& numberOfCharsWritten) noexcept = 0; - virtual BOOL FillConsoleOutputAttribute(const WORD wAttribute, - const DWORD nLength, - const COORD dwWriteCoord, - size_t& numberOfAttrsWritten) noexcept = 0; virtual BOOL SetConsoleTextAttribute(const WORD wAttr) = 0; virtual BOOL PrivateSetLegacyAttributes(const WORD wAttr, @@ -57,10 +49,6 @@ namespace Microsoft::Console::VirtualTerminal virtual BOOL PrivateWriteConsoleInputW(_Inout_ std::deque>& events, _Out_ size_t& eventsWritten) = 0; - virtual BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle, - _In_opt_ const SMALL_RECT* pClipRectangle, - _In_ COORD dwDestinationOrigin, - const CHAR_INFO* pFill) = 0; virtual BOOL SetConsoleWindowInfo(const BOOL bAbsolute, const SMALL_RECT* const lpConsoleWindow) = 0; virtual BOOL PrivateSetCursorKeysMode(const bool fApplicationMode) = 0; @@ -110,5 +98,15 @@ namespace Microsoft::Console::VirtualTerminal virtual BOOL PrivateSetColorTableEntry(const short index, const COLORREF value) const = 0; virtual BOOL PrivateSetDefaultForeground(const COLORREF value) const = 0; virtual BOOL PrivateSetDefaultBackground(const COLORREF value) const = 0; + + virtual BOOL PrivateFillRegion(const COORD startPosition, + const size_t fillLength, + const wchar_t fillChar, + const bool standardFillAttrs) = 0; + + virtual BOOL PrivateScrollRegion(const SMALL_RECT scrollRect, + const std::optional clipRect, + const COORD destinationOrigin, + const bool standardFillAttrs) = 0; }; } diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 09258aca34b..d29f430f7d8 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -186,62 +186,6 @@ class TestGetSet final : public ConGetSet return _fPrivateAllowCursorBlinkingResult; } - BOOL FillConsoleOutputCharacterW(const WCHAR wch, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfCharsWritten) noexcept override - { - Log::Comment(L"FillConsoleOutputCharacterW MOCK called..."); - - DWORD dwCharsWritten = 0; - - if (_fFillConsoleOutputCharacterWResult) - { - Log::Comment(NoThrowString().Format(L"Filling (X: %d, Y:%d) for %d characters with '%c'...", dwWriteCoord.X, dwWriteCoord.Y, nLength, wch)); - - COORD dwCurrentPos = dwWriteCoord; - - while (dwCharsWritten < nLength) - { - CHAR_INFO* pchar = _GetCharAt(dwCurrentPos.Y, dwCurrentPos.X); - pchar->Char.UnicodeChar = wch; - dwCharsWritten++; - _IncrementCoordPos(&dwCurrentPos); - } - } - - numberOfCharsWritten = dwCharsWritten; - - Log::Comment(NoThrowString().Format(L"Fill wrote %d characters.", dwCharsWritten)); - - return _fFillConsoleOutputCharacterWResult; - } - - BOOL FillConsoleOutputAttribute(const WORD wAttribute, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfAttrsWritten) noexcept override - { - Log::Comment(L"FillConsoleOutputAttribute MOCK called..."); - - DWORD dwCharsWritten = 0; - - if (_fFillConsoleOutputAttributeResult) - { - Log::Comment(NoThrowString().Format(L"Filling (X: %d, Y:%d) for %d characters with 0x%x attribute...", dwWriteCoord.X, dwWriteCoord.Y, nLength, wAttribute)); - - COORD dwCurrentPos = dwWriteCoord; - - while (dwCharsWritten < nLength) - { - CHAR_INFO* pchar = _GetCharAt(dwCurrentPos.Y, dwCurrentPos.X); - pchar->Attributes = wAttribute; - dwCharsWritten++; - _IncrementCoordPos(&dwCurrentPos); - } - } - - numberOfAttrsWritten = dwCharsWritten; - - Log::Comment(NoThrowString().Format(L"Fill modified %d characters.", dwCharsWritten)); - - return _fFillConsoleOutputAttributeResult; - } - BOOL SetConsoleTextAttribute(const WORD wAttr) override { Log::Comment(L"SetConsoleTextAttribute MOCK called..."); @@ -401,13 +345,6 @@ class TestGetSet final : public ConGetSet return _fPrivateWriteConsoleControlInputResult; } - 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..."); - - return _fScrollConsoleScreenBufferWResult; - } - BOOL PrivateSetScrollingRegion(const SMALL_RECT* const psrScrollMargins) override { Log::Comment(L"PrivateSetScrollingRegion MOCK called..."); @@ -711,20 +648,24 @@ class TestGetSet final : public ConGetSet return _fPrivateSetDefaultBackgroundResult; } - void _IncrementCoordPos(_Inout_ COORD* pcoord) + BOOL PrivateFillRegion(const COORD /*startPosition*/, + const size_t /*fillLength*/, + const wchar_t /*fillChar*/, + const bool /*standardFillAttrs*/) noexcept override { - pcoord->X++; + Log::Comment(L"PrivateFillRegion MOCK called..."); - if (pcoord->X >= _coordBufferSize.X) - { - pcoord->X = 0; - pcoord->Y++; + return TRUE; + } - if (pcoord->Y >= _coordBufferSize.Y) - { - pcoord->Y = _coordBufferSize.Y - 1; - } - } + BOOL PrivateScrollRegion(const SMALL_RECT /*scrollRect*/, + const std::optional /*clipRect*/, + const COORD /*destinationOrigin*/, + const bool /*standardFillAttrs*/) noexcept override + { + Log::Comment(L"PrivateScrollRegion MOCK called..."); + + return TRUE; } void PrepData() @@ -752,11 +693,6 @@ class TestGetSet final : public ConGetSet } void PrepData(CursorX xact, CursorY yact) - { - PrepData(xact, yact, s_wchDefault, s_wDefaultAttribute); - } - - void PrepData(CursorX xact, CursorY yact, WCHAR wch, WORD wAttr) { Log::Comment(L"Resetting mock data state."); @@ -765,18 +701,16 @@ class TestGetSet final : public ConGetSet _fGetConsoleScreenBufferInfoExResult = TRUE; _fGetConsoleCursorInfoResult = TRUE; _fSetConsoleCursorInfoResult = TRUE; - _fFillConsoleOutputCharacterWResult = TRUE; - _fFillConsoleOutputAttributeResult = TRUE; _fSetConsoleTextAttributeResult = TRUE; _fPrivateWriteConsoleInputWResult = TRUE; _fPrivatePrependConsoleInputResult = TRUE; _fPrivateWriteConsoleControlInputResult = TRUE; - _fScrollConsoleScreenBufferWResult = TRUE; _fSetConsoleWindowInfoResult = TRUE; _fPrivateGetConsoleScreenBufferAttributesResult = TRUE; _fMoveToBottomResult = true; - _PrepCharsBuffer(wch, wAttr); + _coordBufferSize.X = 100; + _coordBufferSize.Y = 600; // Viewport sitting in the "middle" of the buffer somewhere (so all sides have excess buffer around them) _srViewport.Top = 20; @@ -839,47 +773,6 @@ class TestGetSet final : public ConGetSet _coordExpectedCursorPos = _coordCursorPos; } - void _PrepCharsBuffer() - { - _PrepCharsBuffer(s_wchDefault, s_wDefaultAttribute); - } - - void _PrepCharsBuffer(WCHAR const wch, WORD const wAttr) - { - // Buffer large - _coordBufferSize.X = 100; - _coordBufferSize.Y = 600; - - // Buffer data - _FreeCharsBuffer(); - - DWORD const cchTotalBufferSize = _coordBufferSize.Y * _coordBufferSize.X; - - _rgchars = new CHAR_INFO[cchTotalBufferSize]; - - COORD coordStart = { 0 }; - size_t written = 0; - - // Fill buffer with Zs. - Log::Comment(L"Filling buffer with characters so we can tell what's deleted."); - FillConsoleOutputCharacterW(wch, cchTotalBufferSize, coordStart, written); - - // Fill attributes with 0s - Log::Comment(L"Filling buffer with attributes so we can tell what happened."); - FillConsoleOutputAttribute(wAttr, cchTotalBufferSize, coordStart, written); - - VERIFY_ARE_EQUAL(((DWORD)cchTotalBufferSize), ((DWORD)written), L"Ensure the writer says all characters in the buffer were filled."); - } - - void _FreeCharsBuffer() - { - if (_rgchars != nullptr) - { - delete[] _rgchars; - _rgchars = nullptr; - } - } - void ValidateInputEvent(_In_ PCWSTR pwszExpectedResponse) { size_t const cchResponse = wcslen(pwszExpectedResponse); @@ -906,101 +799,6 @@ class TestGetSet final : public ConGetSet } } - bool ValidateEraseBufferState(SMALL_RECT* rgsrRegions, size_t cRegions, wchar_t wchExpectedInRegions, WORD wAttrExpectedInRegions) - { - bool fStateValid = true; - - Log::Comment(NoThrowString().Format(L"The following %zu regions are used as in-bounds for this test:", cRegions)); - for (size_t iRegion = 0; iRegion < cRegions; iRegion++) - { - SMALL_RECT srRegion = rgsrRegions[iRegion]; - - Log::Comment(NoThrowString().Format(L"#%zu - (T: %d, B: %d, L: %d, R:%d)", iRegion, srRegion.Top, srRegion.Bottom, srRegion.Left, srRegion.Right)); - } - - Log::Comment(L"Now checking every character within the buffer..."); - for (short iRow = 0; iRow < _coordBufferSize.Y; iRow++) - { - for (short iCol = 0; iCol < _coordBufferSize.X; iCol++) - { - CHAR_INFO* pchar = _GetCharAt(iRow, iCol); - - bool const fIsInclusive = _IsAnyRegionInclusive(rgsrRegions, cRegions, iRow, iCol); - - WCHAR const wchExpected = fIsInclusive ? wchExpectedInRegions : TestGetSet::s_wchDefault; - - WORD const wAttrExpected = fIsInclusive ? wAttrExpectedInRegions : TestGetSet::s_wDefaultAttribute; - - if (pchar->Char.UnicodeChar != wchExpected) - { - fStateValid = false; - - Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected: '%c'. Actual: '%c'", iCol, iRow, wchExpected, pchar->Char.UnicodeChar)); - - break; - } - - if (pchar->Attributes != wAttrExpected) - { - fStateValid = false; - - Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected Attr: 0x%x. Actual Attr: 0x%x", iCol, iRow, wAttrExpected, pchar->Attributes)); - - break; - } - } - - if (!fStateValid) - { - break; - } - } - - return fStateValid; - } - - bool _IsAnyRegionInclusive(SMALL_RECT* rgsrRegions, size_t cRegions, short sRow, short sCol) - { - bool fIncludesChar = false; - - for (size_t iRegion = 0; iRegion < cRegions; iRegion++) - { - fIncludesChar = _IsInRegionInclusive(rgsrRegions[iRegion], sRow, sCol); - - if (fIncludesChar) - { - break; - } - } - - return fIncludesChar; - } - - bool _IsInRegionInclusive(SMALL_RECT srRegion, short sRow, short sCol) - { - return srRegion.Left <= sCol && - srRegion.Right >= sCol && - srRegion.Top <= sRow && - srRegion.Bottom >= sRow; - } - - CHAR_INFO* _GetCharAt(size_t const iRow, size_t const iCol) - { - CHAR_INFO* pchar = nullptr; - - if (_rgchars != nullptr) - { - pchar = &(_rgchars[(iRow * _coordBufferSize.X) + iCol]); - } - - if (pchar == nullptr) - { - VERIFY_FAIL(L"Failed to retrieve character position from buffer."); - } - - return pchar; - } - void _SetMarginsHelper(SMALL_RECT* rect, SHORT top, SHORT bottom) { rect->Top = top; @@ -1012,7 +810,6 @@ class TestGetSet final : public ConGetSet ~TestGetSet() { - _FreeCharsBuffer(); } static const WCHAR s_wchErase = (WCHAR)0x20; @@ -1021,7 +818,6 @@ class TestGetSet final : public ConGetSet static const WORD s_wDefaultAttribute = 0; static const WORD s_wDefaultFill = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; // dark gray on black. - CHAR_INFO* _rgchars = nullptr; std::deque> _events; COORD _coordBufferSize = { 0, 0 }; @@ -1063,13 +859,10 @@ class TestGetSet final : public ConGetSet BOOL _fSetConsoleCursorPositionResult = false; BOOL _fGetConsoleCursorInfoResult = false; BOOL _fSetConsoleCursorInfoResult = false; - BOOL _fFillConsoleOutputCharacterWResult = false; - BOOL _fFillConsoleOutputAttributeResult = false; BOOL _fSetConsoleTextAttributeResult = false; BOOL _fPrivateWriteConsoleInputWResult = false; BOOL _fPrivatePrependConsoleInputResult = false; BOOL _fPrivateWriteConsoleControlInputResult = false; - BOOL _fScrollConsoleScreenBufferWResult = false; BOOL _fSetConsoleWindowInfoResult = false; BOOL _fExpectedWindowAbsolute = false; @@ -1652,247 +1445,6 @@ class AdapterTest VERIFY_IS_FALSE(_pDispatch->CursorVisibility(fEnd)); } - // 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 - // scrollback content. - TEST_METHOD(EraseScrollbackTests) - { - _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); - _testGetSet->_wAttribute = _testGetSet->s_wAttrErase; - Log::Comment(L"Starting Test"); - - _testGetSet->_fSetConsoleWindowInfoResult = true; - _testGetSet->_fExpectedWindowAbsolute = true; - SMALL_RECT srRegion = { 0 }; - srRegion.Bottom = _testGetSet->_srViewport.Bottom - _testGetSet->_srViewport.Top - 1; - srRegion.Right = _testGetSet->_srViewport.Right - _testGetSet->_srViewport.Left - 1; - _testGetSet->_srExpectedConsoleWindow = srRegion; - - // The cursor will be moved to the same relative location in the new viewport with origin @ 0, 0 - const COORD coordRelativeCursor = { _testGetSet->_coordCursorPos.X - _testGetSet->_srViewport.Left, - _testGetSet->_coordCursorPos.Y - _testGetSet->_srViewport.Top }; - _testGetSet->_coordExpectedCursorPos = coordRelativeCursor; - - VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback)); - - // There are two portions of the screen that are cleared - - // below the viewport and to the right of the viewport. - size_t cRegionsToCheck = 2; - SMALL_RECT rgsrRegionsModified[2]; - - // Region 0 - Below the viewport - srRegion.Top = _testGetSet->_srViewport.Bottom + 1; - srRegion.Left = 0; - - srRegion.Bottom = _testGetSet->_coordBufferSize.Y; - srRegion.Right = _testGetSet->_coordBufferSize.X; - - rgsrRegionsModified[0] = srRegion; - - // Region 1 - To the right of the viewport - srRegion.Top = 0; - srRegion.Left = _testGetSet->_srViewport.Right + 1; - - srRegion.Bottom = _testGetSet->_coordBufferSize.Y; - srRegion.Right = _testGetSet->_coordBufferSize.X; - - rgsrRegionsModified[1] = srRegion; - - // Scan entire buffer and ensure only the necessary region has changed. - bool fRegionSuccess = _testGetSet->ValidateEraseBufferState(rgsrRegionsModified, cRegionsToCheck, TestGetSet::s_wchErase, TestGetSet::s_wAttrErase); - VERIFY_IS_TRUE(fRegionSuccess); - - Log::Comment(L"Test 2: Gracefully fail when getting console information fails."); - _testGetSet->PrepData(); - _testGetSet->_fGetConsoleScreenBufferInfoExResult = false; - - VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback)); - - Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails."); - _testGetSet->PrepData(); - _testGetSet->_fFillConsoleOutputCharacterWResult = false; - - VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback)); - } - - TEST_METHOD(EraseTests) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:uiEraseType", L"{0, 1, 2}") // corresponds to options in DispatchTypes::EraseType - TEST_METHOD_PROPERTY(L"Data:fEraseScreen", L"{FALSE, TRUE}") // corresponds to Line (FALSE) or Screen (TRUE) - END_TEST_METHOD_PROPERTIES() - - // Modify variables based on type of this test - DispatchTypes::EraseType eraseType; - unsigned int uiEraseType; - VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiEraseType", uiEraseType)); - eraseType = (DispatchTypes::EraseType)uiEraseType; - - bool fEraseScreen; - VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"fEraseScreen", fEraseScreen)); - - Log::Comment(L"Starting test..."); - - // This combiniation is a simple VT api call - // Verify that the adapter calls that function, and do nothing else. - // This functionality is covered by ScreenBufferTests::EraseAllTests - if (eraseType == DispatchTypes::EraseType::All && fEraseScreen) - { - Log::Comment(L"Testing Erase in Display - All"); - VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(eraseType)); - return; - } - - Log::Comment(L"Test 1: Perform standard erase operation."); - switch (eraseType) - { - case DispatchTypes::EraseType::FromBeginning: - Log::Comment(L"Erasing line from beginning to cursor."); - break; - case DispatchTypes::EraseType::ToEnd: - Log::Comment(L"Erasing line from cursor to end."); - break; - case DispatchTypes::EraseType::All: - Log::Comment(L"Erasing all."); - break; - default: - VERIFY_FAIL(L"Unsupported erase type."); - } - - if (!fEraseScreen) - { - Log::Comment(L"Erasing just one line (the cursor's line)."); - } - else - { - Log::Comment(L"Erasing entire display (viewport). May be bounded by the cursor."); - } - - _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); - _testGetSet->_wAttribute = _testGetSet->s_wAttrErase; - - if (!fEraseScreen) - { - VERIFY_IS_TRUE(_pDispatch->EraseInLine(eraseType)); - } - else - { - VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(eraseType)); - } - - // Will be always the region of the cursor line (minimum 1) - // and 2 more if it's the display (for the regions before and after the cursor line, total 3) - SMALL_RECT rgsrRegionsModified[3]; // max of 3 regions. - - // Determine selection rectangle for line containing the cursor. - // All sides are inclusive of modified data. (unlike viewport normally) - SMALL_RECT srRegion = { 0 }; - srRegion.Top = _testGetSet->_coordCursorPos.Y; - srRegion.Bottom = srRegion.Top; - - switch (eraseType) - { - case DispatchTypes::EraseType::FromBeginning: - case DispatchTypes::EraseType::All: - srRegion.Left = _testGetSet->_srViewport.Left; - break; - case DispatchTypes::EraseType::ToEnd: - srRegion.Left = _testGetSet->_coordCursorPos.X; - break; - default: - VERIFY_FAIL(L"Unsupported erase type."); - break; - } - - switch (eraseType) - { - case DispatchTypes::EraseType::FromBeginning: - srRegion.Right = _testGetSet->_coordCursorPos.X; - break; - case DispatchTypes::EraseType::All: - case DispatchTypes::EraseType::ToEnd: - srRegion.Right = _testGetSet->_srViewport.Right - 1; - break; - default: - VERIFY_FAIL(L"Unsupported erase type."); - break; - } - rgsrRegionsModified[0] = srRegion; - - size_t cRegionsToCheck = 1; // start with 1 region to check from the line above. We may add up to 2 more. - - // Need to calculate up to two more regions if this is a screen erase. - if (fEraseScreen) - { - // If from beginning or all, add the region *before* the cursor line. - if (eraseType == DispatchTypes::EraseType::FromBeginning || - eraseType == DispatchTypes::EraseType::All) - { - srRegion.Left = _testGetSet->_srViewport.Left; - srRegion.Right = _testGetSet->_srViewport.Right - 1; // viewport is exclusive on the right. this test is inclusive so -1. - srRegion.Top = _testGetSet->_srViewport.Top; - - srRegion.Bottom = _testGetSet->_coordCursorPos.Y - 1; // this might end up being above top. This will be checked below. - - // Only add it if this is still valid. - if (srRegion.Bottom >= srRegion.Top) - { - rgsrRegionsModified[cRegionsToCheck] = srRegion; - cRegionsToCheck++; - } - } - - // If from end or all, add the region *after* the cursor line. - if (eraseType == DispatchTypes::EraseType::ToEnd || - eraseType == DispatchTypes::EraseType::All) - { - srRegion.Left = _testGetSet->_srViewport.Left; - srRegion.Right = _testGetSet->_srViewport.Right - 1; // viewport is exclusive rectangle on the right. this test uses inclusive rectangles so -1. - srRegion.Bottom = _testGetSet->_srViewport.Bottom - 1; // viewport is exclusive rectangle on the bottom. this test uses inclusive rectangles so -1; - - srRegion.Top = _testGetSet->_coordCursorPos.Y + 1; // this might end up being below bottom. This will be checked below. - - // Only add it if this is still valid. - if (srRegion.Bottom >= srRegion.Top) - { - rgsrRegionsModified[cRegionsToCheck] = srRegion; - cRegionsToCheck++; - } - } - } - - // Scan entire buffer and ensure only the necessary region has changed. - bool fRegionSuccess = _testGetSet->ValidateEraseBufferState(rgsrRegionsModified, cRegionsToCheck, TestGetSet::s_wchErase, TestGetSet::s_wAttrErase); - VERIFY_IS_TRUE(fRegionSuccess); - - Log::Comment(L"Test 2: Gracefully fail when getting console information fails."); - _testGetSet->PrepData(); - _testGetSet->_fGetConsoleScreenBufferInfoExResult = false; - - if (!fEraseScreen) - { - VERIFY_IS_FALSE(_pDispatch->EraseInLine(eraseType)); - } - else - { - VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(eraseType)); - } - - Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails."); - _testGetSet->PrepData(); - _testGetSet->_fFillConsoleOutputCharacterWResult = false; - - if (!fEraseScreen) - { - VERIFY_IS_FALSE(_pDispatch->EraseInLine(eraseType)); - } - else - { - VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(eraseType)); - } - } - TEST_METHOD(GraphicsBaseTests) { Log::Comment(L"Starting test..."); @@ -2692,75 +2244,6 @@ class AdapterTest VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); } - TEST_METHOD(HardReset) - { - Log::Comment(L"Starting test..."); - - _testGetSet->PrepData(); - - ///////////////// Components of a EraseScrollback ////////////////////// - _testGetSet->_fExpectedWindowAbsolute = true; - SMALL_RECT srRegion = { 0 }; - srRegion.Bottom = _testGetSet->_srViewport.Bottom - _testGetSet->_srViewport.Top - 1; - srRegion.Right = _testGetSet->_srViewport.Right - _testGetSet->_srViewport.Left - 1; - _testGetSet->_srExpectedConsoleWindow = srRegion; - // The cursor will be moved to the same relative location in the new viewport with origin @ 0, 0 - const COORD coordRelativeCursor = { _testGetSet->_coordCursorPos.X - _testGetSet->_srViewport.Left, - _testGetSet->_coordCursorPos.Y - _testGetSet->_srViewport.Top }; - const COORD coordExpectedCursorPos = { 0, 0 }; - - auto prepExpectedParameters = [&]() { - // Cursor to 1,1 - _testGetSet->_coordExpectedCursorPos = { 0, 0 }; - _testGetSet->_fSetConsoleCursorPositionResult = true; - _testGetSet->_fPrivateSetLegacyAttributesResult = true; - _testGetSet->_fPrivateSetDefaultAttributesResult = true; - _testGetSet->_fPrivateBoldTextResult = true; - _testGetSet->_fExpectedForeground = true; - _testGetSet->_fExpectedBackground = true; - _testGetSet->_fExpectedMeta = true; - _testGetSet->_fExpectedIsBold = false; - _testGetSet->_expectedShowCursor = true; - _testGetSet->_privateShowCursorResult = true; - - // We're expecting _SetDefaultColorHelper to call - // PrivateSetLegacyAttributes with 0 as the wAttr param. - _testGetSet->_wExpectedAttribute = 0; - - // Prepare the results of SoftReset api calls - _testGetSet->_fPrivateSetCursorKeysModeResult = true; - _testGetSet->_fPrivateSetKeypadModeResult = true; - _testGetSet->_fGetConsoleScreenBufferInfoExResult = true; - _testGetSet->_fPrivateSetScrollingRegionResult = true; - }; - prepExpectedParameters(); - - VERIFY_IS_TRUE(_pDispatch->HardReset()); - VERIFY_ARE_EQUAL(_testGetSet->_coordCursorPos, coordExpectedCursorPos); - VERIFY_ARE_EQUAL(_testGetSet->_fUsingRgbColor, false); - - Log::Comment(L"Test 2: Gracefully fail when getting console information fails."); - _testGetSet->PrepData(); - prepExpectedParameters(); - _testGetSet->_fGetConsoleScreenBufferInfoExResult = false; - - VERIFY_IS_FALSE(_pDispatch->HardReset()); - - Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails."); - _testGetSet->PrepData(); - prepExpectedParameters(); - _testGetSet->_fFillConsoleOutputCharacterWResult = false; - - VERIFY_IS_FALSE(_pDispatch->HardReset()); - - Log::Comment(L"Test 4: Gracefully fail when setting the window fails."); - _testGetSet->PrepData(); - prepExpectedParameters(); - _testGetSet->_fSetConsoleWindowInfoResult = false; - - VERIFY_IS_FALSE(_pDispatch->HardReset()); - } - TEST_METHOD(SetColorTableValue) { _testGetSet->PrepData();