mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-25 07:31:42 +00:00
When a character is written in the last column of a row, the cursor doesn't move, but instead sets a "delayed EOL wrap" flag. If another character is then output while that flag is still set, the cursor moves to the start of the next line, before writing to the buffer. That flag is supposed to be reset when certain control sequences are executed, but prior to now we haven't always handled that correctly. With this PR, we should be resetting the flag appropriately in all the places that it's expected to be reset. For the most part, I'm following the DEC STD 070 reference, which lists a bunch of operations that are intended to reset the delayed wrap flag: `DECSTBM`, `DECSWL`, `DECDWL`, `DECDHL`, setting `DECCOLM` and `DECOM`, resetting `DECCOLM`, `DECOM`, and `DECAWM`, `CUU`, `CUD`, `CUF`, `CUB`, `CUP`, `HVP`, `BS`, `LF`, `VT`, `FF`, `CR`, `IND`, `RI`, `NEL`, `ECH`, `DCH`, `ICH`, `EL`, `DECSEL`, `DL`, `IL`, `ED`, and `DECSED`. We were already resetting the flag for any of the operations that performed cursor movement, since that always triggers a reset for us. However, I've now also added manual resets in those ops that weren't moving the cursor. Horizontal tabs are a special case, though. Technically the standard says they should reset the flag, but most DEC terminals never followed that rule, and most modern terminals agree that it's best for a tab to leave the flag as it is. Our implementation now does that too. But as mentioned above, we automatically reset the flag on any cursor movement, so the tab operation had to be handled as a special case, saving and restoring the flag when the cursor is updated. Another flaw in our implementation was that we should have been saving and restoring the flag as part of the cursor state in the `DECSC` and `DECRC` operations. That's now been fixed in this PR too. I should also mention there was a change I had to make to the conpty renderer, because it was sometimes using an `EL` sequence while the terminal was in the delayed EOL wrap state. This would reset the flag, and break subsequent output, so I've now added a check to prevent that from happening. ## Validation Steps Performed I've added some unit tests that confirm the operations listed above are now resetting the delayed EOL wrap as expected, and I've expanded the existing `CursorSaveRestore` test to make sure the flag is being saved and restored correctly. I've also manually confirmed that the test case in issue #3177 now matches XTerm's output, and I've confirmed that the results of the wraptest script[^1] now match XTerm's results. [^1]: https://github.com/mattiase/wraptest/ Closes #3177
340 lines
8.4 KiB
C++
340 lines
8.4 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
#include "cursor.h"
|
|
#include "TextBuffer.hpp"
|
|
|
|
#pragma hdrstop
|
|
|
|
// Routine Description:
|
|
// - Constructor to set default properties for Cursor
|
|
// Arguments:
|
|
// - ulSize - The height of the cursor within this buffer
|
|
Cursor::Cursor(const ULONG ulSize, TextBuffer& parentBuffer) noexcept :
|
|
_parentBuffer{ parentBuffer },
|
|
_fHasMoved(false),
|
|
_fIsVisible(true),
|
|
_fIsOn(true),
|
|
_fIsDouble(false),
|
|
_fBlinkingAllowed(true),
|
|
_fDelay(false),
|
|
_fIsConversionArea(false),
|
|
_fIsPopupShown(false),
|
|
_fDelayedEolWrap(false),
|
|
_fDeferCursorRedraw(false),
|
|
_fHaveDeferredCursorRedraw(false),
|
|
_ulSize(ulSize),
|
|
_cursorType(CursorType::Legacy)
|
|
{
|
|
}
|
|
|
|
Cursor::~Cursor() = default;
|
|
|
|
til::point Cursor::GetPosition() const noexcept
|
|
{
|
|
return _cPosition;
|
|
}
|
|
|
|
bool Cursor::HasMoved() const noexcept
|
|
{
|
|
return _fHasMoved;
|
|
}
|
|
|
|
bool Cursor::IsVisible() const noexcept
|
|
{
|
|
return _fIsVisible;
|
|
}
|
|
|
|
bool Cursor::IsOn() const noexcept
|
|
{
|
|
return _fIsOn;
|
|
}
|
|
|
|
bool Cursor::IsBlinkingAllowed() const noexcept
|
|
{
|
|
return _fBlinkingAllowed;
|
|
}
|
|
|
|
bool Cursor::IsDouble() const noexcept
|
|
{
|
|
return _fIsDouble;
|
|
}
|
|
|
|
bool Cursor::IsConversionArea() const noexcept
|
|
{
|
|
return _fIsConversionArea;
|
|
}
|
|
|
|
bool Cursor::IsPopupShown() const noexcept
|
|
{
|
|
return _fIsPopupShown;
|
|
}
|
|
|
|
bool Cursor::GetDelay() const noexcept
|
|
{
|
|
return _fDelay;
|
|
}
|
|
|
|
ULONG Cursor::GetSize() const noexcept
|
|
{
|
|
return _ulSize;
|
|
}
|
|
|
|
void Cursor::SetHasMoved(const bool fHasMoved) noexcept
|
|
{
|
|
_fHasMoved = fHasMoved;
|
|
}
|
|
|
|
void Cursor::SetIsVisible(const bool fIsVisible) noexcept
|
|
{
|
|
_fIsVisible = fIsVisible;
|
|
_RedrawCursor();
|
|
}
|
|
|
|
void Cursor::SetIsOn(const bool fIsOn) noexcept
|
|
{
|
|
_fIsOn = fIsOn;
|
|
_RedrawCursorAlways();
|
|
}
|
|
|
|
void Cursor::SetBlinkingAllowed(const bool fBlinkingAllowed) noexcept
|
|
{
|
|
_fBlinkingAllowed = fBlinkingAllowed;
|
|
// GH#2642 - From what we've gathered from other terminals, when blinking is
|
|
// disabled, the cursor should remain On always, and have the visibility
|
|
// controlled by the IsVisible property. So when you do a printf "\e[?12l"
|
|
// to disable blinking, the cursor stays stuck On. At this point, only the
|
|
// cursor visibility property controls whether the user can see it or not.
|
|
// (Yes, the cursor can be On and NOT Visible)
|
|
_fIsOn = true;
|
|
_RedrawCursorAlways();
|
|
}
|
|
|
|
void Cursor::SetIsDouble(const bool fIsDouble) noexcept
|
|
{
|
|
_fIsDouble = fIsDouble;
|
|
_RedrawCursor();
|
|
}
|
|
|
|
void Cursor::SetIsConversionArea(const bool fIsConversionArea) noexcept
|
|
{
|
|
// Functionally the same as "Hide cursor"
|
|
// Never called with TRUE, it's only used in the creation of a
|
|
// ConversionAreaInfo, and never changed after that.
|
|
_fIsConversionArea = fIsConversionArea;
|
|
_RedrawCursorAlways();
|
|
}
|
|
|
|
void Cursor::SetIsPopupShown(const bool fIsPopupShown) noexcept
|
|
{
|
|
// Functionally the same as "Hide cursor"
|
|
_fIsPopupShown = fIsPopupShown;
|
|
_RedrawCursorAlways();
|
|
}
|
|
|
|
void Cursor::SetDelay(const bool fDelay) noexcept
|
|
{
|
|
_fDelay = fDelay;
|
|
}
|
|
|
|
void Cursor::SetSize(const ULONG ulSize) noexcept
|
|
{
|
|
_ulSize = ulSize;
|
|
_RedrawCursor();
|
|
}
|
|
|
|
void Cursor::SetStyle(const ULONG ulSize, const CursorType type) noexcept
|
|
{
|
|
_ulSize = ulSize;
|
|
_cursorType = type;
|
|
|
|
_RedrawCursor();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Sends a redraw message to the renderer only if the cursor is currently on.
|
|
// - NOTE: For use with most methods in this class.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Cursor::_RedrawCursor() noexcept
|
|
{
|
|
// Only trigger the redraw if we're on.
|
|
// Don't draw the cursor if this was triggered from a conversion area.
|
|
// (Conversion areas have cursors to mark the insertion point internally, but the user's actual cursor is the one on the primary screen buffer.)
|
|
if (IsOn() && !IsConversionArea())
|
|
{
|
|
if (_fDeferCursorRedraw)
|
|
{
|
|
_fHaveDeferredCursorRedraw = true;
|
|
}
|
|
else
|
|
{
|
|
_RedrawCursorAlways();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Sends a redraw message to the renderer no matter what.
|
|
// - NOTE: For use with the method that turns the cursor on and off to force a refresh
|
|
// and clear the ON cursor from the screen. Not for use with other methods.
|
|
// They should use the other method so refreshes are suppressed while the cursor is off.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Cursor::_RedrawCursorAlways() noexcept
|
|
{
|
|
try
|
|
{
|
|
_parentBuffer.TriggerRedrawCursor(_cPosition);
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
void Cursor::SetPosition(const til::point cPosition) noexcept
|
|
{
|
|
_RedrawCursor();
|
|
_cPosition = cPosition;
|
|
_RedrawCursor();
|
|
ResetDelayEOLWrap();
|
|
}
|
|
|
|
void Cursor::SetXPosition(const til::CoordType NewX) noexcept
|
|
{
|
|
_RedrawCursor();
|
|
_cPosition.x = NewX;
|
|
_RedrawCursor();
|
|
ResetDelayEOLWrap();
|
|
}
|
|
|
|
void Cursor::SetYPosition(const til::CoordType NewY) noexcept
|
|
{
|
|
_RedrawCursor();
|
|
_cPosition.y = NewY;
|
|
_RedrawCursor();
|
|
ResetDelayEOLWrap();
|
|
}
|
|
|
|
void Cursor::IncrementXPosition(const til::CoordType DeltaX) noexcept
|
|
{
|
|
_RedrawCursor();
|
|
_cPosition.x += DeltaX;
|
|
_RedrawCursor();
|
|
ResetDelayEOLWrap();
|
|
}
|
|
|
|
void Cursor::IncrementYPosition(const til::CoordType DeltaY) noexcept
|
|
{
|
|
_RedrawCursor();
|
|
_cPosition.y += DeltaY;
|
|
_RedrawCursor();
|
|
ResetDelayEOLWrap();
|
|
}
|
|
|
|
void Cursor::DecrementXPosition(const til::CoordType DeltaX) noexcept
|
|
{
|
|
_RedrawCursor();
|
|
_cPosition.x -= DeltaX;
|
|
_RedrawCursor();
|
|
ResetDelayEOLWrap();
|
|
}
|
|
|
|
void Cursor::DecrementYPosition(const til::CoordType DeltaY) noexcept
|
|
{
|
|
_RedrawCursor();
|
|
_cPosition.y -= DeltaY;
|
|
_RedrawCursor();
|
|
ResetDelayEOLWrap();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Routine Description:
|
|
// - Copies properties from another cursor into this one.
|
|
// - This is primarily to copy properties that would otherwise not be specified during CreateInstance
|
|
// - NOTE: As of now, this function is specifically used to handle the ResizeWithReflow operation.
|
|
// It will need modification for other future users.
|
|
// Arguments:
|
|
// - OtherCursor - The cursor to copy properties from
|
|
// Return Value:
|
|
// - <none>
|
|
void Cursor::CopyProperties(const Cursor& OtherCursor) noexcept
|
|
{
|
|
// We shouldn't copy the position as it will be already rearranged by the resize operation.
|
|
//_cPosition = pOtherCursor->_cPosition;
|
|
|
|
_fHasMoved = OtherCursor._fHasMoved;
|
|
_fIsVisible = OtherCursor._fIsVisible;
|
|
_fIsOn = OtherCursor._fIsOn;
|
|
_fIsDouble = OtherCursor._fIsDouble;
|
|
_fBlinkingAllowed = OtherCursor._fBlinkingAllowed;
|
|
_fDelay = OtherCursor._fDelay;
|
|
_fIsConversionArea = OtherCursor._fIsConversionArea;
|
|
|
|
// A resize operation should invalidate the delayed end of line status, so do not copy.
|
|
//_fDelayedEolWrap = OtherCursor._fDelayedEolWrap;
|
|
//_coordDelayedAt = OtherCursor._coordDelayedAt;
|
|
|
|
_fDeferCursorRedraw = OtherCursor._fDeferCursorRedraw;
|
|
_fHaveDeferredCursorRedraw = OtherCursor._fHaveDeferredCursorRedraw;
|
|
|
|
// Size will be handled separately in the resize operation.
|
|
//_ulSize = OtherCursor._ulSize;
|
|
_cursorType = OtherCursor._cursorType;
|
|
}
|
|
|
|
void Cursor::DelayEOLWrap() noexcept
|
|
{
|
|
_coordDelayedAt = _cPosition;
|
|
_fDelayedEolWrap = true;
|
|
}
|
|
|
|
void Cursor::ResetDelayEOLWrap() noexcept
|
|
{
|
|
_coordDelayedAt = {};
|
|
_fDelayedEolWrap = false;
|
|
}
|
|
|
|
til::point Cursor::GetDelayedAtPosition() const noexcept
|
|
{
|
|
return _coordDelayedAt;
|
|
}
|
|
|
|
bool Cursor::IsDelayedEOLWrap() const noexcept
|
|
{
|
|
return _fDelayedEolWrap;
|
|
}
|
|
|
|
void Cursor::StartDeferDrawing() noexcept
|
|
{
|
|
_fDeferCursorRedraw = true;
|
|
}
|
|
|
|
bool Cursor::IsDeferDrawing() noexcept
|
|
{
|
|
return _fDeferCursorRedraw;
|
|
}
|
|
|
|
void Cursor::EndDeferDrawing() noexcept
|
|
{
|
|
if (_fHaveDeferredCursorRedraw)
|
|
{
|
|
_RedrawCursorAlways();
|
|
}
|
|
|
|
_fDeferCursorRedraw = FALSE;
|
|
}
|
|
|
|
const CursorType Cursor::GetType() const noexcept
|
|
{
|
|
return _cursorType;
|
|
}
|
|
|
|
void Cursor::SetType(const CursorType type) noexcept
|
|
{
|
|
_cursorType = type;
|
|
}
|