Merge the LineFeed functionality into AdaptDispatch (#14874)

The main purpose of this PR was to merge the `ITerminalApi::LineFeed`
implementations into a shared method in `AdaptDispatch`, and avoid the
VT code path depending on the `AdjustCursorPosition` function (which
could then be massively simplified). This refactoring also fixes some
bugs that existed in the original `LineFeed` implementations.

## References and Relevant Issues

This helps to close the gap between the Conhost and Terminal (#13408).
This improves some of the scrollbar mark bugs mentioned in #11000.

## Detailed Description of the Pull Request / Additional comments

I had initially hoped the line feed functionality could be implemented
entirely within `AdaptDispatch`, but there is still some Conhost and
Terminal-specific behavior that needs to be triggered when we reach the
bottom of the buffer, and the row coordinates are cycled.

In Conhost we need to trigger an accessibility scroll event, and in
Windows Terminal we need to update selection and marker offsets, reset
pattern intervals, and preserve the user's scroll offset. This is now
handled by a new `NotifyBufferRotation` method in `ITerminalApi`.

But this made me realise that the `_EraseAll` method should have been
doing the same thing when it reached the bottom of the buffer. So I've
added a call to the new `NotifyBufferRotation` API from there as well.

And in the case of Windows Terminal, the scroll offset preservation was
something that was also needed for a regular viewport pan. So I've put
that in a separate `_PreserveUserScrollOffset` method which is called
from the `SetViewportPosition` handler as well.

## Validation Steps Performed

Because of the API changes, there were a number of unit tests that
needed to be updated:

- Some of the `ScreenBufferTests` were accessing margin state in the
`SCREEN_INFORMATION` class which doesn't exist anymore, so I had to add
a little helper function which now manually detects the active margins.

- Some of the `AdapterTest` tests were dependent on APIs that no longer
exist, so they needed to be rewritten so they now check the resulting
state rather than expecting a mock API call.

- The `ScrollWithMargins` test in `ConptyRoundtripTests` was testing
functionality that didn't previously work correctly (issue #3673). Now
that it's been fixed, that test needed to be updated accordingly.

Other than getting the unit tests working, I've manually verified that
issue #3673 is now fixed. And I've also checked that the scroll markers,
selections, and user scroll offset are all being updated correctly, both
with a regular viewport pan, as well as when overrunning the buffer.

Closes #3673
This commit is contained in:
James Holderness 2023-03-30 19:32:54 +01:00 committed by GitHub
parent da3a33f3bc
commit fc95802531
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 331 additions and 639 deletions

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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<wchar_t>(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<char>('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()

View File

@ -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
// <blank, was AAAA>
//
// Then we scrolled the contents of everything OUTSIDE the margin frame down.
// BBBB
// CCCC
// DDDD
// <blank, filled during scroll down of EEEE>
// 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;

View File

@ -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.

View File

@ -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:
// - <none>
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:
// - <none>
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:
// - <none>
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.

View File

@ -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;

View File

@ -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:
// - <none>
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:
// - <none>
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:
// - <none>
// Return Value:
// - <none>
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

View File

@ -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<Microsoft::Console::VirtualTerminal::StateMachine> _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;

View File

@ -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.");

View File

@ -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;

View File

@ -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<int, int> AdaptDispatch::_GetVerticalMargins(const til::rect& viewport, const bool absolute)
std::pair<int, int> 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:
// - <none>
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:
// - <none>
// Return value:
// - <none>
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:
// - <none>
// Return value:
// - <none>
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:

View File

@ -200,7 +200,7 @@ namespace Microsoft::Console::VirtualTerminal
};
void _WriteToBuffer(const std::wstring_view string);
std::pair<int, int> _GetVerticalMargins(const til::rect& viewport, const bool absolute);
std::pair<int, int> _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;

View File

@ -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> _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)