Skip to content

Commit 3c3b1aa

Browse files
authored
Add support for horizontal scrolling sequences (#15368)
This PR introduces four new escapes sequences: `DECIC` (Insert Column), `DECDC` (Delete Column), `DECBI` (Back Index), and `DECFI` (Forward Index), which allow for horizontal scrolling within a margin area. ## References and Relevant Issues This follows on from the horizontal margins PR #15084 to complete the requirements for the horizontal scrolling extension. ## Detailed Description of the Pull Request / Additional comments The implementation is fairly straightforward, since they're all built on top of the existing `_ScrollRectHorizontally` method. ## Validation Steps Performed Thanks to @al20878, these operations have been extensively tested on a number of DEC terminals and I've manually confirmed our implementation matches their behavior. I've also added a unit tests that covers the basic execution of each of the operations. Closes #15109
1 parent 6775300 commit 3c3b1aa

File tree

11 files changed

+288
-10
lines changed

11 files changed

+288
-10
lines changed

.github/actions/spelling/expect/expect.txt

+11
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ DECANM
410410
DECARM
411411
DECAUPSS
412412
DECAWM
413+
DECBI
413414
DECBKM
414415
DECCARA
415416
DECCIR
@@ -418,13 +419,16 @@ DECCKSR
418419
DECCOLM
419420
DECCRA
420421
DECCTR
422+
DECDC
421423
DECDHL
422424
decdld
423425
DECDMAC
424426
DECDWL
425427
DECEKBD
426428
DECERA
429+
DECFI
427430
DECFRA
431+
DECIC
428432
DECID
429433
DECINVM
430434
DECKPAM
@@ -571,6 +575,7 @@ EDITTEXT
571575
EDITUPDATE
572576
edputil
573577
Efast
578+
efghijklmn
574579
EHsc
575580
EINS
576581
EJO
@@ -971,6 +976,8 @@ KLF
971976
KLMNO
972977
KLMNOPQRST
973978
KLMNOPQRSTQQQQQ
979+
KLMNOPQRSTUVWXY
980+
KLMNOPQRSTY
974981
KOK
975982
KPRIORITY
976983
KVM
@@ -1134,6 +1141,7 @@ mmsystem
11341141
MNC
11351142
MNOPQ
11361143
MNOPQR
1144+
MNOPQRSTUVWXY
11371145
MODALFRAME
11381146
MODERNCORE
11391147
MONITORINFO
@@ -2044,6 +2052,7 @@ USRDLL
20442052
utr
20452053
UVWX
20462054
UVWXY
2055+
UVWXYZ
20472056
uwa
20482057
uwp
20492058
uxtheme
@@ -2304,7 +2313,9 @@ YOffset
23042313
YSubstantial
23052314
YVIRTUALSCREEN
23062315
YWalk
2316+
Zab
23072317
zabcd
2318+
Zabcdefghijklmn
23082319
Zabcdefghijklmnopqrstuvwxyz
23092320
ZCmd
23102321
ZCtrl

src/host/ut_host/ScreenBufferTests.cpp

+112
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ class ScreenBufferTests
186186
TEST_METHOD(InsertReplaceMode);
187187
TEST_METHOD(InsertChars);
188188
TEST_METHOD(DeleteChars);
189+
TEST_METHOD(HorizontalScrollOperations);
189190
TEST_METHOD(ScrollingWideCharsHorizontally);
190191

191192
TEST_METHOD(EraseScrollbackTests);
@@ -4346,6 +4347,117 @@ void ScreenBufferTests::DeleteChars()
43464347
}
43474348
}
43484349

