Skip to content

Commit ffc8b12

Browse files
committed
PRE-MERGE #14936 Ensure that delayed EOL wrap is reset when necessary
2 parents 6a9254e + 4eada04 commit ffc8b12

File tree

7 files changed

+170
-12
lines changed

7 files changed

+170
-12
lines changed

src/buffer/out/cursor.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,9 @@ void Cursor::CopyProperties(const Cursor& OtherCursor) noexcept
286286
_cursorType = OtherCursor._cursorType;
287287
}
288288

289-
void Cursor::DelayEOLWrap(const til::point coordDelayedAt) noexcept
289+
void Cursor::DelayEOLWrap() noexcept
290290
{
291-
_coordDelayedAt = coordDelayedAt;
291+
_coordDelayedAt = _cPosition;
292292
_fDelayedEolWrap = true;
293293
}
294294

src/buffer/out/cursor.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class Cursor final
7676

7777
void CopyProperties(const Cursor& OtherCursor) noexcept;
7878

79-
void DelayEOLWrap(const til::point coordDelayedAt) noexcept;
79+
void DelayEOLWrap() noexcept;
8080
void ResetDelayEOLWrap() noexcept;
8181
til::point GetDelayedAtPosition() const noexcept;
8282
bool IsDelayedEOLWrap() const noexcept;

src/buffer/out/textBuffer.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,11 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition)
901901
}
902902
TriggerRedraw(Viewport::FromDimensions({ 0, rowIndex }, { GetSize().Width(), 1 }));
903903
}
904+
// There is some variation in how this was handled by the different DEC
905+
// terminals, but the STD 070 reference (on page D-13) makes it clear that
906+
// the delayed wrap (aka the Last Column Flag) was expected to be reset when
907+
// line rendition controls were executed.
908+
GetCursor().ResetDelayEOLWrap();
904909
}
905910

906911
void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) noexcept

src/host/ut_host/ScreenBufferTests.cpp

+112-7
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ class ScreenBufferTests
252252
TEST_METHOD(TestDeferredMainBufferResize);
253253

254254
TEST_METHOD(RectangularAreaOperations);
255+
256+
TEST_METHOD(DelayedWrapReset);
255257
};
256258

257259
void ScreenBufferTests::SingleAlternateBufferCreationTest()
@@ -6266,48 +6268,55 @@ void ScreenBufferTests::CursorSaveRestore()
62666268
VERIFY_SUCCEEDED(si.SetViewportOrigin(true, til::point(0, 0), true));
62676269

62686270
Log::Comment(L"Restore after save.");
6269-
// Set the cursor position, attributes, and character set.
6271+
// Set the cursor position, delayed wrap, attributes, and character set.
62706272
cursor.SetPosition({ 20, 10 });
6273+
cursor.DelayEOLWrap();
62716274
si.SetAttributes(colorAttrs);
62726275
stateMachine.ProcessString(selectGraphicsChars);
62736276
// Save state.
62746277
stateMachine.ProcessString(saveCursor);
6275-
// Reset the cursor position, attributes, and character set.
6278+
// Reset the cursor position, delayed wrap, attributes, and character set.
62766279
cursor.SetPosition({ 0, 0 });
62776280
si.SetAttributes(defaultAttrs);
62786281
stateMachine.ProcessString(selectAsciiChars);
62796282
// Restore state.
62806283
stateMachine.ProcessString(restoreCursor);
6281-
// Verify initial position, colors, and graphic character set.
6284+
// Verify initial position, delayed wrap, colors, and graphic character set.
62826285
VERIFY_ARE_EQUAL(til::point(20, 10), cursor.GetPosition());
6286+
VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap());
6287+
cursor.ResetDelayEOLWrap();
62836288
VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes());
62846289
stateMachine.ProcessString(asciiText);
62856290
VERIFY_IS_TRUE(_ValidateLineContains({ 20, 10 }, graphicText, colorAttrs));
62866291

62876292
Log::Comment(L"Restore again without save.");
6288-
// Reset the cursor position, attributes, and character set.
6293+
// Reset the cursor position, delayed wrap, attributes, and character set.
62896294
cursor.SetPosition({ 0, 0 });
62906295
si.SetAttributes(defaultAttrs);
62916296
stateMachine.ProcessString(selectAsciiChars);
62926297
// Restore state.
62936298
stateMachine.ProcessString(restoreCursor);
6294-
// Verify initial saved position, colors, and graphic character set.
6299+
// Verify initial saved position, delayed wrap, colors, and graphic character set.
62956300
VERIFY_ARE_EQUAL(til::point(20, 10), cursor.GetPosition());
6301+
VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap());
6302+
cursor.ResetDelayEOLWrap();
62966303
VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes());
62976304
stateMachine.ProcessString(asciiText);
62986305
VERIFY_IS_TRUE(_ValidateLineContains({ 20, 10 }, graphicText, colorAttrs));
62996306

