diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 4d1a25b198..40a506c3b3 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -490,9 +490,7 @@ void Terminal::Write(std::wstring_view stringView) const til::point cursorPosAfter{ cursor.GetPosition() }; // Firing the CursorPositionChanged event is very expensive so we try not to - // do that when the cursor does not need to be redrawn. We don't do this - // inside _AdjustCursorPosition, only once we're done writing the whole run - // of output. + // do that when the cursor does not need to be redrawn. if (cursorPosBefore != cursorPosAfter) { _NotifyTerminalCursorPositionChanged(); @@ -1078,146 +1076,16 @@ Viewport Terminal::_GetVisibleViewport() const noexcept size); } -void Terminal::_AdjustCursorPosition(const til::point proposedPosition) +void Terminal::_PreserveUserScrollOffset(const int viewportDelta) noexcept { -#pragma warning(suppress : 26496) // cpp core checks wants this const but it's modified below. - auto proposedCursorPosition = proposedPosition; - auto& cursor = _activeBuffer().GetCursor(); - const auto bufferSize = _activeBuffer().GetSize(); - - // If we're about to scroll past the bottom of the buffer, instead cycle the - // buffer. - til::CoordType rowsPushedOffTopOfBuffer = 0; - const auto newRows = std::max(0, proposedCursorPosition.y - bufferSize.Height() + 1); - if (proposedCursorPosition.y >= bufferSize.Height()) + // When the mutable viewport is moved down, and there's an active selection, + // or the visible viewport isn't already at the bottom, then we want to keep + // the visible viewport where it is. To do this, we adjust the scroll offset + // by the same amount that we've just moved down. + if (viewportDelta > 0 && (IsSelectionActive() || _scrollOffset != 0)) { - for (auto dy = 0; dy < newRows; dy++) - { - _activeBuffer().IncrementCircularBuffer(); - proposedCursorPosition.y--; - rowsPushedOffTopOfBuffer++; - - // Update our selection too, so it doesn't move as the buffer is cycled - if (_selection) - { - // Stash this, so we can make sure to update the pivot to match later - const auto pivotWasStart = _selection->start == _selection->pivot; - // If the start of the selection is above 0, we can reduce both the start and end by 1 - if (_selection->start.y > 0) - { - _selection->start.y -= 1; - _selection->end.y -= 1; - } - else - { - // The start of the selection is at 0, if the end is greater than 0, then only reduce the end - if (_selection->end.y > 0) - { - _selection->start.x = 0; - _selection->end.y -= 1; - } - else - { - // Both the start and end of the selection are at 0, clear the selection - _selection.reset(); - } - } - - // If we still have a selection, make sure to sync the pivot - // with whichever value is the right one. - // - // Failure to do this might lead to GH #14462 - if (_selection.has_value()) - { - _selection->pivot = pivotWasStart ? _selection->start : _selection->end; - } - } - } - - // manually erase our pattern intervals since the locations have changed now - _patternIntervalTree = {}; - } - - // Update Cursor Position - cursor.SetPosition(proposedCursorPosition); - - // Move the viewport down if the cursor moved below the viewport. - // Obviously, don't need to do this in the alt buffer. - if (!_inAltBuffer()) - { - auto updatedViewport = false; - const auto scrollAmount = std::max(0, proposedCursorPosition.y - _mutableViewport.BottomInclusive()); - if (scrollAmount > 0) - { - const auto newViewTop = std::max(0, proposedCursorPosition.y - (_mutableViewport.Height() - 1)); - // In the alt buffer, we never need to adjust _mutableViewport, which is the viewport of the main buffer. - if (newViewTop != _mutableViewport.Top()) - { - _mutableViewport = Viewport::FromDimensions({ 0, newViewTop }, - _mutableViewport.Dimensions()); - updatedViewport = true; - } - } - - // If the viewport moved, or we circled the buffer, we might need to update - // our _scrollOffset - if (updatedViewport || newRows != 0) - { - const auto oldScrollOffset = _scrollOffset; - - // scroll if... - // - no selection is active - // - viewport is already at the bottom - const auto scrollToOutput = !IsSelectionActive() && _scrollOffset == 0; - - _scrollOffset = scrollToOutput ? 0 : _scrollOffset + scrollAmount + newRows; - - // Clamp the range to make sure that we don't scroll way off the top of the buffer - _scrollOffset = std::clamp(_scrollOffset, - 0, - _activeBuffer().GetSize().Height() - _mutableViewport.Height()); - - // If the new scroll offset is different, then we'll still want to raise a scroll event - updatedViewport = updatedViewport || (oldScrollOffset != _scrollOffset); - } - - // If the viewport moved, then send a scrolling notification. - if (updatedViewport) - { - _NotifyScrollEvent(); - } - } - - if (rowsPushedOffTopOfBuffer != 0) - { - if (_scrollMarks.size() > 0) - { - for (auto& mark : _scrollMarks) - { - // Move the mark up - mark.start.y -= rowsPushedOffTopOfBuffer; - - // If the mark had sub-regions, then move those pointers too - if (mark.commandEnd.has_value()) - { - (*mark.commandEnd).y -= rowsPushedOffTopOfBuffer; - } - if (mark.outputEnd.has_value()) - { - (*mark.outputEnd).y -= rowsPushedOffTopOfBuffer; - } - } - - _scrollMarks.erase(std::remove_if(_scrollMarks.begin(), - _scrollMarks.end(), - [](const VirtualTerminal::DispatchTypes::ScrollMark& m) { return m.start.y < 0; }), - _scrollMarks.end()); - } - // We have to report the delta here because we might have circled the text buffer. - // That didn't change the viewport and therefore the TriggerScroll(void) - // method can't detect the delta on its own. - const til::point delta{ 0, -rowsPushedOffTopOfBuffer }; - _activeBuffer().TriggerScroll(delta); + const auto maxScrollOffset = _activeBuffer().GetSize().Height() - _mutableViewport.Height(); + _scrollOffset = std::min(_scrollOffset + viewportDelta, maxScrollOffset); } } diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index d670275f0e..589b94b47b 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -113,10 +113,8 @@ public: void SetTextAttributes(const TextAttribute& attrs) noexcept override; void SetAutoWrapMode(const bool wrapAtEOL) noexcept override; bool GetAutoWrapMode() const noexcept override; - void SetScrollingRegion(const til::inclusive_rect& scrollMargins) noexcept override; void WarningBell() override; bool GetLineFeedMode() const noexcept override; - void LineFeed(const bool withReturn, const bool wrapForced) override; void SetWindowTitle(const std::wstring_view title) override; CursorType GetUserDefaultCursorStyle() const noexcept override; bool ResizeWindow(const til::CoordType width, const til::CoordType height) noexcept override; @@ -140,6 +138,7 @@ public: bool IsConsolePty() const noexcept override; bool IsVtInputEnabled() const noexcept override; void NotifyAccessibilityChange(const til::rect& changedRect) noexcept override; + void NotifyBufferRotation(const int delta) override; #pragma endregion void ClearMark(); @@ -420,7 +419,7 @@ private: Microsoft::Console::Types::Viewport _GetMutableViewport() const noexcept; Microsoft::Console::Types::Viewport _GetVisibleViewport() const noexcept; - void _AdjustCursorPosition(const til::point proposedPosition); + void _PreserveUserScrollOffset(const int viewportDelta) noexcept; void _NotifyScrollEvent() noexcept; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index c4ec65d62e..f861b16edc 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -48,9 +48,11 @@ void Terminal::SetViewportPosition(const til::point position) noexcept // The viewport is fixed at 0,0 for the alt buffer, so this is a no-op. if (!_inAltBuffer()) { + const auto viewportDelta = position.y - _GetMutableViewport().Origin().y; const auto dimensions = _GetMutableViewport().Dimensions(); _mutableViewport = Viewport::FromDimensions(position, dimensions); - Terminal::_NotifyScrollEvent(); + _PreserveUserScrollOffset(viewportDelta); + _NotifyScrollEvent(); } } @@ -70,11 +72,6 @@ bool Terminal::GetAutoWrapMode() const noexcept return true; } -void Terminal::SetScrollingRegion(const til::inclusive_rect& /*scrollMargins*/) noexcept -{ - // TODO: This will be needed to fully support DECSTBM. -} - void Terminal::WarningBell() { _pfnWarningBell(); @@ -86,22 +83,6 @@ bool Terminal::GetLineFeedMode() const noexcept return false; } -void Terminal::LineFeed(const bool withReturn, const bool wrapForced) -{ - auto cursorPos = _activeBuffer().GetCursor().GetPosition(); - - // If the line was forced to wrap, set the wrap status. - // When explicitly moving down a row, clear the wrap status. - _activeBuffer().GetRowByOffset(cursorPos.y).SetWrapForced(wrapForced); - - cursorPos.y++; - if (withReturn) - { - cursorPos.x = 0; - } - _AdjustCursorPosition(cursorPos); -} - void Terminal::SetWindowTitle(const std::wstring_view title) { if (!_suppressApplicationTitle) @@ -467,3 +448,62 @@ void Terminal::NotifyAccessibilityChange(const til::rect& /*changedRect*/) noexc { // This is only needed in conhost. Terminal handles accessibility in another way. } + +void Terminal::NotifyBufferRotation(const int delta) +{ + // Update our selection, so it doesn't move as the buffer is cycled + if (_selection) + { + // If the end of the selection will be out of range after the move, we just + // clear the selection. Otherwise we move both the start and end points up + // by the given delta and clamp to the first row. + if (_selection->end.y < delta) + { + _selection.reset(); + } + else + { + // Stash this, so we can make sure to update the pivot to match later. + const auto pivotWasStart = _selection->start == _selection->pivot; + _selection->start.y = std::max(_selection->start.y - delta, 0); + _selection->end.y = std::max(_selection->end.y - delta, 0); + // Make sure to sync the pivot with whichever value is the right one. + _selection->pivot = pivotWasStart ? _selection->start : _selection->end; + } + } + + // manually erase our pattern intervals since the locations have changed now + _patternIntervalTree = {}; + + const auto hasScrollMarks = _scrollMarks.size() > 0; + if (hasScrollMarks) + { + for (auto& mark : _scrollMarks) + { + // Move the mark up + mark.start.y -= delta; + + // If the mark had sub-regions, then move those pointers too + if (mark.commandEnd.has_value()) + { + (*mark.commandEnd).y -= delta; + } + if (mark.outputEnd.has_value()) + { + (*mark.outputEnd).y -= delta; + } + } + + _scrollMarks.erase(std::remove_if(_scrollMarks.begin(), + _scrollMarks.end(), + [](const auto& m) { return m.start.y < 0; }), + _scrollMarks.end()); + } + + const auto oldScrollOffset = _scrollOffset; + _PreserveUserScrollOffset(delta); + if (_scrollOffset != oldScrollOffset || hasScrollMarks) + { + _NotifyScrollEvent(); + } +} diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 1bb2f033e1..dc66b3a03e 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -1700,11 +1700,12 @@ void ConptyRoundtripTests::ScrollWithMargins() hostSm.ProcessString(completeCursorAtPromptLine); // Set up the verifications like above. - auto verifyBufferAfter = [&](const TextBuffer& tb) { + auto verifyBufferAfter = [&](const TextBuffer& tb, const auto panOffset) { auto& cursor = tb.GetCursor(); // Verify the cursor is waiting on the freshly revealed line (1 above mode line) // and in the left most column. - VERIFY_ARE_EQUAL(initialTermView.Height() - 2, cursor.GetPosition().y); + const auto bottomLine = initialTermView.BottomInclusive() + panOffset; + VERIFY_ARE_EQUAL(bottomLine - 1, cursor.GetPosition().y); VERIFY_ARE_EQUAL(0, cursor.GetPosition().x); // For all rows except the last two, verify that we have a run of four letters. @@ -1712,44 +1713,46 @@ void ConptyRoundtripTests::ScrollWithMargins() { // Start with B this time because the A line got scrolled off the top. const std::wstring expectedString(4, static_cast(L'B' + i)); - const til::point expectedPos{ 0, i }; + const til::point expectedPos{ 0, panOffset + i }; TestUtils::VerifyExpectedString(tb, expectedString, expectedPos); } // For the second to last row, verify that it is blank. { const std::wstring expectedBlankLine(initialTermView.Width(), L' '); - const til::point blankLinePos{ 0, rowsToWrite - 1 }; + const til::point blankLinePos{ 0, panOffset + rowsToWrite - 1 }; TestUtils::VerifyExpectedString(tb, expectedBlankLine, blankLinePos); } // For the last row, verify we have an entire row of asterisks for the mode line. { const std::wstring expectedModeLine(initialTermView.Width() - 1, L'*'); - const til::point modeLinePos{ 0, rowsToWrite }; + const til::point modeLinePos{ 0, panOffset + rowsToWrite }; TestUtils::VerifyExpectedString(tb, expectedModeLine, modeLinePos); } }; // This will verify the text emitted from the PTY. - expectedOutput.push_back("\x1b[H"); // cursor returns to top left corner. - for (auto i = 0; i < rowsToWrite - 1; ++i) + expectedOutput.push_back("\r\n"); // cursor moved to bottom left corner + expectedOutput.push_back("\n"); // linefeed pans the viewport down { - const std::string expectedString(4, static_cast('B' + i)); + // Cursor gets reset into second line from bottom, left most column + std::stringstream ss; + ss << "\x1b[" << initialTermView.Height() - 1 << ";1H"; + expectedOutput.push_back(ss.str()); + } + { + // Bottom of the scroll region is replaced with a blank line + const std::string expectedString(initialTermView.Width(), ' '); expectedOutput.push_back(expectedString); - expectedOutput.push_back("\x1b[K"); // erase the rest of the line. - expectedOutput.push_back("\r\n"); - } - { - expectedOutput.push_back(""); // nothing for the empty line - expectedOutput.push_back("\x1b[K"); // erase the rest of the line. - expectedOutput.push_back("\r\n"); } + expectedOutput.push_back("\r\n"); // cursor moved to bottom left corner { + // Mode line is redrawn at the bottom of the viewport const std::string expectedString(initialTermView.Width() - 1, '*'); - // There will be one extra blank space at the end of the line, to prevent delayed EOL wrapping - expectedOutput.push_back(expectedString + " "); + expectedOutput.push_back(expectedString); + expectedOutput.push_back(" "); } { // Cursor gets reset into second line from bottom, left most column @@ -1761,15 +1764,15 @@ void ConptyRoundtripTests::ScrollWithMargins() Log::Comment(L"Verify host buffer contains pattern moved up one and mode line still in place."); // Verify the host side. - verifyBufferAfter(hostTb); + verifyBufferAfter(hostTb, 0); Log::Comment(L"Emit PTY frame and validate it transmits the right data."); // Paint the frame VERIFY_SUCCEEDED(renderer.PaintFrame()); Log::Comment(L"Verify terminal buffer contains pattern moved up one and mode line still in place."); - // Verify the terminal side. - verifyBufferAfter(termTb); + // Verify the terminal side. Note the viewport has panned down a line. + verifyBufferAfter(termTb, 1); } void ConptyRoundtripTests::DontWrapMoveCursorInSingleFrame() diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index a86149cc6d..d6f1e87e80 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -44,7 +44,6 @@ using Microsoft::Console::VirtualTerminal::StateMachine; const BOOL fKeepCursorVisible, _Inout_opt_ til::CoordType* psScrollY) { - const auto inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); const auto bufferSize = screenInfo.GetBufferSize().Dimensions(); if (coordCursor.x < 0) { @@ -68,182 +67,10 @@ using Microsoft::Console::VirtualTerminal::StateMachine; } else { - if (inVtMode) - { - // In VT mode, the cursor must be left in the last column. - coordCursor.x = bufferSize.width - 1; - } - else - { - // For legacy apps, it is left where it was at the start of the write. - coordCursor.x = screenInfo.GetTextBuffer().GetCursor().GetPosition().x; - } + coordCursor.x = screenInfo.GetTextBuffer().GetCursor().GetPosition().x; } } - // 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(); - auto srMargins = screenInfo.GetAbsoluteScrollMargins().ToInclusive(); - const auto fMarginsSet = srMargins.bottom > srMargins.top; - auto currentCursor = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - const auto iCurrentCursorY = currentCursor.y; - - const auto fCursorInMargins = iCurrentCursorY <= srMargins.bottom && iCurrentCursorY >= srMargins.top; - const auto cursorAboveViewport = coordCursor.y < 0 && inVtMode; - const auto fScrollDown = fMarginsSet && fCursorInMargins && (coordCursor.y > srMargins.bottom); - auto fScrollUp = fMarginsSet && fCursorInMargins && (coordCursor.y < srMargins.top); - - const auto fScrollUpWithoutMargins = (!fMarginsSet) && cursorAboveViewport; - // if we're in VT mode, AND MARGINS AREN'T SET and a Reverse Line Feed took the cursor up past the top of the viewport, - // VT style scroll the contents of the screen. - // This can happen in applications like `less`, that don't set margins, because they're going to - // scroll the entire screen anyways, so no need for them to ever set the margins. - if (fScrollUpWithoutMargins) - { - fScrollUp = true; - srMargins.top = 0; - srMargins.bottom = screenInfo.GetViewport().BottomInclusive(); - } - - const auto scrollDownAtTop = fScrollDown && relativeMargins.Top() == 0; - if (scrollDownAtTop) - { - // We're trying to scroll down, and the top margin is at the top of the viewport. - // In this case, we want the lines that are "scrolled off" to appear in - // the scrollback instead of being discarded. - // To do this, we're going to scroll everything starting at the bottom - // margin down, then move the viewport down. - - const auto delta = coordCursor.y - srMargins.bottom; - til::inclusive_rect scrollRect; - scrollRect.left = 0; - scrollRect.top = srMargins.bottom + 1; // One below margins - scrollRect.bottom = bufferSize.height - 1; // -1, otherwise this would be an exclusive rect. - scrollRect.right = bufferSize.width - 1; // -1, otherwise this would be an exclusive rect. - - // This is the Y position we're moving the contents below the bottom margin to. - auto moveToYPosition = scrollRect.top + delta; - - // This is where the viewport will need to be to give the effect of - // scrolling the contents in the margins. - auto newViewTop = viewport.Top() + delta; - - // This is how many new lines need to be added to the buffer to support this operation. - const auto newRows = (viewport.BottomExclusive() + delta) - bufferSize.height; - - // If we're near the bottom of the buffer, we might need to insert some - // new rows at the bottom. - // If we do this, then the viewport is now one line higher than it used - // to be, so it needs to move down by one less line. - for (auto i = 0; i < newRows; i++) - { - screenInfo.GetTextBuffer().IncrementCircularBuffer(); - moveToYPosition--; - newViewTop--; - scrollRect.top--; - } - - const til::point newPostMarginsOrigin{ 0, moveToYPosition }; - const til::point newViewOrigin{ 0, newViewTop }; - - try - { - ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, fillAttributes); - } - CATCH_LOG(); - - // Move the viewport down - auto hr = screenInfo.SetViewportOrigin(true, newViewOrigin, true); - if (FAILED(hr)) - { - return NTSTATUS_FROM_HRESULT(hr); - } - // If we didn't actually move the viewport, it's because we're at the - // bottom of the buffer, and the top lines of the viewport have - // changed. Manually invalidate here, to make sure the screen - // displays the correct text. - if (newViewOrigin == viewport.Origin()) - { - // Inside this block, we're shifting down at the bottom. - // This means that we had something like this: - // AAAA - // BBBB - // CCCC - // DDDD - // EEEE - // - // Our margins were set for lines A-D, but not on line E. - // So we circled the whole buffer up by one: - // BBBB - // CCCC - // DDDD - // EEEE - // - // - // Then we scrolled the contents of everything OUTSIDE the margin frame down. - // BBBB - // CCCC - // DDDD - // - // EEEE - // - // And now we need to report that only the bottom line didn't "move" as we put the EEEE - // back where it started, but everything else moved. - // In this case, delta was 1. So the amount that moved is the entire viewport height minus the delta. - auto invalid = Viewport::FromDimensions(viewport.Origin(), { viewport.Width(), viewport.Height() - delta }); - screenInfo.GetTextBuffer().TriggerRedraw(invalid); - } - - // reset where our local viewport is, and recalculate the cursor and - // margin positions. - viewport = screenInfo.GetViewport(); - if (newRows > 0) - { - currentCursor.y -= newRows; - coordCursor.y -= newRows; - } - srMargins = screenInfo.GetAbsoluteScrollMargins().ToInclusive(); - } - - // If we did the above scrollDownAtTop case, then we've already scrolled - // the margins content, and we can skip this. - if (fScrollUp || (fScrollDown && !scrollDownAtTop)) - { - auto diff = coordCursor.y - (fScrollUp ? srMargins.top : srMargins.bottom); - - til::inclusive_rect scrollRect; - scrollRect.top = srMargins.top; - scrollRect.bottom = srMargins.bottom; - scrollRect.left = 0; // NOTE: Left/Right Scroll margins don't do anything currently. - scrollRect.right = bufferSize.width - 1; // -1, otherwise this would be an exclusive rect. - - til::point dest; - dest.x = scrollRect.left; - dest.y = scrollRect.top - diff; - - try - { - ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, fillAttributes); - } - CATCH_LOG(); - - coordCursor.y -= diff; - } - - // If the margins are set, then it shouldn't be possible for the cursor to - // move below the bottom of the viewport. Either it should be constrained - // inside the margins by one of the scrollDown cases handled above, or - // we'll need to clamp it inside the viewport here. - if (fMarginsSet && coordCursor.y > viewport.BottomInclusive()) - { - coordCursor.y = viewport.BottomInclusive(); - } - auto Status = STATUS_SUCCESS; if (coordCursor.y >= bufferSize.height) @@ -263,7 +90,6 @@ using Microsoft::Console::VirtualTerminal::StateMachine; } const auto cursorMovedPastViewport = coordCursor.y > screenInfo.GetViewport().BottomInclusive(); - const auto cursorMovedPastVirtualViewport = coordCursor.y > screenInfo.GetVirtualViewport().BottomInclusive(); if (SUCCEEDED_NTSTATUS(Status)) { // if at right or bottom edge of window, scroll right or down one char. @@ -283,20 +109,6 @@ using Microsoft::Console::VirtualTerminal::StateMachine; screenInfo.MakeCursorVisible(coordCursor); } Status = screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible); - - // MSFT:19989333 - Only re-initialize the cursor row if the cursor moved - // below the terminal section of the buffer (the virtual viewport), - // and the visible part of the buffer (the actual viewport). - // If this is only cursorMovedPastViewport, and you scroll up, then type - // a character, we'll re-initialize the line the cursor is on. - // If this is only cursorMovedPastVirtualViewport and you scroll down, - // (with terminal scrolling disabled) then all lines newly exposed - // will get their attributes constantly cleared out. - // Both cursorMovedPastViewport and cursorMovedPastVirtualViewport works - if (inVtMode && cursorMovedPastViewport && cursorMovedPastVirtualViewport) - { - screenInfo.InitializeCursorRowAttributes(); - } } return Status; diff --git a/src/host/output.cpp b/src/host/output.cpp index bbaceb9921..8b0ce0b7fd 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -302,8 +302,7 @@ 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. - const auto inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); - auto fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer(inVtMode); + auto fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer(); if (fSuccess) { // Trigger a graphical update if we're active. diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index dbe8f16857..82be404df6 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -138,26 +138,6 @@ bool ConhostInternalGetSet::GetAutoWrapMode() const return WI_IsFlagSet(outputMode, ENABLE_WRAP_AT_EOL_OUTPUT); } -// Routine Description: -// - Sets the top and bottom scrolling margins for the current page. This creates -// a subsection of the screen that scrolls when input reaches the end of the -// region, leaving the rest of the screen untouched. -// Arguments: -// - scrollMargins - A rect who's Top and Bottom members will be used to set -// the new values of the top and bottom margins. If (0,0), then the margins -// will be disabled. NOTE: This is a rect in the case that we'll need the -// left and right margins in the future. -// Return Value: -// - -void ConhostInternalGetSet::SetScrollingRegion(const til::inclusive_rect& scrollMargins) -{ - auto& screenInfo = _io.GetActiveOutputBuffer(); - auto srScrollMargins = screenInfo.GetRelativeScrollMargins().ToInclusive(); - srScrollMargins.top = scrollMargins.top; - srScrollMargins.bottom = scrollMargins.bottom; - screenInfo.SetScrollMargins(Viewport::FromInclusive(srScrollMargins)); -} - // Method Description: // - Retrieves the current Line Feed/New Line (LNM) mode. // Arguments: @@ -170,36 +150,6 @@ bool ConhostInternalGetSet::GetLineFeedMode() const return WI_IsFlagClear(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN); } -// Routine Description: -// - Performs a line feed, possibly preceded by carriage return. -// Arguments: -// - withReturn - Set to true if a carriage return should be performed as well. -// - wrapForced - Set to true is the line feed was the result of the line wrapping. -// Return Value: -// - -void ConhostInternalGetSet::LineFeed(const bool withReturn, const bool wrapForced) -{ - auto& screenInfo = _io.GetActiveOutputBuffer(); - auto& textBuffer = screenInfo.GetTextBuffer(); - auto cursorPosition = textBuffer.GetCursor().GetPosition(); - - // If the line was forced to wrap, set the wrap status. - // When explicitly moving down a row, clear the wrap status. - textBuffer.GetRowByOffset(cursorPosition.y).SetWrapForced(wrapForced); - - cursorPosition.y += 1; - if (withReturn) - { - cursorPosition.x = 0; - } - else - { - cursorPosition = textBuffer.ClampPositionWithinLine(cursorPosition); - } - - THROW_IF_NTSTATUS_FAILED(AdjustCursorPosition(screenInfo, cursorPosition, FALSE, nullptr)); -} - // Routine Description: // - Sends a notify message to play the "SystemHand" sound event. // Return Value: @@ -474,6 +424,25 @@ void ConhostInternalGetSet::NotifyAccessibilityChange(const til::rect& changedRe } } +// Routine Description: +// - Implements conhost-specific behavior when the buffer is rotated. +// Arguments: +// - delta - the number of cycles that the buffer has rotated. +// Return value: +// - +void ConhostInternalGetSet::NotifyBufferRotation(const int delta) +{ + auto& screenInfo = _io.GetActiveOutputBuffer(); + if (screenInfo.IsActiveScreenBuffer()) + { + auto pNotifier = ServiceLocator::LocateAccessibilityNotifier(); + if (pNotifier) + { + pNotifier->NotifyConsoleUpdateScrollEvent(0, -delta); + } + } +} + void ConhostInternalGetSet::MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& /*mark*/) { // Not implemented for conhost. diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index f673e9110b..030dae3f18 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -41,12 +41,9 @@ public: void SetAutoWrapMode(const bool wrapAtEOL) override; bool GetAutoWrapMode() const override; - void SetScrollingRegion(const til::inclusive_rect& scrollMargins) override; - void WarningBell() override; bool GetLineFeedMode() const override; - void LineFeed(const bool withReturn, const bool wrapForced) override; void SetWindowTitle(const std::wstring_view title) override; @@ -74,6 +71,7 @@ public: bool IsVtInputEnabled() const override; void NotifyAccessibilityChange(const til::rect& changedRect) override; + void NotifyBufferRotation(const int delta) override; void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) override; void MarkCommandStart() override; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 32395d2f52..029eca42b7 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -47,7 +47,6 @@ SCREEN_INFORMATION::SCREEN_INFORMATION( _pAccessibilityNotifier{ pNotifier }, _api{ *this }, _stateMachine{ nullptr }, - _scrollMargins{ Viewport::Empty() }, _viewport(Viewport::Empty()), _psiAlternateBuffer{ nullptr }, _psiMainBuffer{ nullptr }, @@ -1799,37 +1798,6 @@ void SCREEN_INFORMATION::MakeCursorVisible(const til::point CursorPosition) } } -// Method Description: -// - Sets the scroll margins for this buffer. -// Arguments: -// - margins: The new values of the scroll margins, *relative to the viewport* -void SCREEN_INFORMATION::SetScrollMargins(const Viewport margins) -{ - _scrollMargins = margins; -} - -// Method Description: -// - Returns the scrolling margins boundaries for this screen buffer, relative -// to the origin of the text buffer. Most callers will want the absolute -// positions of the margins, though they are set and stored relative to -// origin of the viewport. -// Arguments: -// - -Viewport SCREEN_INFORMATION::GetAbsoluteScrollMargins() const -{ - return _viewport.ConvertFromOrigin(_scrollMargins); -} - -// Method Description: -// - Returns the scrolling margins boundaries for this screen buffer, relative -// to the current viewport. -// Arguments: -// - -Viewport SCREEN_INFORMATION::GetRelativeScrollMargins() const -{ - return _scrollMargins; -} - // Routine Description: // - Retrieves the active buffer of this buffer. If this buffer has an // alternate buffer, this is the alternate buffer. Otherwise, it is this buffer. @@ -2641,34 +2609,6 @@ void SCREEN_INFORMATION::UpdateBottom() _virtualBottom = _viewport.BottomInclusive(); } -// Method Description: -// - 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. -// Our rows are usually pre-initialized, so re-initialize it here to -// emulate that behavior. -// See MSFT:17415310. -// Arguments: -// - -// Return Value: -// - -void SCREEN_INFORMATION::InitializeCursorRowAttributes() -{ - if (_textBuffer) - { - const auto& cursor = _textBuffer->GetCursor(); - auto& row = _textBuffer->GetRowByOffset(cursor.GetPosition().y); - // 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.SetAttrToEnd(0, fillAttributes); - // The row should also be single width to start with. - row.SetLineRendition(LineRendition::SingleWidth); - } -} - // Method Description: // - Returns the "virtual" Viewport - the viewport with its bottom at // `_virtualBottom`. For VT operations, this is essentially the mutable diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 6845f369ed..d67e3f5684 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -193,10 +193,6 @@ public: void MakeCursorVisible(const til::point CursorPosition); - Microsoft::Console::Types::Viewport GetRelativeScrollMargins() const; - Microsoft::Console::Types::Viewport GetAbsoluteScrollMargins() const; - void SetScrollMargins(const Microsoft::Console::Types::Viewport margins); - [[nodiscard]] NTSTATUS UseAlternateScreenBuffer(); void UseMainScreenBuffer(); @@ -226,8 +222,6 @@ public: FontInfoDesired& GetDesiredFont() noexcept; const FontInfoDesired& GetDesiredFont() const noexcept; - void InitializeCursorRowAttributes(); - void SetIgnoreLegacyEquivalentVTAttributes() noexcept; void ResetIgnoreLegacyEquivalentVTAttributes() noexcept; @@ -270,8 +264,6 @@ private: std::shared_ptr _stateMachine; - Microsoft::Console::Types::Viewport _scrollMargins; //The margins of the VT specified scroll region. Left and Right are currently unused, but could be in the future. - // Specifies which coordinates of the screen buffer are visible in the // window client (the "viewport" into the buffer) Microsoft::Console::Types::Viewport _viewport; diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 9539cfb17b..c7856418cc 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -1229,6 +1229,28 @@ void ScreenBufferTests::VtResizeComprehensive() VERIFY_ARE_EQUAL(expectedViewHeight, newViewHeight); } +til::rect _GetRelativeScrollMargins() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + auto& stateMachine = si.GetStateMachine(); + const auto viewport = si.GetViewport(); + auto& cursor = si.GetTextBuffer().GetCursor(); + const auto savePos = cursor.GetPosition(); + + // We can't access the AdaptDispatch internals where the margins are stored, + // but we calculate their boundaries by using VT sequences to move down and + // up as far as possible, and read the cursor positions at the two limits. + stateMachine.ProcessString(L"\033[H\033[9999B"); + const auto bottom = cursor.GetPosition().y - viewport.Top(); + stateMachine.ProcessString(L"\033[9999A"); + const auto top = cursor.GetPosition().y - viewport.Top(); + + cursor.SetPosition(savePos); + const auto noMargins = (top == 0 && bottom == viewport.Height() - 1); + return noMargins ? til::rect{} : til::rect{ 0, top, 0, bottom }; +} + void ScreenBufferTests::VtResizeDECCOLM() { // Run this test in isolation - for one reason or another, this breaks other tests. @@ -1252,13 +1274,13 @@ void ScreenBufferTests::VtResizeDECCOLM() return si.GetTextBuffer().GetCursor().GetPosition() - si.GetViewport().Origin(); }; auto areMarginsSet = [&]() { - const auto margins = si.GetRelativeScrollMargins(); - return margins.BottomInclusive() > margins.Top(); + const auto margins = _GetRelativeScrollMargins(); + return margins.bottom > margins.top; }; stateMachine.ProcessString(setInitialMargins); stateMachine.ProcessString(setInitialCursor); - auto initialMargins = si.GetRelativeScrollMargins(); + auto initialMargins = _GetRelativeScrollMargins(); auto initialCursorPosition = getRelativeCursorPosition(); auto initialSbHeight = si.GetBufferSize().Height(); @@ -1275,7 +1297,7 @@ void ScreenBufferTests::VtResizeDECCOLM() auto newViewWidth = si.GetViewport().Width(); VERIFY_IS_TRUE(areMarginsSet()); - VERIFY_ARE_EQUAL(initialMargins, si.GetRelativeScrollMargins()); + VERIFY_ARE_EQUAL(initialMargins, _GetRelativeScrollMargins()); VERIFY_ARE_EQUAL(initialCursorPosition, getRelativeCursorPosition()); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); @@ -1311,7 +1333,7 @@ void ScreenBufferTests::VtResizeDECCOLM() stateMachine.ProcessString(setInitialMargins); stateMachine.ProcessString(setInitialCursor); - initialMargins = si.GetRelativeScrollMargins(); + initialMargins = _GetRelativeScrollMargins(); initialCursorPosition = getRelativeCursorPosition(); initialSbHeight = newSbHeight; @@ -1329,7 +1351,7 @@ void ScreenBufferTests::VtResizeDECCOLM() newViewWidth = si.GetViewport().Width(); VERIFY_IS_TRUE(areMarginsSet()); - VERIFY_ARE_EQUAL(initialMargins, si.GetRelativeScrollMargins()); + VERIFY_ARE_EQUAL(initialMargins, _GetRelativeScrollMargins()); VERIFY_ARE_EQUAL(initialCursorPosition, getRelativeCursorPosition()); VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); @@ -1649,6 +1671,7 @@ void ScreenBufferTests::VtNewlineOutsideMargins() Log::Comment(L"Reset viewport and apply DECSTBM margins"); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, viewportTop }, true)); + si.UpdateBottom(); stateMachine.ProcessString(L"\x1b[1;5r"); // Make sure we clear the margins on exit so they can't break other tests. auto clearMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[r"); }); @@ -6389,8 +6412,8 @@ void ScreenBufferTests::ScreenAlignmentPattern() WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); auto areMarginsSet = [&]() { - const auto margins = si.GetRelativeScrollMargins(); - return margins.BottomInclusive() > margins.Top(); + const auto margins = _GetRelativeScrollMargins(); + return margins.bottom > margins.top; }; Log::Comment(L"Set the initial buffer state."); diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index 4705f38281..bcf635907a 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -51,10 +51,8 @@ namespace Microsoft::Console::VirtualTerminal virtual void SetAutoWrapMode(const bool wrapAtEOL) = 0; virtual bool GetAutoWrapMode() const = 0; - virtual void SetScrollingRegion(const til::inclusive_rect& scrollMargins) = 0; virtual void WarningBell() = 0; virtual bool GetLineFeedMode() const = 0; - virtual void LineFeed(const bool withReturn, const bool wrapForced) = 0; virtual void SetWindowTitle(const std::wstring_view title) = 0; virtual void UseAlternateScreenBuffer() = 0; virtual void UseMainScreenBuffer() = 0; @@ -77,6 +75,7 @@ namespace Microsoft::Console::VirtualTerminal virtual bool IsConsolePty() const = 0; virtual void NotifyAccessibilityChange(const til::rect& changedRect) = 0; + virtual void NotifyBufferRotation(const int delta) = 0; virtual void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) = 0; virtual void MarkCommandStart() = 0; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index ddf9d1bb6e..32c06d67bf 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -95,7 +95,7 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) // different position from where the EOL was marked. if (delayedCursorPosition == cursorPosition) { - _api.LineFeed(true, true); + _DoLineFeed(textBuffer, true, true); cursorPosition = cursor.GetPosition(); // We need to recalculate the width when moving to a new line. state.columnLimit = textBuffer.GetLineWidth(cursorPosition.y); @@ -248,14 +248,13 @@ bool AdaptDispatch::CursorPrevLine(const VTInt distance) // - absolute - Should coordinates be absolute or relative to the viewport. // Return Value: // - A std::pair containing the top and bottom coordinates (inclusive). -std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport, const bool absolute) +std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept { // If the top is out of range, reset the margins completely. const auto bottommostRow = viewport.bottom - viewport.top - 1; if (_scrollMargins.top >= bottommostRow) { _scrollMargins.top = _scrollMargins.bottom = 0; - _api.SetScrollingRegion(_scrollMargins); } // If margins aren't set, use the full extent of the viewport. const auto marginsSet = _scrollMargins.top < _scrollMargins.bottom; @@ -696,23 +695,11 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // by moving the current contents of the viewport into the scrollback. if (eraseType == DispatchTypes::EraseType::Scrollback) { - _EraseScrollback(); - // GH#2715 - If this succeeded, but we're in a conpty, return `false` to - // make the state machine propagate this ED sequence to the connected - // terminal application. While we're in conpty mode, we don't really - // have a scrollback, but the attached terminal might. - return !_api.IsConsolePty(); + return _EraseScrollback(); } else if (eraseType == DispatchTypes::EraseType::All) { - // GH#5683 - If this succeeded, but we're in a conpty, return `false` to - // make the state machine propagate this ED sequence to the connected - // terminal application. While we're in conpty mode, when the client - // requests a Erase All operation, we need to manually tell the - // connected terminal to do the same thing, so that the terminal will - // move it's own buffer contents into the scrollback. - _EraseAll(); - return !_api.IsConsolePty(); + return _EraseAll(); } const auto viewport = _api.GetViewport(); @@ -2039,7 +2026,6 @@ void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin, } _scrollMargins.top = actualTop; _scrollMargins.bottom = actualBottom; - _api.SetScrollingRegion(_scrollMargins); } } @@ -2088,6 +2074,94 @@ bool AdaptDispatch::CarriageReturn() return _CursorMovePosition(Offset::Unchanged(), Offset::Absolute(1), true); } +// Routine Description: +// - Helper method for executing a line feed, possibly preceded by carriage return. +// Arguments: +// - textBuffer - Target buffer on which the line feed is executed. +// - withReturn - Set to true if a carriage return should be performed as well. +// - wrapForced - Set to true is the line feed was the result of the line wrapping. +// Return Value: +// - +void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced) +{ + const auto viewport = _api.GetViewport(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto bufferWidth = textBuffer.GetSize().Width(); + const auto bufferHeight = textBuffer.GetSize().Height(); + + auto& cursor = textBuffer.GetCursor(); + const auto currentPosition = cursor.GetPosition(); + auto newPosition = currentPosition; + + // If the line was forced to wrap, set the wrap status. + // When explicitly moving down a row, clear the wrap status. + textBuffer.GetRowByOffset(currentPosition.y).SetWrapForced(wrapForced); + + if (currentPosition.y != bottomMargin) + { + // If we're not at the bottom margin then there's no scrolling, + // so we make sure we don't move past the bottom of the viewport. + newPosition.y = std::min(currentPosition.y + 1, viewport.bottom - 1); + newPosition = textBuffer.ClampPositionWithinLine(newPosition); + } + else if (topMargin > viewport.top) + { + // If the top margin isn't at the top of the viewport, then we're + // just scrolling the margin area and the cursor stays where it is. + _ScrollRectVertically(textBuffer, { 0, topMargin, bufferWidth, bottomMargin + 1 }, -1); + } + else if (viewport.bottom < bufferHeight) + { + // If the top margin is at the top of the viewport, then we'll scroll + // the content up by panning the viewport down, and also move the cursor + // down a row. But we only do this if the viewport hasn't yet reached + // the end of the buffer. + _api.SetViewportPosition({ viewport.left, viewport.top + 1 }); + newPosition.y++; + + // And if the bottom margin didn't cover the full viewport, we copy the + // lower part of the viewport down so it remains static. But for a full + // pan we reset the newly revealed row with the current attributes. + if (bottomMargin < viewport.bottom - 1) + { + _ScrollRectVertically(textBuffer, { 0, bottomMargin + 1, bufferWidth, viewport.bottom + 1 }, 1); + } + else + { + auto eraseAttributes = textBuffer.GetCurrentAttributes(); + eraseAttributes.SetStandardErase(); + textBuffer.GetRowByOffset(newPosition.y).Reset(eraseAttributes); + } + } + else + { + // If the viewport has reached the end of the buffer, we can't pan down, + // so we cycle the row coordinates, which effectively scrolls the buffer + // content up. In this case we don't need to move the cursor down. + textBuffer.IncrementCircularBuffer(true); + _api.NotifyBufferRotation(1); + + // We trigger a scroll rather than a redraw, since that's more efficient, + // but we need to turn the cursor off before doing so, otherwise a ghost + // cursor can be left behind in the previous position. + cursor.SetIsOn(false); + textBuffer.TriggerScroll({ 0, -1 }); + + // And again, if the bottom margin didn't cover the full viewport, we + // copy the lower part of the viewport down so it remains static. + if (bottomMargin < viewport.bottom - 1) + { + _ScrollRectVertically(textBuffer, { 0, bottomMargin, bufferWidth, bufferHeight }, 1); + } + } + + // If a carriage return was requested, we also move to the leftmost column. + newPosition.x = withReturn ? 0 : newPosition.x; + + cursor.SetPosition(newPosition); + _ApplyCursorMovementFlags(cursor); +} + // Routine Description: // - IND/NEL - Performs a line feed, possibly preceded by carriage return. // Moves the cursor down one line, and possibly also to the leftmost column. @@ -2097,16 +2171,17 @@ bool AdaptDispatch::CarriageReturn() // - True if handled successfully. False otherwise. bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType) { + auto& textBuffer = _api.GetTextBuffer(); switch (lineFeedType) { case DispatchTypes::LineFeedType::DependsOnMode: - _api.LineFeed(_api.GetLineFeedMode(), false); + _DoLineFeed(textBuffer, _api.GetLineFeedMode(), false); return true; case DispatchTypes::LineFeedType::WithoutReturn: - _api.LineFeed(false, false); + _DoLineFeed(textBuffer, false, false); return true; case DispatchTypes::LineFeedType::WithReturn: - _api.LineFeed(true, false); + _DoLineFeed(textBuffer, true, false); return true; default: return false; @@ -2636,8 +2711,8 @@ bool AdaptDispatch::ScreenAlignmentPattern() // Arguments: // - // Return value: -// - -void AdaptDispatch::_EraseScrollback() +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_EraseScrollback() { const auto viewport = _api.GetViewport(); const auto top = viewport.top; @@ -2658,6 +2733,12 @@ void AdaptDispatch::_EraseScrollback() // Move the cursor to the same relative location. cursor.SetYPosition(row - top); cursor.SetHasMoved(true); + + // GH#2715 - If this succeeded, but we're in a conpty, return `false` to + // make the state machine propagate this ED sequence to the connected + // terminal application. While we're in conpty mode, we don't really + // have a scrollback, but the attached terminal might. + return !_api.IsConsolePty(); } //Routine Description: @@ -2671,13 +2752,14 @@ void AdaptDispatch::_EraseScrollback() // Arguments: // - // Return value: -// - -void AdaptDispatch::_EraseAll() +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_EraseAll() { const auto viewport = _api.GetViewport(); const auto viewportHeight = viewport.bottom - viewport.top; auto& textBuffer = _api.GetTextBuffer(); const auto bufferSize = textBuffer.GetSize(); + const auto inPtyMode = _api.IsConsolePty(); // Stash away the current position of the cursor within the viewport. // We'll need to restore the cursor to that same relative position, after @@ -2692,10 +2774,21 @@ void AdaptDispatch::_EraseAll() auto newViewportTop = lastChar == til::point{} ? 0 : lastChar.y + 1; const auto newViewportBottom = newViewportTop + viewportHeight; const auto delta = newViewportBottom - (bufferSize.Height()); - for (auto i = 0; i < delta; i++) + if (delta > 0) { - textBuffer.IncrementCircularBuffer(); - newViewportTop--; + for (auto i = 0; i < delta; i++) + { + textBuffer.IncrementCircularBuffer(); + } + _api.NotifyBufferRotation(delta); + newViewportTop -= delta; + // We don't want to trigger a scroll in pty mode, because we're going to + // pass through the ED sequence anyway, and this will just result in the + // buffer being scrolled up by two pages instead of one. + if (!inPtyMode) + { + textBuffer.TriggerScroll({ 0, -delta }); + } } // Move the viewport _api.SetViewportPosition({ viewport.left, newViewportTop }); @@ -2710,6 +2803,14 @@ void AdaptDispatch::_EraseAll() // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(newViewportTop, newViewportBottom); + + // GH#5683 - If this succeeded, but we're in a conpty, return `false` to + // make the state machine propagate this ED sequence to the connected + // terminal application. While we're in conpty mode, when the client + // requests a Erase All operation, we need to manually tell the + // connected terminal to do the same thing, so that the terminal will + // move it's own buffer contents into the scrollback. + return !inPtyMode; } //Routine Description: diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 45e45e0796..c577f8a314 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -200,7 +200,7 @@ namespace Microsoft::Console::VirtualTerminal }; void _WriteToBuffer(const std::wstring_view string); - std::pair _GetVerticalMargins(const til::rect& viewport, const bool absolute); + std::pair _GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept; bool _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins); void _ApplyCursorMovementFlags(Cursor& cursor) noexcept; void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const wchar_t fillChar, const TextAttribute fillAttrs); @@ -208,8 +208,8 @@ namespace Microsoft::Console::VirtualTerminal void _ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps); void _ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps); til::rect _CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize); - void _EraseScrollback(); - void _EraseAll(); + bool _EraseScrollback(); + bool _EraseAll(); void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); void _ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); void _InsertDeleteCharacterHelper(const VTInt delta); @@ -218,6 +218,8 @@ namespace Microsoft::Console::VirtualTerminal void _DoSetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin); + void _DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced); + void _OperatingStatus() const; void _CursorPositionReport(const bool extendedReport); void _MacroSpaceReport() const; diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index a88964a180..862d449031 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -121,17 +121,6 @@ public: _textBuffer->SetCurrentAttributes(attrs); } - void SetScrollingRegion(const til::inclusive_rect& scrollMargins) override - { - Log::Comment(L"SetScrollingRegion MOCK called..."); - - if (_setScrollingRegionResult) - { - VERIFY_ARE_EQUAL(_expectedScrollRegion, scrollMargins); - _activeScrollRegion = scrollMargins; - } - } - void WarningBell() override { Log::Comment(L"WarningBell MOCK called..."); @@ -143,14 +132,6 @@ public: return _getLineFeedModeResult; } - void LineFeed(const bool withReturn, const bool /*wrapForced*/) override - { - Log::Comment(L"LineFeed MOCK called..."); - - THROW_HR_IF(E_FAIL, !_lineFeedResult); - VERIFY_ARE_EQUAL(_expectedLineFeedWithReturn, withReturn); - } - void SetWindowTitle(const std::wstring_view title) { Log::Comment(L"SetWindowTitle MOCK called..."); @@ -245,6 +226,11 @@ public: Log::Comment(L"NotifyAccessibilityChange MOCK called..."); } + void NotifyBufferRotation(const int /*delta*/) override + { + Log::Comment(L"NotifyBufferRotation MOCK called..."); + } + void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& /*mark*/) override { Log::Comment(L"MarkPrompt MOCK called..."); @@ -366,15 +352,6 @@ public: VERIFY_ARE_EQUAL(pwszExpectedResponse, _response); } - void _SetMarginsHelper(til::inclusive_rect* rect, til::CoordType top, til::CoordType bottom) - { - rect->top = top; - rect->bottom = bottom; - //The rectangle is going to get converted from VT space to conhost space - _expectedScrollRegion.top = (top > 0) ? rect->top - 1 : rect->top; - _expectedScrollRegion.bottom = (bottom > 0) ? rect->bottom - 1 : rect->bottom; - } - ~TestGetSet() = default; static const WCHAR s_wchErase = (WCHAR)0x20; @@ -399,8 +376,6 @@ public: DummyRenderer _renderer; std::unique_ptr _textBuffer; til::inclusive_rect _viewport; - til::inclusive_rect _expectedScrollRegion; - til::inclusive_rect _activeScrollRegion; til::point _expectedCursorPos; @@ -411,10 +386,7 @@ public: bool _setTextAttributesResult = false; bool _returnResponseResult = false; - bool _setScrollingRegionResult = false; bool _getLineFeedModeResult = false; - bool _lineFeedResult = false; - bool _expectedLineFeedWithReturn = false; bool _setWindowTitleResult = false; std::wstring_view _expectedWindowTitle{}; @@ -2212,119 +2184,94 @@ public: auto sScreenHeight = _testGetSet->_viewport.bottom - _testGetSet->_viewport.top; Log::Comment(L"Test 1: Verify having both values is valid."); - _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); - _testGetSet->_setScrollingRegionResult = TRUE; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(2, 6)); + VERIFY_ARE_EQUAL(2, _pDispatch->_scrollMargins.top + 1); + VERIFY_ARE_EQUAL(6, _pDispatch->_scrollMargins.bottom + 1); Log::Comment(L"Test 2: Verify having only top is valid."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 7, 0); - _testGetSet->_expectedScrollRegion.bottom = _testGetSet->_viewport.bottom - 1; // We expect the bottom to be the bottom of the viewport, exclusive. - _testGetSet->_setScrollingRegionResult = TRUE; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(7, 0)); + VERIFY_ARE_EQUAL(7, _pDispatch->_scrollMargins.top + 1); + VERIFY_ARE_EQUAL(sScreenHeight, _pDispatch->_scrollMargins.bottom + 1); Log::Comment(L"Test 3: Verify having only bottom is valid."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 0, 7); - _testGetSet->_setScrollingRegionResult = TRUE; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(0, 7)); + VERIFY_ARE_EQUAL(1, _pDispatch->_scrollMargins.top + 1); + VERIFY_ARE_EQUAL(7, _pDispatch->_scrollMargins.bottom + 1); Log::Comment(L"Test 4: Verify having no values is valid."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 0, 0); - _testGetSet->_setScrollingRegionResult = TRUE; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(0, 0)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 5: Verify having both values, but bad bounds has no effect."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 7, 3); - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_activeScrollRegion = {}; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); - VERIFY_ARE_EQUAL(til::inclusive_rect{}, _testGetSet->_activeScrollRegion); + _pDispatch->_scrollMargins = {}; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(7, 3)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 6: Verify setting margins to (0, height) clears them"); // First set, - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(2, 6)); // Then clear - _testGetSet->_SetMarginsHelper(&srTestMargins, 0, sScreenHeight); - _testGetSet->_expectedScrollRegion.top = 0; - _testGetSet->_expectedScrollRegion.bottom = 0; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(0, sScreenHeight)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 7: Verify setting margins to (1, height) clears them"); // First set, - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(2, 6)); // Then clear - _testGetSet->_SetMarginsHelper(&srTestMargins, 1, sScreenHeight); - _testGetSet->_expectedScrollRegion.top = 0; - _testGetSet->_expectedScrollRegion.bottom = 0; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(1, sScreenHeight)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 8: Verify setting margins to (1, 0) clears them"); // First set, - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(2, 6)); // Then clear - _testGetSet->_SetMarginsHelper(&srTestMargins, 1, 0); - _testGetSet->_expectedScrollRegion.top = 0; - _testGetSet->_expectedScrollRegion.bottom = 0; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(1, 0)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 9: Verify having top and bottom margin the same has no effect."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 4, 4); - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_activeScrollRegion = {}; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); - VERIFY_ARE_EQUAL(til::inclusive_rect{}, _testGetSet->_activeScrollRegion); + _pDispatch->_scrollMargins = {}; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(4, 4)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 10: Verify having top margin out of bounds has no effect."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, sScreenHeight + 1, sScreenHeight + 10); - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_activeScrollRegion = {}; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); - VERIFY_ARE_EQUAL(til::inclusive_rect{}, _testGetSet->_activeScrollRegion); + _pDispatch->_scrollMargins = {}; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(sScreenHeight + 1, sScreenHeight + 10)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); Log::Comment(L"Test 11: Verify having bottom margin out of bounds has no effect."); - - _testGetSet->_SetMarginsHelper(&srTestMargins, 1, sScreenHeight + 1); - _testGetSet->_setScrollingRegionResult = TRUE; - _testGetSet->_activeScrollRegion = {}; - VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.top, srTestMargins.bottom)); - VERIFY_ARE_EQUAL(til::inclusive_rect{}, _testGetSet->_activeScrollRegion); + _pDispatch->_scrollMargins = {}; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(1, sScreenHeight + 1)); + VERIFY_ARE_EQUAL(til::inclusive_rect{}, _pDispatch->_scrollMargins); } TEST_METHOD(LineFeedTest) { Log::Comment(L"Starting test..."); - // All test cases need the LineFeed call to succeed. - _testGetSet->_lineFeedResult = TRUE; + _testGetSet->PrepData(); + auto& cursor = _testGetSet->_textBuffer->GetCursor(); Log::Comment(L"Test 1: Line feed without carriage return."); - _testGetSet->_expectedLineFeedWithReturn = false; + cursor.SetPosition({ 10, 0 }); VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::WithoutReturn)); + VERIFY_ARE_EQUAL(til::point(10, 1), cursor.GetPosition()); Log::Comment(L"Test 2: Line feed with carriage return."); - _testGetSet->_expectedLineFeedWithReturn = true; + cursor.SetPosition({ 10, 0 }); VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::WithReturn)); + VERIFY_ARE_EQUAL(til::point(0, 1), cursor.GetPosition()); Log::Comment(L"Test 3: Line feed depends on mode, and mode reset."); _testGetSet->_getLineFeedModeResult = false; - _testGetSet->_expectedLineFeedWithReturn = false; + cursor.SetPosition({ 10, 0 }); VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::DependsOnMode)); + VERIFY_ARE_EQUAL(til::point(10, 1), cursor.GetPosition()); Log::Comment(L"Test 4: Line feed depends on mode, and mode set."); _testGetSet->_getLineFeedModeResult = true; - _testGetSet->_expectedLineFeedWithReturn = true; + cursor.SetPosition({ 10, 0 }); VERIFY_IS_TRUE(_pDispatch->LineFeed(DispatchTypes::LineFeedType::DependsOnMode)); + VERIFY_ARE_EQUAL(til::point(0, 1), cursor.GetPosition()); } TEST_METHOD(SetConsoleTitleTest)