4350+
void ScreenBufferTests::HorizontalScrollOperations()
4351+
{
4352+
BEGIN_TEST_METHOD_PROPERTIES()
4353+
TEST_METHOD_PROPERTY(L"Data:op", L"{0, 1, 2, 3}")
4354+
END_TEST_METHOD_PROPERTIES();
4355+
4356+
enum Op : int
4357+
{
4358+
DECIC,
4359+
DECDC,
4360+
DECFI,
4361+
DECBI
4362+
} op;
4363+
VERIFY_SUCCEEDED(TestData::TryGetValue(L"op", (int&)op));
4364+
4365+
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
4366+
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
4367+
auto& stateMachine = si.GetStateMachine();
4368+
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
4369+
4370+
// Set the buffer width to 40, with a centered viewport of 20.
4371+
const auto bufferWidth = 40;
4372+
const auto bufferHeight = si.GetBufferSize().Height();
4373+
const auto viewportStart = 10;
4374+
const auto viewportEnd = viewportStart + 20;
4375+
VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false));
4376+
si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true);
4377+
4378+
// Set the margin area to columns 10 to 29 and rows 14 to 19 (zero based).
4379+
stateMachine.ProcessString(L"\x1b[?69h");
4380+
stateMachine.ProcessString(L"\x1b[11;30s");
4381+
stateMachine.ProcessString(L"\x1b[15;20r");
4382+
// Make sure we clear the margins on exit so they can't break other tests.
4383+
auto clearMargins = wil::scope_exit([&] {
4384+
stateMachine.ProcessString(L"\x1b[r");
4385+
stateMachine.ProcessString(L"\x1b[s");
4386+
stateMachine.ProcessString(L"\x1b[?69l");
4387+
});
4388+
4389+
// Fill the buffer with text. Red on Blue.
4390+
const auto bufferChars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn";
4391+
const auto bufferAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
4392+
_FillLines(0, 25, bufferChars, bufferAttr);
4393+
4394+
// Set the attributes that will be used to fill the revealed area.
4395+
auto fillAttr = TextAttribute{ RGB(12, 34, 56), RGB(78, 90, 12) };
4396+
fillAttr.SetCrossedOut(true);
4397+
fillAttr.SetReverseVideo(true);
4398+
fillAttr.SetUnderlined(true);
4399+
si.SetAttributes(fillAttr);
4400+
// But note that the meta attributes are expected to be cleared.
4401+
auto expectedFillAttr = fillAttr;
4402+
expectedFillAttr.SetStandardErase();
4403+
4404+
auto& cursor = si.GetTextBuffer().GetCursor();
4405+
switch (op)
4406+
{
4407+
case DECIC:
4408+
Log::Comment(L"Insert 4 columns in the middle of the margin area.");
4409+
cursor.SetPosition({ 20, 17 });
4410+
stateMachine.ProcessString(L"\x1b[4'}");
4411+
VERIFY_ARE_EQUAL(til::point(20, 17), cursor.GetPosition(), L"The cursor should not move.");
4412+
VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"KLMNOPQRST", bufferAttr),
4413+
L"The margin area left of the cursor position should remain unchanged.");
4414+
VERIFY_IS_TRUE(_ValidateLinesContain(20, 14, 20, L" ", expectedFillAttr),
4415+
L"4 blank columns should be inserted at the cursor position.");
4416+
VERIFY_IS_TRUE(_ValidateLinesContain(24, 14, 20, L"UVWXYZ", bufferAttr),
4417+
L"The area right of that should be scrolled right by 4 columns.");
4418+
break;
4419+
case DECDC:
4420+
Log::Comment(L"Delete 4 columns in the middle of the margin area.");
4421+
cursor.SetPosition({ 20, 17 });
4422+
stateMachine.ProcessString(L"\x1b[4'~");
4423+
VERIFY_ARE_EQUAL(til::point(20, 17), cursor.GetPosition(), L"The cursor should not move.");
4424+
VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"KLMNOPQRSTYZabcd", bufferAttr),
4425+
L"The area right of the cursor position should be scrolled left by 4 columns.");
4426+
VERIFY_IS_TRUE(_ValidateLinesContain(26, 14, 20, L" ", expectedFillAttr),
4427+
L"4 blank columns should be inserted at the right of the margin area.");
4428+
break;
4429+
case DECFI:
4430+
Log::Comment(L"Forward index 4 times, 2 columns before the right margin.");
4431+
cursor.SetPosition({ 27, 17 });
4432+
stateMachine.ProcessString(L"\x1b\x39\x1b\x39\x1b\x39\x1b\x39");
4433+
VERIFY_ARE_EQUAL(til::point(29, 17), cursor.GetPosition(), L"The cursor should not pass the right margin.");
4434+
VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L"MNOPQRSTUVWXYZabcd", bufferAttr),
4435+
L"The margin area should scroll left by 2 columns.");
4436+
VERIFY_IS_TRUE(_ValidateLinesContain(28, 14, 20, L" ", expectedFillAttr),
4437+
L"2 blank columns should be inserted at the right of the margin area.");
4438+
break;
4439+
case DECBI:
4440+
Log::Comment(L"Back index 4 times, 2 columns before the left margin.");
4441+
cursor.SetPosition({ 12, 17 });
4442+
stateMachine.ProcessString(L"\x1b\x36\x1b\x36\x1b\x36\x1b\x36");
4443+
VERIFY_ARE_EQUAL(til::point(10, 17), cursor.GetPosition(), L"The cursor should not pass the left margin.");
4444+
VERIFY_IS_TRUE(_ValidateLinesContain(10, 14, 20, L" ", expectedFillAttr),
4445+
L"2 blank columns should be inserted at the left of the margin area.");
4446+
VERIFY_IS_TRUE(_ValidateLinesContain(12, 14, 20, L"KLMNOPQRSTUVWXYZab", bufferAttr),
4447+
L"The rest of the margin area should scroll right by 2 columns.");
4448+
break;
4449+
}
4450+
4451+
VERIFY_IS_TRUE(_ValidateLinesContain(0, 14, bufferChars, bufferAttr),
4452+
L"Content above the top margin should remain unchanged.");
4453+
VERIFY_IS_TRUE(_ValidateLinesContain(20, 25, bufferChars, bufferAttr),
4454+
L"Content below the bottom margin should remain unchanged.");
4455+
VERIFY_IS_TRUE(_ValidateLinesContain(0, 14, 20, L"ABCDEFGHIJ", bufferAttr),
4456+
L"Content before the left margin should remain unchanged.");
4457+
VERIFY_IS_TRUE(_ValidateLinesContain(30, 14, 20, L"efghijklmn", bufferAttr),
4458+
L"Content beyond the right margin should remain unchanged.");
4459+
}
4460+
43494461
void ScreenBufferTests::ScrollingWideCharsHorizontally()
43504462
{
43514463
// The point of this test is to make sure wide characters can be

src/terminal/adapter/ITermDispatch.hpp

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
5151
virtual bool ScrollDown(const VTInt distance) = 0; // SD
5252
virtual bool InsertLine(const VTInt distance) = 0; // IL
5353
virtual bool DeleteLine(const VTInt distance) = 0; // DL
54+
virtual bool InsertColumn(const VTInt distance) = 0; // DECIC
55+
virtual bool DeleteColumn(const VTInt distance) = 0; // DECDC
5456
virtual bool SetKeypadMode(const bool applicationMode) = 0; // DECKPAM, DECKPNM
5557
virtual bool SetAnsiMode(const bool ansiMode) = 0; // DECANM
5658
virtual bool SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) = 0; // DECSTBM
@@ -59,6 +61,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
5961
virtual bool CarriageReturn() = 0; // CR
6062
virtual bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) = 0; // IND, NEL, LF, FF, VT
6163
virtual bool ReverseLineFeed() = 0; // RI
64+
virtual bool BackIndex() = 0; // DECBI
65+
virtual bool ForwardIndex() = 0; // DECFI
6266
virtual bool SetWindowTitle(std::wstring_view title) = 0; // OscWindowTitle
6367
virtual bool HorizontalTabSet() = 0; // HTS
6468
virtual bool ForwardTab(const VTInt numTabs) = 0; // CHT, HT

0 commit comments

Comments
 (0)