63006307
Log::Comment(L"Restore after reset.");
63016308
// Soft reset.
63026309
stateMachine.ProcessString(L"\x1b[!p");
6303-
// Set the cursor position, attributes, and character set.
6310+
// Set the cursor position, delayed wrap, attributes, and character set.
63046311
cursor.SetPosition({ 20, 10 });
6312+
cursor.DelayEOLWrap();
63056313
si.SetAttributes(colorAttrs);
63066314
stateMachine.ProcessString(selectGraphicsChars);
63076315
// Restore state.
63086316
stateMachine.ProcessString(restoreCursor);
6309-
// Verify home position, default attributes, and ascii character set.
6317+
// Verify home position, no delayed wrap, default attributes, and ascii character set.
63106318
VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition());
6319+
VERIFY_IS_FALSE(cursor.IsDelayedEOLWrap());
63116320
VERIFY_ARE_EQUAL(defaultAttrs, si.GetAttributes());
63126321
stateMachine.ProcessString(asciiText);
63136322
VERIFY_IS_TRUE(_ValidateLineContains(til::point(0, 0), asciiText, defaultAttrs));
@@ -7538,3 +7547,99 @@ void ScreenBufferTests::RectangularAreaOperations()
75387547
VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.top, targetArea.bottom, bufferChars, bufferAttr));
75397548
VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.right, targetArea.top, targetArea.bottom, bufferChar, bufferAttr));
75407549
}
7550+
7551+
void ScreenBufferTests::DelayedWrapReset()
7552+
{
7553+
BEGIN_TEST_METHOD_PROPERTIES()
7554+
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
7555+
TEST_METHOD_PROPERTY(L"Data:op", L"{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34}")
7556+
END_TEST_METHOD_PROPERTIES();
7557+
7558+
int opIndex;
7559+
VERIFY_SUCCEEDED(TestData::TryGetValue(L"op", opIndex));
7560+
7561+
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
7562+
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
7563+
auto& stateMachine = si.GetStateMachine();
7564+
const auto width = si.GetTextBuffer().GetSize().Width();
7565+
const auto halfWidth = width / 2;
7566+
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
7567+
WI_SetFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN);
7568+
stateMachine.ProcessString(L"\033[?40h"); // Make sure DECCOLM is allowed
7569+
7570+
const auto startRow = 5;
7571+
const auto startCol = width - 1;
7572+
7573+
// The operations below are all those that the DEC STD 070 reference has
7574+
// documented as needing to reset the Last Column Flag (see page D-13). The
7575+
// only controls that we haven't included are HT and SUB, because most DEC
7576+
// terminals did *not* trigger a reset after executing those sequences, and
7577+
// most modern terminals also seem to have agreed that that is the correct
7578+
// approach to take.
7579+
const struct
7580+
{
7581+
std::wstring_view name;
7582+
std::wstring_view sequence;
7583+
til::point expectedPos = {};
7584+
bool absolutePos = false;
7585+
} ops[] = {
7586+
{ L"DECSTBM", L"\033[1;10r", { 0, 0 }, true },
7587+
{ L"DECSWL", L"\033#5" },
7588+
{ L"DECDWL", L"\033#6", { halfWidth - 1, startRow }, true },
7589+
{ L"DECDHL (top)", L"\033#3", { halfWidth - 1, startRow }, true },
7590+
{ L"DECDHL (bottom)", L"\033#4", { halfWidth - 1, startRow }, true },
7591+
{ L"DECCOLM set", L"\033[?3h", { 0, 0 }, true },
7592+
{ L"DECOM set", L"\033[?6h", { 0, 0 }, true },
7593+
{ L"DECCOLM set", L"\033[?3l", { 0, 0 }, true },
7594+
{ L"DECOM reset", L"\033[?6l", { 0, 0 }, true },
7595+
{ L"DECAWM reset", L"\033[?7l" },
7596+
{ L"CUU", L"\033[A", { 0, -1 } },
7597+
{ L"CUD", L"\033[B", { 0, 1 } },
7598+
{ L"CUF", L"\033[C" },
7599+
{ L"CUB", L"\033[D", { -1, 0 } },
7600+
{ L"CUP", L"\033[3;7H", { 6, 2 }, true },
7601+
{ L"HVP", L"\033[3;7f", { 6, 2 }, true },
7602+
{ L"BS", L"\b", { -1, 0 } },
7603+
{ L"LF", L"\n", { 0, 1 } },
7604+
{ L"VT", L"\v", { 0, 1 } },
7605+
{ L"FF", L"\f", { 0, 1 } },
7606+
{ L"CR", L"\r", { 0, startRow }, true },
7607+
{ L"IND", L"\033D", { 0, 1 } },
7608+
{ L"RI", L"\033M", { 0, -1 } },
7609+
{ L"NEL", L"\033E", { 0, startRow + 1 }, true },
7610+
{ L"ECH", L"\033[X" },
7611+
{ L"DCH", L"\033[P" },
7612+
{ L"ICH", L"\033[@" },
7613+
{ L"EL", L"\033[K" },
7614+
{ L"DECSEL", L"\033[?K" },
7615+
{ L"DL", L"\033[M", { 0, startRow }, true },
7616+
{ L"IL", L"\033[L", { 0, startRow }, true },
7617+
{ L"ED", L"\033[J" },
7618+
{ L"ED (all)", L"\033[2J" },
7619+
{ L"ED (scrollback)", L"\033[3J" },
7620+
{ L"DECSED", L"\033[?J" },
7621+
};
7622+
const auto& op = gsl::at(ops, opIndex);
7623+
7624+
Log::Comment(L"Writing a character at the end of the line should set delayed EOL wrap");
7625+
const auto startPos = til::point{ startCol, startRow };
7626+
si.GetTextBuffer().GetCursor().SetPosition(startPos);
7627+
stateMachine.ProcessCharacter(L'X');
7628+
{
7629+
auto& cursor = si.GetTextBuffer().GetCursor();
7630+
VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap());
7631+
VERIFY_ARE_EQUAL(startPos, cursor.GetPosition());
7632+
}
7633+
7634+
Log::Comment(NoThrowString().Format(
7635+
L"Executing a %s control should reset delayed EOL wrap",
7636+
op.name.data()));
7637+
const auto expectedPos = op.absolutePos ? op.expectedPos : startPos + op.expectedPos;
7638+
stateMachine.ProcessString(op.sequence);
7639+
{
7640+
auto& cursor = si.GetTextBuffer().GetCursor();
7641+
const auto actualPos = cursor.GetPosition() - si.GetViewport().Origin();
7642+
VERIFY_IS_FALSE(cursor.IsDelayedEOLWrap());
7643+
VERIFY_ARE_EQUAL(expectedPos, actualPos);
7644+
}
7645+
}

src/renderer/vt/paint.cpp

+5-1
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,11 @@ using namespace Microsoft::Console::Types;
604604
{
605605
RETURN_IF_FAILED(_EraseCharacter(numSpaces));
606606
}
607-
else
607+
// If we're past the end of the row (i.e. in the "delayed EOL wrap"
608+
// state), then there is no need to erase the rest of line. In fact
609+
// if we did output an EL sequence at this point, it could reset the
610+
// "delayed EOL wrap" state, breaking subsequent output.
611+
else if (_lastText.x <= _lastViewport.RightInclusive())
608612
{
609613
RETURN_IF_FAILED(_EraseLine());
610614
}

src/terminal/adapter/adaptDispatch.cpp

+44-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
149149
cursor.SetPosition(cursorPosition);
150150
if (wrapAtEOL)
151151
{
152-
cursor.DelayEOLWrap(cursorPosition);
152+
cursor.DelayEOLWrap();
153153
}
154154
}
155155
else
@@ -446,6 +446,7 @@ bool AdaptDispatch::CursorSaveState()
446446
auto& savedCursorState = _savedCursorState.at(_usingAltBuffer);
447447
savedCursorState.Column = cursorPosition.x + 1;
448448
savedCursorState.Row = cursorPosition.y + 1;
449+
savedCursorState.IsDelayedEOLWrap = textBuffer.GetCursor().IsDelayedEOLWrap();
449450
savedCursorState.IsOriginModeRelative = _modes.test(Mode::Origin);
450451
savedCursorState.Attributes = attributes;
451452
savedCursorState.TermOutput = _termOutput;
@@ -484,6 +485,12 @@ bool AdaptDispatch::CursorRestoreState()
484485
_modes.reset(Mode::Origin);
485486
CursorPosition(row, col);
486487

488+
// If the delayed wrap flag was set when the cursor was saved, we need to restore than now.
489+
if (savedCursorState.IsDelayedEOLWrap)
490+
{
491+
_api.GetTextBuffer().GetCursor().DelayEOLWrap();
492+
}
493+
487494
// Once the cursor position is restored, we can then restore the actual origin mode.
488495
_modes.set(Mode::Origin, savedCursorState.IsOriginModeRelative);
489496

@@ -600,6 +607,8 @@ void AdaptDispatch::_InsertDeleteCharacterHelper(const VTInt delta)
600607
const auto startCol = textBuffer.GetCursor().GetPosition().x;
601608
const auto endCol = textBuffer.GetLineWidth(row);
602609
_ScrollRectHorizontally(textBuffer, { startCol, row, endCol, row + 1 }, delta);
610+
// The ICH and DCH controls are expected to reset the delayed wrap flag.
611+
textBuffer.GetCursor().ResetDelayEOLWrap();
603612
}
604613

605614
// Routine Description:
@@ -668,6 +677,9 @@ bool AdaptDispatch::EraseCharacters(const VTInt numChars)
668677
const auto startCol = textBuffer.GetCursor().GetPosition().x;
669678
const auto endCol = std::min<VTInt>(startCol + numChars, textBuffer.GetLineWidth(row));
670679

680+
// The ECH control is expected to reset the delayed wrap flag.
681+
textBuffer.GetCursor().ResetDelayEOLWrap();
682+
671683
auto eraseAttributes = textBuffer.GetCurrentAttributes();
672684
eraseAttributes.SetStandardErase();
673685
_FillRect(textBuffer, { startCol, row, endCol, row + 1 }, L' ', eraseAttributes);
@@ -721,6 +733,11 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType)
721733
const auto row = textBuffer.GetCursor().GetPosition().y;
722734
const auto col = textBuffer.GetCursor().GetPosition().x;
723735

736+
// The ED control is expected to reset the delayed wrap flag.
737+
// The special case variants above ("erase all" and "erase scrollback")
738+
// take care of that themselves when they set the cursor position.
739+
textBuffer.GetCursor().ResetDelayEOLWrap();
740+
724741
auto eraseAttributes = textBuffer.GetCurrentAttributes();
725742
eraseAttributes.SetStandardErase();
726743

@@ -758,6 +775,9 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType)
758775
const auto row = textBuffer.GetCursor().GetPosition().y;
759776
const auto col = textBuffer.GetCursor().GetPosition().x;
760777

778+
// The EL control is expected to reset the delayed wrap flag.
779+
textBuffer.GetCursor().ResetDelayEOLWrap();
780+
761781
auto eraseAttributes = textBuffer.GetCurrentAttributes();
762782
eraseAttributes.SetStandardErase();
763783
switch (eraseType)
@@ -822,6 +842,9 @@ bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType erase
822842
const auto row = textBuffer.GetCursor().GetPosition().y;
823843
const auto col = textBuffer.GetCursor().GetPosition().x;
824844

845+
// The DECSED control is expected to reset the delayed wrap flag.
846+
textBuffer.GetCursor().ResetDelayEOLWrap();
847+
825848
switch (eraseType)
826849
{
827850
case DispatchTypes::EraseType::FromBeginning:
@@ -855,6 +878,9 @@ bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseTyp
855878
const auto row = textBuffer.GetCursor().GetPosition().y;
856879
const auto col = textBuffer.GetCursor().GetPosition().x;
857880

881+
// The DECSEL control is expected to reset the delayed wrap flag.
882+
textBuffer.GetCursor().ResetDelayEOLWrap();
883+
858884
switch (eraseType)
859885
{
860886
case DispatchTypes::EraseType::FromBeginning:
@@ -1606,6 +1632,11 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
16061632
return true;
16071633
case DispatchTypes::ModeParams::DECAWM_AutoWrapMode:
16081634
_api.SetAutoWrapMode(enable);
1635+
// Resetting DECAWM should also reset the delayed wrap flag.
1636+
if (!enable)
1637+
{
1638+
_api.GetTextBuffer().GetCursor().ResetDelayEOLWrap();
1639+
}
16091640
return true;
16101641
case DispatchTypes::ModeParams::DECARM_AutoRepeatMode:
16111642
_terminalInput.SetInputMode(TerminalInput::Mode::AutoRepeat, enable);
@@ -2097,8 +2128,20 @@ bool AdaptDispatch::ForwardTab(const VTInt numTabs)
20972128
}
20982129
}
20992130

2131+
// While the STD 070 reference suggests that horizontal tabs should reset
2132+
// the delayed wrap, almost none of the DEC terminals actually worked that
2133+
// way, and most modern terminal emulators appear to have taken the same
2134+
// approach (i.e. they don't reset). For us this is a bit messy, since all
2135+
// cursor movement resets the flag automatically, so we need to save the
2136+
// original state here, and potentially reapply it after the move.
2137+
const auto delayedWrapOriginallySet = cursor.IsDelayedEOLWrap();
21002138
cursor.SetXPosition(column);
21012139
_ApplyCursorMovementFlags(cursor);
2140+
if (delayedWrapOriginallySet)
2141+
{
2142+
cursor.DelayEOLWrap();
2143+
}
2144+
21022145
return true;
21032146
}
21042147

src/terminal/adapter/adaptDispatch.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ namespace Microsoft::Console::VirtualTerminal
170170
{
171171
VTInt Row = 1;
172172
VTInt Column = 1;
173+
bool IsDelayedEOLWrap = false;
173174
bool IsOriginModeRelative = false;
174175
TextAttribute Attributes = {};
175176
TerminalOutput TermOutput = {};

0 commit comments

Comments
 (0)