Repository: microsoft/terminal (language: cpp)

Issue:
Add support for horizontal margin sequences
# Description of the new feature/enhancement

The `DECSLRM` escape sequence lets you set left and right margins, so you can wrap your output within a given horizontal range, and also limit the scrolling within those boundaries. This is useful for apps like multiplexers, where you can have two panes side by side, and you need to be able to scroll the one side independently of the other.

# Proposed technical implementation details (optional)

There are actually two sequences we need to implement for this. The first is `DECLRMM` (Left Right Margin Mode), without which the margin functionality won't be active. It's disabled by default because you can't use horizontal margins at the same time as double-width line attributes.

The main sequence is `DECSLRM` (Set Left Right Margins), which works similarly to the `DECSTBM` sequence (Set Top Bottom Margins) which we already support. But there a bunch of operations we then need to update to take those margins into account - cursor movement, insert and delete ops, text output, etc.

I should also note that the `DECSLRM` sequence clashes with the `ANSISYSSC` sequence which we already support. But the way most modern terminals deal with that is to disable `ANSISYSSC` when the `DECLRMM` mode is enabled. So by default `CSI s` is `ANSISYSSC` (as we have it now), but with `DECLRMM` enabled it's interpreted as `DECSLRM`.


Discussion / hints:

Fix patch:
diff --git a/.github/actions/spelling/expect/alphabet.txt b/.github/actions/spelling/expect/alphabet.txt
index 68b264a0b14..b2c7597eee8 100644
--- a/.github/actions/spelling/expect/alphabet.txt
+++ b/.github/actions/spelling/expect/alphabet.txt
@@ -9,6 +9,7 @@ ABCDEFGHIJ
 abcdefghijk
 ABCDEFGHIJKLMNO
 abcdefghijklmnop
+ABCDEFGHIJKLMNOPQRS
 ABCDEFGHIJKLMNOPQRST
 ABCG
 ABE
@@ -21,9 +22,12 @@ BBGGRR
 efg
 EFG
 EFGh
+KLMNOQQQQQQQQQQ
 QQQQQQQQQQABCDEFGHIJ
+QQQQQQQQQQABCDEFGHIJKLMNOPQRS
 QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ
 QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
+QQQQQQQQQQABCDEFGHIJPQRST
 QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ
 qrstuvwxyz
 qwerty
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index f129cafae24..d9226c95dc4 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -222,6 +222,10 @@ class ScreenBufferTests
     TEST_METHOD(CursorUpDownOutsideMargins);
     TEST_METHOD(CursorUpDownExactlyAtMargins);
 
+    TEST_METHOD(CursorLeftRightAcrossMargins);
+    TEST_METHOD(CursorLeftRightOutsideMargins);
+    TEST_METHOD(CursorLeftRightExactlyAtMargins);
+
     TEST_METHOD(CursorNextPreviousLine);
     TEST_METHOD(CursorPositionRelative);
 
@@ -3945,11 +3949,11 @@ void ScreenBufferTests::InsertReplaceMode()
 void ScreenBufferTests::InsertChars()
 {
     BEGIN_TEST_METHOD_PROPERTIES()
-        TEST_METHOD_PROPERTY(L"Data:setMargins", L"{false, true}")
+        TEST_METHOD_PROPERTY(L"Data:setVerticalMargins", L"{false, true}")
     END_TEST_METHOD_PROPERTIES();
 
-    bool setMargins;
-    VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins));
+    bool setVerticalMargins;
+    VERIFY_SUCCEEDED(TestData::TryGetValue(L"setVerticalMargins", setVerticalMargins));
 
     auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
     auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
@@ -3964,16 +3968,24 @@ void ScreenBufferTests::InsertChars()
     VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false));
     si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true);
 
-    // Tests are run both with and without the DECSTBM margins set. This should not alter
-    // the results, since the ICH operation is not affected by vertical margins.
-    stateMachine.ProcessString(setMargins ? L"\x1b[15;20r" : L"\x1b[r");
+    // Tests are run both with and without the DECSTBM margins set. And while they
+    // don't affect the ICH operation directly, when we're outside the vertical
+    // margins, the horizontal margins won't apply, and that does affect ICH.
+    const auto horizontalMarginsActive = !setVerticalMargins;
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[11;30s");
+    stateMachine.ProcessString(setVerticalMargins ? L"\x1b[15;20r" : L"\x1b[r");
     // 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"); });
+    auto clearMargins = wil::scope_exit([&] {
+        stateMachine.ProcessString(L"\x1b[r");
+        stateMachine.ProcessString(L"\x1b[s");
+        stateMachine.ProcessString(L"\x1b[?69l");
+    });
 
     Log::Comment(
         L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. "
         L"Then insert 5 spaces at the cursor. Watch spaces get inserted, text slides right "
-        L"out of the viewport, pushing some of the Qs out of the buffer.");
+        L"up to the right margin or off the edge of the buffer.");
 
     const auto insertLine = til::CoordType{ 10 };
     auto insertPos = til::CoordType{ 20 };
@@ -4007,7 +4019,8 @@ void ScreenBufferTests::InsertChars()
 
     // Insert 5 spaces at the cursor position.
     // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
-    //  After: QQQQQQQQQQABCDEFGHIJ     KLMNOPQRSTQQQQQ
+    //  After: QQQQQQQQQQABCDEFGHIJ     KLMNOQQQQQQQQQQ (horizontal margins active)
+    //  After: QQQQQQQQQQABCDEFGHIJ     KLMNOPQRSTQQQQQ (horizontal margins inactive)
     Log::Comment(L"Inserting 5 spaces in the middle of the line.");
     auto before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
     stateMachine.ProcessString(L"\x1b[5@");
@@ -4023,18 +4036,29 @@ void ScreenBufferTests::InsertChars()
                    L"First half of the alphabet should remain unchanged.");
     VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L"     ", expectedFillAttr),
                    L"Spaces should be inserted with standard erase attributes at the cursor position.");
-    VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNOPQRST", textAttr),
-                   L"Second half of the alphabet should have moved to the right by the number of spaces inserted.");
-    VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd + 5, insertLine }, L"QQQQQ", bufferAttr),
-                   L"Field of Qs right of the viewport should be moved right, half pushed outside the buffer.");
+
+    if (horizontalMarginsActive)
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNO", textAttr),
+                       L"Second half of the alphabet should have moved to the right but only up to right margin.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQ", bufferAttr),
+                       L"Field of Qs right of the margin area should remain unchanged.");
+    }
+    else
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ insertPos + 5, insertLine }, L"KLMNOPQRST", textAttr),
+                       L"Second half of the alphabet should have moved to the right by the number of spaces inserted.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd + 5, insertLine }, L"QQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should be moved right, half pushed outside the buffer.");
+    }
 
     Log::Comment(
-        L"Test 2: Inserting at the exact end of the line. Same line structure. "
-        L"Move cursor to right edge of window and insert > 1 space. "
+        L"Test 2: Inserting at the exact end of the scroll area. Same line structure. "
+        L"Move cursor to right edge of window or right margin and insert > 1 space. "
         L"Only 1 should be inserted, everything else unchanged.");
 
     // Move cursor to right edge.
-    insertPos = bufferWidth - 1;
+    insertPos = horizontalMarginsActive ? viewportEnd - 1 : bufferWidth - 1;
     VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true));
     expectedCursor = cursor.GetPosition();
 
@@ -4046,8 +4070,9 @@ void ScreenBufferTests::InsertChars()
 
     // Insert 5 spaces at the right edge. Only 1 should be inserted.
     // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
-    //  After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ
-    Log::Comment(L"Inserting 5 spaces at the right edge of the buffer.");
+    //  After: QQQQQQQQQQABCDEFGHIJKLMNOPQRS QQQQQQQQQQ (horizontal margins active)
+    //  After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ  (horizontal margins inactive)
+    Log::Comment(L"Inserting 5 spaces at the right edge.");
     before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
     stateMachine.ProcessString(L"\x1b[5@");
     after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
@@ -4058,20 +4083,32 @@ void ScreenBufferTests::InsertChars()
     // Verify the updated structure of the line.
     VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr),
                    L"Field of Qs left of the viewport should remain unchanged.");
-    VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr),
-                   L"Entire viewport range should remain unchanged.");
-    VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQ", bufferAttr),
-                   L"Field of Qs right of the viewport should remain unchanged except for the last spot.");
     VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L" ", expectedFillAttr),
                    L"One space should be inserted with standard erase attributes at the cursor position.");
 
+    if (horizontalMarginsActive)
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRS", textAttr),
+                       L"Viewport range should remain unchanged except for the last spot.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should remain unchanged.");
+    }
+    else
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, insertLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr),
+                       L"Entire viewport range should remain unchanged.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should remain unchanged except for the last spot.");
+    }
+
     Log::Comment(
-        L"Test 3: Inserting at the exact beginning of the line. Same line structure. "
-        L"Move cursor to left edge of buffer and insert > buffer width of space. "
-        L"The whole row should be replaced with spaces.");
+        L"Test 3: Inserting at the beginning of the scroll area. Same line structure. "
+        L"Move cursor to left edge of buffer or left margin and insert > buffer width of space. "
+        L"The whole scroll area should be replaced with spaces.");
 
     // Move cursor to left edge.
-    VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, insertLine }, true));
+    insertPos = horizontalMarginsActive ? viewportStart : 0;
+    VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true));
     expectedCursor = cursor.GetPosition();
 
     // Fill the entire line with Qs. Blue on Green.
@@ -4080,10 +4117,11 @@ void ScreenBufferTests::InsertChars()
     // Fill the viewport range with text. Red on Blue.
     _FillLine({ viewportStart, insertLine }, textChars, textAttr);
 
-    // Insert greater than the buffer width at the left edge. The entire line should be erased.
+    // Insert greater than the buffer width at the left edge.
     // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
-    //  After:
-    Log::Comment(L"Inserting 100 spaces at the left edge of the buffer.");
+    //  After: QQQQQQQQQQ                    QQQQQQQQQQ (horizontal margins active)
+    //  After:                                          (horizontal margins inactive)
+    Log::Comment(L"Inserting 100 spaces at the left edge.");
     before = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
     stateMachine.ProcessString(L"\x1b[100@");
     after = si.GetTextBuffer().GetRowByOffset(insertLine).GetText();
@@ -4092,18 +4130,30 @@ void ScreenBufferTests::InsertChars()
     VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from insert operation.");
 
     // Verify the updated structure of the line.
-    VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', expectedFillAttr),
-                   L"A whole line of spaces was inserted at the start, erasing the line.");
+    if (horizontalMarginsActive)
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ 0, insertLine }, L"QQQQQQQQQQ", bufferAttr),
+                       L"Field of Qs left of the viewport should remain unchanged.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ insertPos, insertLine }, L"                    ", expectedFillAttr),
+                       L"Entire viewport range should be erased with spaces.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, insertLine }, L"QQQQQQQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should remain unchanged.");
+    }
+    else
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains(insertLine, L' ', expectedFillAttr),
+                       L"A whole line of spaces was inserted at the start, erasing the line.");
+    }
 }
 
 void ScreenBufferTests::DeleteChars()
 {
     BEGIN_TEST_METHOD_PROPERTIES()
-        TEST_METHOD_PROPERTY(L"Data:setMargins", L"{false, true}")
+        TEST_METHOD_PROPERTY(L"Data:setVerticalMargins", L"{false, true}")
     END_TEST_METHOD_PROPERTIES();
 
-    bool setMargins;
-    VERIFY_SUCCEEDED(TestData::TryGetValue(L"setMargins", setMargins));
+    bool setVerticalMargins;
+    VERIFY_SUCCEEDED(TestData::TryGetValue(L"setVerticalMargins", setVerticalMargins));
 
     auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
     auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
@@ -4118,16 +4168,24 @@ void ScreenBufferTests::DeleteChars()
     VERIFY_SUCCEEDED(si.ResizeScreenBuffer({ bufferWidth, bufferHeight }, false));
     si.SetViewport(Viewport::FromExclusive({ viewportStart, 0, viewportEnd, 25 }), true);
 
-    // Tests are run both with and without the DECSTBM margins set. This should not alter
-    // the results, since the DCH operation is not affected by vertical margins.
-    stateMachine.ProcessString(setMargins ? L"\x1b[15;20r" : L"\x1b[r");
+    // Tests are run both with and without the DECSTBM margins set. And while they
+    // don't affect the DCH operation directly, when we're outside the vertical
+    // margins, the horizontal margins won't apply, and that does affect DCH.
+    const auto horizontalMarginsActive = !setVerticalMargins;
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[11;30s");
+    stateMachine.ProcessString(setVerticalMargins ? L"\x1b[15;20r" : L"\x1b[r");
     // 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"); });
+    auto clearMargins = wil::scope_exit([&] {
+        stateMachine.ProcessString(L"\x1b[r");
+        stateMachine.ProcessString(L"\x1b[s");
+        stateMachine.ProcessString(L"\x1b[?69l");
+    });
 
     Log::Comment(
         L"Test 1: Fill the line with Qs. Write some text within the viewport boundaries. "
         L"Then delete 5 characters at the cursor. Watch the rest of the line slide left, "
-        L"replacing the deleted characters, with spaces inserted at the end of the line.");
+        L"with spaces inserted at the end of the line or the right margin.");
 
     const auto deleteLine = til::CoordType{ 10 };
     auto deletePos = til::CoordType{ 20 };
@@ -4161,7 +4219,8 @@ void ScreenBufferTests::DeleteChars()
 
     // Delete 5 characters at the cursor position.
     // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
-    //  After: QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ
+    //  After: QQQQQQQQQQABCDEFGHIJPQRST     QQQQQQQQQQ (horizontal margins active)
+    //  After: QQQQQQQQQQABCDEFGHIJPQRSTQQQQQQQQQQ      (horizontal margins inactive)
     Log::Comment(L"Deleting 5 characters in the middle of the line.");
     auto before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
     stateMachine.ProcessString(L"\x1b[5P");
@@ -4177,18 +4236,29 @@ void ScreenBufferTests::DeleteChars()
                    L"First half of the alphabet should remain unchanged.");
     VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L"PQRST", textAttr),
                    L"Only half of the second part of the alphabet remains.");
-    VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
-                   L"Field of Qs right of the viewport should be moved left.");
-    VERIFY_IS_TRUE(_ValidateLineContains({ bufferWidth - 5, deleteLine }, L"     ", expectedFillAttr),
-                   L"The rest of the line should be replaced with spaces with standard erase attributes.");
+
+    if (horizontalMarginsActive)
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L"     ", expectedFillAttr),
+                       L"The rest of the margin area should be replaced with spaces with standard erase attributes.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should remain unchanged.");
+    }
+    else
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd - 5, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should be moved left.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ bufferWidth - 5, deleteLine }, L"     ", expectedFillAttr),
+                       L"The rest of the line should be replaced with spaces with standard erase attributes.");
+    }
 
     Log::Comment(
-        L"Test 2: Deleting at the exact end of the line. Same line structure. "
-        L"Move cursor to right edge of window and delete > 1 character. "
+        L"Test 2: Deleting at the exact end of the scroll area. Same line structure. "
+        L"Move cursor to right edge of window or right margin and delete > 1 character. "
         L"Only 1 should be deleted, everything else unchanged.");
 
     // Move cursor to right edge.
-    deletePos = bufferWidth - 1;
+    deletePos = horizontalMarginsActive ? viewportEnd - 1 : bufferWidth - 1;
     VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true));
     expectedCursor = cursor.GetPosition();
 
@@ -4200,8 +4270,9 @@ void ScreenBufferTests::DeleteChars()
 
     // Delete 5 characters at the right edge. Only 1 should be deleted.
     // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
-    //  After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ
-    Log::Comment(L"Deleting 5 characters at the right edge of the buffer.");
+    //  After: QQQQQQQQQQABCDEFGHIJKLMNOPQRS QQQQQQQQQQ (horizontal margins active)
+    //  After: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQ  (horizontal margins inactive)
+    Log::Comment(L"Deleting 5 characters at the right edge.");
     before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
     stateMachine.ProcessString(L"\x1b[5P");
     after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
@@ -4212,20 +4283,32 @@ void ScreenBufferTests::DeleteChars()
     // Verify the updated structure of the line.
     VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
                    L"Field of Qs left of the viewport should remain unchanged.");
-    VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr),
-                   L"Entire viewport range should remain unchanged.");
-    VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQ", bufferAttr),
-                   L"Field of Qs right of the viewport should remain unchanged except for the last spot.");
     VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L" ", expectedFillAttr),
                    L"One character should be erased with standard erase attributes at the cursor position.");
 
+    if (horizontalMarginsActive)
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRS", textAttr),
+                       L"Viewport range should remain unchanged except for the last spot.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should remain unchanged.");
+    }
+    else
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportStart, deleteLine }, L"ABCDEFGHIJKLMNOPQRST", textAttr),
+                       L"Entire viewport range should remain unchanged.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should remain unchanged except for the last spot.");
+    }
+
     Log::Comment(
-        L"Test 3: Deleting at the exact beginning of the line. Same line structure. "
-        L"Move cursor to left edge of buffer and delete > buffer width of characters. "
-        L"The whole row should be replaced with spaces.");
+        L"Test 3: Deleting at the beginning of the scroll area. Same line structure. "
+        L"Move cursor to left edge of buffer or left margin and delete > buffer width of characters. "
+        L"The whole scroll area should be replaced with spaces.");
 
     // Move cursor to left edge.
-    VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, deleteLine }, true));
+    deletePos = horizontalMarginsActive ? viewportStart : 0;
+    VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true));
     expectedCursor = cursor.GetPosition();
 
     // Fill the entire line with Qs. Blue on Green.
@@ -4234,10 +4317,11 @@ void ScreenBufferTests::DeleteChars()
     // Fill the viewport range with text. Red on Blue.
     _FillLine({ viewportStart, deleteLine }, textChars, textAttr);
 
-    // Delete greater than the buffer width at the left edge. The entire line should be erased.
+    // Delete greater than the buffer width at the left edge.
     // Before: QQQQQQQQQQABCDEFGHIJKLMNOPQRSTQQQQQQQQQQ
-    //  After:
-    Log::Comment(L"Deleting 100 characters at the left edge of the buffer.");
+    //  After: QQQQQQQQQQ                    QQQQQQQQQQ (horizontal margins active)
+    //  After:                                          (horizontal margins inactive)
+    Log::Comment(L"Deleting 100 characters at the left edge.");
     before = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
     stateMachine.ProcessString(L"\x1b[100P");
     after = si.GetTextBuffer().GetRowByOffset(deleteLine).GetText();
@@ -4246,8 +4330,20 @@ void ScreenBufferTests::DeleteChars()
     VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition(), L"Verify cursor didn't move from delete operation.");
 
     // Verify the updated structure of the line.
-    VERIFY_IS_TRUE(_ValidateLineContains(deleteLine, L' ', expectedFillAttr),
-                   L"A whole line of spaces was inserted from the right, erasing the line.");
+    if (horizontalMarginsActive)
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains({ 0, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
+                       L"Field of Qs left of the viewport should remain unchanged.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ deletePos, deleteLine }, L"                    ", expectedFillAttr),
+                       L"Entire viewport range should be erased with spaces.");
+        VERIFY_IS_TRUE(_ValidateLineContains({ viewportEnd, deleteLine }, L"QQQQQQQQQQ", bufferAttr),
+                       L"Field of Qs right of the viewport should remain unchanged.");
+    }
+    else
+    {
+        VERIFY_IS_TRUE(_ValidateLineContains(deleteLine, L' ', expectedFillAttr),
+                       L"A whole line of spaces was inserted from the right, erasing the line.");
+    }
 }
 
 void ScreenBufferTests::ScrollingWideCharsHorizontally()
@@ -4609,24 +4705,24 @@ void ScreenBufferTests::ProtectedAttributeTests()
 void _CommonScrollingSetup()
 {
     // Used for testing MSFT:20204600
-    // Place an A on the first line, and a B on the 6th line (index 5).
+    // Place As on the first line, and Bs on the 6th line (index 5).
     // Set the scrolling region in between those lines (so scrolling won't affect them.)
-    // First write "1\n2\n3\n4", to put 1-4 on the lines in between the A and B.
+    // First write "1111\n2222\n3333\n4444", to put 1-4 on the lines in between the A and B.
     // the viewport will look like:
-    // A
-    // 1
-    // 2
-    // 3
-    // 4
-    // B
-    // then write "\n5\n6\n7\n", which will cycle around the scroll region a bit.
+    // AAAAAA...
+    // 111111...
+    // 222222...
+    // 333333...
+    // 444444...
+    // BBBBBB...
+    // then write "\n5555\n6666\n7777\n", which will cycle around the scroll region a bit.
     // the viewport will look like:
-    // A
-    // 5
-    // 6
-    // 7
+    // AAAAAA...
+    // 555555...
+    // 666666...
+    // 777777...
     //
-    // B
+    // BBBBBB...
 
     auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
     auto& si = gci.GetActiveOutputBuffer();
@@ -4635,38 +4731,46 @@ void _CommonScrollingSetup()
     auto& cursor = si.GetTextBuffer().GetCursor();
     const auto oldView = si.GetViewport();
     const auto view = Viewport::FromDimensions({ 0, 0 }, { oldView.Width(), 6 });
+    const auto bufferWidth = tbi.GetSize().Width();
+    const auto attr = tbi.GetCurrentAttributes();
+
     si.SetViewport(view, true);
     cursor.SetPosition({ 0, 0 });
-    stateMachine.ProcessString(L"A");
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'A'));
     cursor.SetPosition({ 0, 5 });
-    stateMachine.ProcessString(L"B");
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'B'));
     stateMachine.ProcessString(L"\x1b[2;5r");
     stateMachine.ProcessString(L"\x1b[2;1H");
-    stateMachine.ProcessString(L"1\n2\n3\n4");
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'1'));
+    stateMachine.ProcessCharacter(L'\n');
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'2'));
+    stateMachine.ProcessCharacter(L'\n');
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'3'));
+    stateMachine.ProcessCharacter(L'\n');
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'4'));
 
     Log::Comment(NoThrowString().Format(
         L"cursor=%s", VerifyOutputTraits<til::point>::ToString(cursor.GetPosition()).GetBuffer()));
     Log::Comment(NoThrowString().Format(
         L"viewport=%s", VerifyOutputTraits<til::inclusive_rect>::ToString(si.GetViewport().ToInclusive()).GetBuffer()));
 
-    VERIFY_ARE_EQUAL(1, cursor.GetPosition().x);
+    VERIFY_ARE_EQUAL(bufferWidth - 1, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(4, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"1", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"2", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"3", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"4", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter5->Chars());
-    }
 
-    stateMachine.ProcessString(L"\n5\n6\n7\n");
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'1', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L'2', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L'3', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L'4', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
+
+    stateMachine.ProcessCharacter(L'\n');
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'5'));
+    stateMachine.ProcessCharacter(L'\n');
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'6'));
+    stateMachine.ProcessCharacter(L'\n');
+    stateMachine.ProcessString(std::wstring(bufferWidth, L'7'));
+    stateMachine.ProcessCharacter(L'\n');
 
     Log::Comment(NoThrowString().Format(
         L"cursor=%s", VerifyOutputTraits<til::point>::ToString(cursor.GetPosition()).GetBuffer()));
@@ -4675,21 +4779,13 @@ void _CommonScrollingSetup()
 
     VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(4, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"5", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"6", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"7", iter3->Chars());
-        // Chars() will return a single space for an empty row.
-        VERIFY_ARE_EQUAL(L"\x20", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 }
 
 void ScreenBufferTests::ScrollUpInMargins()
@@ -4703,7 +4799,8 @@ void ScreenBufferTests::ScrollUpInMargins()
     auto& si = gci.GetActiveOutputBuffer();
     auto& tbi = si.GetTextBuffer();
     auto& stateMachine = si.GetStateMachine();
-    auto& cursor = si.GetTextBuffer().GetCursor();
+    auto& cursor = tbi.GetCursor();
+    const auto attr = tbi.GetCurrentAttributes();
 
     // Execute a Scroll Up command
     stateMachine.ProcessString(L"\x1b[S");
@@ -4715,20 +4812,35 @@ void ScreenBufferTests::ScrollUpInMargins()
 
     VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(4, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"6", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"7", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
+
+    _CommonScrollingSetup();
+    // Set horizontal margins
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[3;6s");
+    auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); });
+
+    // Execute a Scroll Up command
+    stateMachine.ProcessString(L"\x1b[S");
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 1 }, L"55", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 1 }, L"6666", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 1 }, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L"7777", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L"    ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 }
 
 void ScreenBufferTests::ScrollDownInMargins()
@@ -4742,7 +4854,8 @@ void ScreenBufferTests::ScrollDownInMargins()
     auto& si = gci.GetActiveOutputBuffer();
     auto& tbi = si.GetTextBuffer();
     auto& stateMachine = si.GetStateMachine();
-    auto& cursor = si.GetTextBuffer().GetCursor();
+    auto& cursor = tbi.GetCursor();
+    const auto attr = tbi.GetCurrentAttributes();
 
     // Execute a Scroll Down command
     stateMachine.ProcessString(L"\x1b[T");
@@ -4754,20 +4867,37 @@ void ScreenBufferTests::ScrollDownInMargins()
 
     VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(4, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"5", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"6", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"7", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
+
+    _CommonScrollingSetup();
+    // Set horizontal margins
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[3;6s");
+    auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); });
+
+    // Execute a Scroll Down command
+    stateMachine.ProcessString(L"\x1b[T");
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 1 }, L"55", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 1 }, L"    ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 1 }, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L"5555", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L"6666", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 4 }, L"  ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 4 }, L"7777", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 4 }, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 }
 
 void ScreenBufferTests::InsertLinesInMargins()
@@ -4781,7 +4911,8 @@ void ScreenBufferTests::InsertLinesInMargins()
     auto& si = gci.GetActiveOutputBuffer();
     auto& tbi = si.GetTextBuffer();
     auto& stateMachine = si.GetStateMachine();
-    auto& cursor = si.GetTextBuffer().GetCursor();
+    auto& cursor = tbi.GetCursor();
+    const auto attr = tbi.GetCurrentAttributes();
 
     // Move to column 5 of line 3
     stateMachine.ProcessString(L"\x1b[3;5H");
@@ -4796,20 +4927,13 @@ void ScreenBufferTests::InsertLinesInMargins()
     // Verify cursor moved to left margin.
     VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(2, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"5", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"6", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 
     Log::Comment(
         L"Does the common scrolling setup, then inserts one line with no "
@@ -4831,20 +4955,44 @@ void ScreenBufferTests::InsertLinesInMargins()
     // Verify cursor moved to left margin.
     VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(1, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"5", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"6", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"7", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L' ', attr));
+
+    Log::Comment(
+        L"Does the common scrolling setup, then inserts two lines inside the "
+        L"margin boundaries, and verifies the rows have what we'd expect.");
+
+    _CommonScrollingSetup();
+    // Set horizontal margins
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[3;6s");
+    auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); });
+    // Move to column 5 of line 3
+    stateMachine.ProcessString(L"\x1b[3;5H");
+    // Insert 2 lines
+    stateMachine.ProcessString(L"\x1b[2L");
+
+    // Verify cursor moved to left margin.
+    VERIFY_ARE_EQUAL(2, cursor.GetPosition().x);
+    VERIFY_ARE_EQUAL(2, cursor.GetPosition().y);
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L"    ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L"    ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 4 }, L"  ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 4 }, L"6666", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 4 }, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 }
 
 void ScreenBufferTests::DeleteLinesInMargins()
@@ -4858,7 +5006,8 @@ void ScreenBufferTests::DeleteLinesInMargins()
     auto& si = gci.GetActiveOutputBuffer();
     auto& tbi = si.GetTextBuffer();
     auto& stateMachine = si.GetStateMachine();
-    auto& cursor = si.GetTextBuffer().GetCursor();
+    auto& cursor = tbi.GetCursor();
+    const auto attr = tbi.GetCurrentAttributes();
 
     // Move to column 5 of line 3
     stateMachine.ProcessString(L"\x1b[3;5H");
@@ -4873,20 +5022,13 @@ void ScreenBufferTests::DeleteLinesInMargins()
     // Verify cursor moved to left margin.
     VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(2, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"5", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 
     Log::Comment(
         L"Does the common scrolling setup, then deletes one line with no "
@@ -4908,20 +5050,42 @@ void ScreenBufferTests::DeleteLinesInMargins()
     // Verify cursor moved to left margin.
     VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(1, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"6", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"7", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L'B', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L' ', attr));
+
+    Log::Comment(
+        L"Does the common scrolling setup, then deletes two lines inside the "
+        L"margin boundaries, and verifies the rows have what we'd expect.");
+
+    _CommonScrollingSetup();
+    // Set horizontal margins
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[3;6s");
+    auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); });
+    // Move to column 5 of line 3
+    stateMachine.ProcessString(L"\x1b[3;5H");
+    // Delete 2 lines
+    stateMachine.ProcessString(L"\x1b[2M");
+
+    // Verify cursor moved to left margin.
+    VERIFY_ARE_EQUAL(2, cursor.GetPosition().x);
+    VERIFY_ARE_EQUAL(2, cursor.GetPosition().y);
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L"    ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L"    ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 }
 
 void ScreenBufferTests::ReverseLineFeedInMargins()
@@ -4935,7 +5099,8 @@ void ScreenBufferTests::ReverseLineFeedInMargins()
     auto& si = gci.GetActiveOutputBuffer();
     auto& tbi = si.GetTextBuffer();
     auto& stateMachine = si.GetStateMachine();
-    auto& cursor = si.GetTextBuffer().GetCursor();
+    auto& cursor = tbi.GetCursor();
+    const auto attr = tbi.GetCurrentAttributes();
 
     // Move to column 5 of line 2, the top margin
     stateMachine.ProcessString(L"\x1b[2;5H");
@@ -4949,20 +5114,13 @@ void ScreenBufferTests::ReverseLineFeedInMargins()
 
     VERIFY_ARE_EQUAL(4, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(1, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"A", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"\x20", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"5", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"6", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"7", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 
     Log::Comment(
         L"Does the common scrolling setup, then executes a reverse line feed "
@@ -4986,20 +5144,45 @@ void ScreenBufferTests::ReverseLineFeedInMargins()
 
     VERIFY_ARE_EQUAL(4, cursor.GetPosition().x);
     VERIFY_ARE_EQUAL(0, cursor.GetPosition().y);
-    {
-        auto iter0 = tbi.GetCellDataAt({ 0, 0 });
-        auto iter1 = tbi.GetCellDataAt({ 0, 1 });
-        auto iter2 = tbi.GetCellDataAt({ 0, 2 });
-        auto iter3 = tbi.GetCellDataAt({ 0, 3 });
-        auto iter4 = tbi.GetCellDataAt({ 0, 4 });
-        auto iter5 = tbi.GetCellDataAt({ 0, 5 });
-        VERIFY_ARE_EQUAL(L"\x20", iter0->Chars());
-        VERIFY_ARE_EQUAL(L"A", iter1->Chars());
-        VERIFY_ARE_EQUAL(L"5", iter2->Chars());
-        VERIFY_ARE_EQUAL(L"6", iter3->Chars());
-        VERIFY_ARE_EQUAL(L"7", iter4->Chars());
-        VERIFY_ARE_EQUAL(L"B", iter5->Chars());
-    }
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(1, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(2, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(3, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(4, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
+
+    Log::Comment(
+        L"Does the common scrolling setup, then executes a reverse line feed "
+        L"below the top margin, and verifies the rows have what we'd expect.");
+
+    _CommonScrollingSetup();
+    // Set horizontal margins
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[3;6s");
+    auto resetMargins = wil::scope_exit([&] { stateMachine.ProcessString(L"\x1b[?69l"); });
+    // Move to column 5 of line 2, the top margin
+    stateMachine.ProcessString(L"\x1b[2;5H");
+    // Execute a reverse line feed (RI)
+    stateMachine.ProcessString(L"\x1bM");
+
+    VERIFY_ARE_EQUAL(4, cursor.GetPosition().x);
+    VERIFY_ARE_EQUAL(1, cursor.GetPosition().y);
+
+    VERIFY_IS_TRUE(_ValidateLineContains(0, L'A', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 1 }, L"55", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 1 }, L"    ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 1 }, L'5', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 2 }, L"66", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 2 }, L"5555", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 2 }, L'6', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 3 }, L"77", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 3 }, L"6666", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 3 }, L'7', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 0, 4 }, L"  ", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 2, 4 }, L"7777", attr));
+    VERIFY_IS_TRUE(_ValidateLineContains({ 6, 4 }, L' ', attr));
+    VERIFY_IS_TRUE(_ValidateLineContains(5, L'B', attr));
 }
 
 void ScreenBufferTests::LineFeedEscapeSequences()
@@ -5085,6 +5268,42 @@ void ScreenBufferTests::LineFeedEscapeSequences()
         VERIFY_IS_TRUE(_ValidateLineContains(initialY - 1, L'Q', {}));
         VERIFY_IS_TRUE(_ValidateLineContains(initialY, L' ', si.GetAttributes()));
     }
+
+    {
+        Log::Comment(L"Starting at the bottom of the scroll margins with horizontal margins");
+        // Set the margins to rows 5 to 10.
+        stateMachine.ProcessString(L"\x1b[5;10r");
+        // Set the margins to columns 3 to 6.
+        stateMachine.ProcessString(L"\x1b[?69h");
+        stateMachine.ProcessString(L"\x1b[3;6s");
+        // 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");
+            stateMachine.ProcessString(L"\x1b[s");
+            stateMachine.ProcessString(L"\x1b[?69l");
+        });
+
+        const auto initialY = si.GetViewport().Top() + 9;
+        const auto expectedY = initialY;
+        const auto initialXInMargins = 5;
+        const auto expectedXInMargins = withReturn ? 2 : initialXInMargins;
+        const auto expectedViewportTop = si.GetViewport().Top();
+        _FillLine(initialY, L'R', {});
+        cursor.SetPosition({ initialXInMargins, initialY });
+        stateMachine.ProcessString(escapeSequence);
+
+        VERIFY_ARE_EQUAL(expectedXInMargins, cursor.GetPosition().x);
+        VERIFY_ARE_EQUAL(expectedY, cursor.GetPosition().y);
+        VERIFY_ARE_EQUAL(expectedViewportTop, si.GetViewport().Top());
+        // Verify the line of Rs has been scrolled up only within the margins.
+        const auto defaultAttr = TextAttribute{};
+        VERIFY_IS_TRUE(_ValidateLineContains({ 0, initialY - 1 }, L"QQ", defaultAttr));
+        VERIFY_IS_TRUE(_ValidateLineContains({ 2, initialY - 1 }, L"RRRR", defaultAttr));
+        VERIFY_IS_TRUE(_ValidateLineContains({ 6, initialY - 1 }, L'Q', defaultAttr));
+        VERIFY_IS_TRUE(_ValidateLineContains({ 0, initialY }, L"RR", defaultAttr));
+        VERIFY_IS_TRUE(_ValidateLineContains({ 2, initialY }, L"    ", si.GetAttributes()));
+        VERIFY_IS_TRUE(_ValidateLineContains({ 6, initialY }, L'R', defaultAttr));
+    }
 }
 
 void ScreenBufferTests::ScrollLines256Colors()
@@ -5267,28 +5486,35 @@ void ScreenBufferTests::SetOriginMode()
     cursor.SetPosition({ 40, 12 });
     stateMachine.ProcessString(L"\x1B[6;20r");
     VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition());
+    stateMachine.ProcessString(L"\x1B[?69h");
+    cursor.SetPosition({ 40, 12 });
+    stateMachine.ProcessString(L"\x1B[31;50s");
+    VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition());
     Log::Comment(L"Cursor addressing is relative to the top-left of the screen.");
     stateMachine.ProcessString(L"\x1B[13;41H");
     VERIFY_ARE_EQUAL(til::point(40, 12), cursor.GetPosition());
-    Log::Comment(L"The cursor can be moved below the bottom margin.");
-    stateMachine.ProcessString(L"\x1B[23;41H");
-    VERIFY_ARE_EQUAL(til::point(40, 22), cursor.GetPosition());
+    Log::Comment(L"The cursor can be moved past the bottom right margins.");
+    stateMachine.ProcessString(L"\x1B[23;61H");
+    VERIFY_ARE_EQUAL(til::point(60, 22), cursor.GetPosition());
 
     // Testing the effects of DECOM being set (relative cursor addressing)
     Log::Comment(L"Setting DECOM moves the cursor to the top-left of the margin area.");
     cursor.SetPosition({ 40, 12 });
     stateMachine.ProcessString(L"\x1B[?6h");
-    VERIFY_ARE_EQUAL(til::point(0, 5), cursor.GetPosition());
+    VERIFY_ARE_EQUAL(til::point(30, 5), cursor.GetPosition());
     Log::Comment(L"Setting a margin moves the cursor to the top-left of the margin area.");
     cursor.SetPosition({ 40, 12 });
     stateMachine.ProcessString(L"\x1B[6;20r");
-    VERIFY_ARE_EQUAL(til::point(0, 5), cursor.GetPosition());
+    VERIFY_ARE_EQUAL(til::point(30, 5), cursor.GetPosition());
+    cursor.SetPosition({ 40, 12 });
+    stateMachine.ProcessString(L"\x1B[31;50s");
+    VERIFY_ARE_EQUAL(til::point(30, 5), cursor.GetPosition());
     Log::Comment(L"Cursor addressing is relative to the top-left of the margin area.");
-    stateMachine.ProcessString(L"\x1B[8;41H");
+    stateMachine.ProcessString(L"\x1B[8;11H");
     VERIFY_ARE_EQUAL(til::point(40, 12), cursor.GetPosition());
-    Log::Comment(L"The cursor cannot be moved below the bottom margin.");
-    stateMachine.ProcessString(L"\x1B[100;41H");
-    VERIFY_ARE_EQUAL(til::point(40, 19), cursor.GetPosition());
+    Log::Comment(L"The cursor cannot be moved past the bottom right margins.");
+    stateMachine.ProcessString(L"\x1B[100;100H");
+    VERIFY_ARE_EQUAL(til::point(49, 19), cursor.GetPosition());
 
     // Testing the effects of DECOM being reset (absolute cursor addressing)
     Log::Comment(L"Resetting DECOM moves the cursor to the top-left of the screen.");
@@ -5299,16 +5525,20 @@ void ScreenBufferTests::SetOriginMode()
     cursor.SetPosition({ 40, 12 });
     stateMachine.ProcessString(L"\x1B[6;20r");
     VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition());
+    cursor.SetPosition({ 40, 12 });
+    stateMachine.ProcessString(L"\x1B[31;50s");
+    VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition());
     Log::Comment(L"Cursor addressing is relative to the top-left of the screen.");
     stateMachine.ProcessString(L"\x1B[13;41H");
     VERIFY_ARE_EQUAL(til::point(40, 12), cursor.GetPosition());
-    Log::Comment(L"The cursor can be moved below the bottom margin.");
-    stateMachine.ProcessString(L"\x1B[23;41H");
-    VERIFY_ARE_EQUAL(til::point(40, 22), cursor.GetPosition());
+    Log::Comment(L"The cursor can be moved past the bottom right margins.");
+    stateMachine.ProcessString(L"\x1B[23;61H");
+    VERIFY_ARE_EQUAL(til::point(60, 22), cursor.GetPosition());
 
     // Testing the effects of DECOM being set with no margins
     Log::Comment(L"With no margins, setting DECOM moves the cursor to the top-left of the screen.");
     stateMachine.ProcessString(L"\x1B[r");
+    stateMachine.ProcessString(L"\x1B[s");
     cursor.SetPosition({ 40, 12 });
     stateMachine.ProcessString(L"\x1B[?6h");
     VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition());
@@ -5316,8 +5546,8 @@ void ScreenBufferTests::SetOriginMode()
     stateMachine.ProcessString(L"\x1B[13;41H");
     VERIFY_ARE_EQUAL(til::point(40, 12), cursor.GetPosition());
 
-    // Reset DECOM so we don't affect future tests
-    stateMachine.ProcessString(L"\x1B[?6l");
+    // Reset DECOM and DECLRMM so we don't affect future tests
+    stateMachine.ProcessString(L"\x1B[?6;69l");
 }
 
 void ScreenBufferTests::SetAutoWrapMode()
@@ -6219,6 +6449,136 @@ void ScreenBufferTests::CursorUpDownExactlyAtMargins()
     stateMachine.ProcessString(L"\x1b[r");
 }
 
+void ScreenBufferTests::CursorLeftRightAcrossMargins()
+{
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer();
+    auto& tbi = si.GetTextBuffer();
+    auto& stateMachine = si.GetStateMachine();
+    auto& cursor = si.GetTextBuffer().GetCursor();
+
+    VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24);
+
+    // Set some scrolling margins
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[31;50s");
+
+    stateMachine.ProcessString(L"\x1b[12;40H");
+    VERIFY_ARE_EQUAL(39, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"\x1b[99C");
+    VERIFY_ARE_EQUAL(49, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"X");
+    {
+        auto iter = tbi.GetCellDataAt({ 49, 11 });
+        VERIFY_ARE_EQUAL(L"X", iter->Chars());
+    }
+
+    stateMachine.ProcessString(L"\x1b[12;40H");
+    VERIFY_ARE_EQUAL(39, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"\x1b[99D");
+    VERIFY_ARE_EQUAL(30, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"Y");
+    {
+        auto iter = tbi.GetCellDataAt({ 30, 11 });
+        VERIFY_ARE_EQUAL(L"Y", iter->Chars());
+    }
+
+    stateMachine.ProcessString(L"\x1b[s");
+    stateMachine.ProcessString(L"\x1b[?69l");
+}
+
+void ScreenBufferTests::CursorLeftRightOutsideMargins()
+{
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer();
+    auto& tbi = si.GetTextBuffer();
+    auto& stateMachine = si.GetStateMachine();
+    auto& cursor = si.GetTextBuffer().GetCursor();
+
+    VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24);
+
+    // Set some scrolling margins
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[31;50s");
+
+    stateMachine.ProcessString(L"\x1b[12;1H");
+    VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"\x1b[1C");
+    VERIFY_ARE_EQUAL(1, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"Y");
+    {
+        auto iter = tbi.GetCellDataAt({ 1, 11 });
+        VERIFY_ARE_EQUAL(L"Y", iter->Chars());
+    }
+
+    stateMachine.ProcessString(L"\x1b[12;80H");
+    VERIFY_ARE_EQUAL(79, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"\x1b[1D");
+    VERIFY_ARE_EQUAL(78, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"X");
+    {
+        auto iter = tbi.GetCellDataAt({ 78, 11 });
+        VERIFY_ARE_EQUAL(L"X", iter->Chars());
+    }
+
+    stateMachine.ProcessString(L"\x1b[s");
+    stateMachine.ProcessString(L"\x1b[?69l");
+}
+
+void ScreenBufferTests::CursorLeftRightExactlyAtMargins()
+{
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& si = gci.GetActiveOutputBuffer();
+    auto& tbi = si.GetTextBuffer();
+    auto& stateMachine = si.GetStateMachine();
+    auto& cursor = si.GetTextBuffer().GetCursor();
+
+    VERIFY_IS_TRUE(si.GetViewport().BottomInclusive() > 24);
+
+    // Set some scrolling margins
+    stateMachine.ProcessString(L"\x1b[?69h");
+    stateMachine.ProcessString(L"\x1b[31;50s");
+
+    stateMachine.ProcessString(L"\x1b[12;50H");
+    VERIFY_ARE_EQUAL(49, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"\x1b[1C");
+    VERIFY_ARE_EQUAL(49, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"1");
+    {
+        auto iter = tbi.GetCellDataAt({ 49, 11 });
+        VERIFY_ARE_EQUAL(L"1", iter->Chars());
+    }
+
+    stateMachine.ProcessString(L"\x1b[1D");
+    VERIFY_ARE_EQUAL(48, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"2");
+    {
+        auto iter = tbi.GetCellDataAt({ 48, 11 });
+        VERIFY_ARE_EQUAL(L"2", iter->Chars());
+    }
+
+    stateMachine.ProcessString(L"\x1b[12;31H");
+    VERIFY_ARE_EQUAL(30, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"\x1b[1D");
+    VERIFY_ARE_EQUAL(30, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"3");
+    {
+        auto iter = tbi.GetCellDataAt({ 30, 11 });
+        VERIFY_ARE_EQUAL(L"3", iter->Chars());
+    }
+
+    stateMachine.ProcessString(L"\x1b[1C");
+    VERIFY_ARE_EQUAL(32, cursor.GetPosition().x);
+    stateMachine.ProcessString(L"4");
+    {
+        auto iter = tbi.GetCellDataAt({ 32, 11 });
+        VERIFY_ARE_EQUAL(L"4", iter->Chars());
+    }
+
+    stateMachine.ProcessString(L"\x1b[s");
+    stateMachine.ProcessString(L"\x1b[?69l");
+}
+
 void ScreenBufferTests::CursorNextPreviousLine()
 {
     auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
@@ -6245,26 +6605,34 @@ void ScreenBufferTests::CursorNextPreviousLine()
     // We should end up in column 0 of line 5.
     VERIFY_ARE_EQUAL(til::point(0, 5), cursor.GetPosition());
 
-    // Set the margins to 8:12 (9:13 in VT coordinates).
+    // Enable DECLRMM margin mode
+    stateMachine.ProcessString(L"\x1b[?69h");
+    // Set horizontal margins to 10:29 (11:30 in VT coordinates).
+    stateMachine.ProcessString(L"\x1b[11;30s");
+    // Set vertical margins to 8:12 (9:13 in VT coordinates).
     stateMachine.ProcessString(L"\x1b[9;13r");
     // 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"); });
+    auto clearMargins = wil::scope_exit([&] {
+        stateMachine.ProcessString(L"\x1b[r");
+        stateMachine.ProcessString(L"\x1b[s");
+        stateMachine.ProcessString(L"\x1b[?69l");
+    });
 
     Log::Comment(L"CNL inside margins");
     // Starting from column 20 of line 10.
     cursor.SetPosition({ 20, 10 });
     // Move down 5 lines (CNL).
     stateMachine.ProcessString(L"\x1b[5E");
-    // We should stop on line 12, the bottom margin.
-    VERIFY_ARE_EQUAL(til::point(0, 12), cursor.GetPosition());
+    // We should stop on column 10, line 12, the bottom left margins.
+    VERIFY_ARE_EQUAL(til::point(10, 12), cursor.GetPosition());
 
     Log::Comment(L"CPL inside margins");
     // Starting from column 20 of line 10.
     cursor.SetPosition({ 20, 10 });
     // Move up 5 lines (CPL).
     stateMachine.ProcessString(L"\x1b[5F");
-    // We should stop on line 8, the top margin.
-    VERIFY_ARE_EQUAL(til::point(0, 8), cursor.GetPosition());
+    // We should stop on column 10, line 8, the top left margins.
+    VERIFY_ARE_EQUAL(til::point(10, 8), cursor.GetPosition());
 
     Log::Comment(L"CNL below bottom");
     // Starting from column 20 of line 13 (1 below bottom margin).
@@ -6372,8 +6740,8 @@ void ScreenBufferTests::CursorSaveRestore()
 
     const auto selectAsciiChars = L"\x1b(B";
     const auto selectGraphicsChars = L"\x1b(0";
-    const auto saveCursor = L"\x1b[s";
-    const auto restoreCursor = L"\x1b[u";
+    const auto saveCursor = L"\x1b\x37";
+    const auto restoreCursor = L"\x1b\x38";
     const auto setDECOM = L"\x1b[?6h";
     const auto resetDECOM = L"\x1b[?6l";
 
@@ -6436,10 +6804,12 @@ void ScreenBufferTests::CursorSaveRestore()
 
     Log::Comment(L"Restore origin mode.");
     // Set margins and origin mode to relative.
+    stateMachine.ProcessString(L"\x1b[?69h");
     stateMachine.ProcessString(L"\x1b[10;20r");
+    stateMachine.ProcessString(L"\x1b[31;50s");
     stateMachine.ProcessString(setDECOM);
     // Verify home position inside margins.
-    VERIFY_ARE_EQUAL(til::point(0, 9), cursor.GetPosition());
+    VERIFY_ARE_EQUAL(til::point(30, 9), cursor.GetPosition());
     // Save state and reset origin mode to absolute.
     stateMachine.ProcessString(saveCursor);
     stateMachine.ProcessString(resetDECOM);
@@ -6449,37 +6819,43 @@ void ScreenBufferTests::CursorSaveRestore()
     stateMachine.ProcessString(restoreCursor);
     stateMachine.ProcessString(L"\x1b[H");
     // Verify home position inside margins, i.e. relative origin mode restored.
-    VERIFY_ARE_EQUAL(til::point(0, 9), cursor.GetPosition());
+    VERIFY_ARE_EQUAL(til::point(30, 9), cursor.GetPosition());
 
     Log::Comment(L"Restore relative to new origin.");
     // Reset margins, with absolute origin, and set cursor position.
     stateMachine.ProcessString(L"\x1b[r");
+    stateMachine.ProcessString(L"\x1b[s");
     stateMachine.ProcessString(setDECOM);
     cursor.SetPosition({ 5, 5 });
     // Save state.
     stateMachine.ProcessString(saveCursor);
     // Set margins and restore state.
     stateMachine.ProcessString(L"\x1b[15;25r");
+    stateMachine.ProcessString(L"\x1b[31;50s");
     stateMachine.ProcessString(restoreCursor);
-    // Verify Y position is now relative to new top margin
-    VERIFY_ARE_EQUAL(til::point(5, 19), cursor.GetPosition());
+    // Verify position is now relative to new top left margins
+    VERIFY_ARE_EQUAL(til::point(35, 19), cursor.GetPosition());
 
     Log::Comment(L"Clamp inside bottom margin.");
     // Reset margins, with absolute origin, and set cursor position.
     stateMachine.ProcessString(L"\x1b[r");
+    stateMachine.ProcessString(L"\x1b[s");
     stateMachine.ProcessString(setDECOM);
-    cursor.SetPosition({ 5, 15 });
+    cursor.SetPosition({ 15, 15 });
     // Save state.
     stateMachine.ProcessString(saveCursor);
     // Set margins and restore state.
     stateMachine.ProcessString(L"\x1b[1;10r");
+    stateMachine.ProcessString(L"\x1b[1;10s");
     stateMachine.ProcessString(restoreCursor);
-    // Verify Y position is clamped inside the top margin
-    VERIFY_ARE_EQUAL(til::point(5, 9), cursor.GetPosition());
+    // Verify position is clamped inside the bottom right margins
+    VERIFY_ARE_EQUAL(til::point(9, 9), cursor.GetPosition());
 
     // Reset origin mode and margins.
     stateMachine.ProcessString(resetDECOM);
     stateMachine.ProcessString(L"\x1b[r");
+    stateMachine.ProcessString(L"\x1b[s");
+    stateMachine.ProcessString(L"\x1b[?69l");
 }
 
 void ScreenBufferTests::ScreenAlignmentPattern()
@@ -7716,7 +8092,7 @@ void ScreenBufferTests::DelayedWrapReset()
 {
     BEGIN_TEST_METHOD_PROPERTIES()
         TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
-        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}")
+        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,35}")
     END_TEST_METHOD_PROPERTIES();
 
     int opIndex;
@@ -7729,7 +8105,6 @@ void ScreenBufferTests::DelayedWrapReset()
     const auto halfWidth = width / 2;
     WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
     WI_SetFlag(si.OutputMode, DISABLE_NEWLINE_AUTO_RETURN);
-    stateMachine.ProcessString(L"\033[?40h"); // Make sure DECCOLM is allowed
 
     const auto startRow = 5;
     const auto startCol = width - 1;
@@ -7748,13 +8123,14 @@ void ScreenBufferTests::DelayedWrapReset()
         bool absolutePos = false;
     } ops[] = {
         { L"DECSTBM", L"\033[1;10r", { 0, 0 }, true },
+        { L"DECSLRM", L"\033[1;10s", { 0, 0 }, true },
         { L"DECSWL", L"\033#5" },
         { L"DECDWL", L"\033#6", { halfWidth - 1, startRow }, true },
         { L"DECDHL (top)", L"\033#3", { halfWidth - 1, startRow }, true },
         { L"DECDHL (bottom)", L"\033#4", { halfWidth - 1, startRow }, true },
         { L"DECCOLM set", L"\033[?3h", { 0, 0 }, true },
         { L"DECOM set", L"\033[?6h", { 0, 0 }, true },
-        { L"DECCOLM set", L"\033[?3l", { 0, 0 }, true },
+        { L"DECCOLM reset", L"\033[?3l", { 0, 0 }, true },
         { L"DECOM reset", L"\033[?6l", { 0, 0 }, true },
         { L"DECAWM reset", L"\033[?7l" },
         { L"CUU", L"\033[A", { 0, -1 } },
@@ -7785,6 +8161,17 @@ void ScreenBufferTests::DelayedWrapReset()
     };
     const auto& op = gsl::at(ops, opIndex);
 
+    if (op.name == L"DECSLRM")
+    {
+        // Private mode 69 makes sure DECSLRM is allowed
+        stateMachine.ProcessString(L"\033[?69h");
+    }
+    if (op.name.starts_with(L"DECCOLM"))
+    {
+        // Private mode 40 makes sure DECCOLM is allowed
+        stateMachine.ProcessString(L"\033[?40h");
+    }
+
     Log::Comment(L"Writing a character at the end of the line should set delayed EOL wrap");
     const auto startPos = til::point{ startCol, startRow };
     si.GetTextBuffer().GetCursor().SetPosition(startPos);
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index bff3933cebe..f123d948428 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -431,6 +431,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         XTERM_EnableDECCOLMSupport = DECPrivateMode(40),
         DECNKM_NumericKeypadMode = DECPrivateMode(66),
         DECBKM_BackarrowKeyMode = DECPrivateMode(67),
+        DECLRMM_LeftRightMarginMode = DECPrivateMode(69),
         VT200_MOUSE_MODE = DECPrivateMode(1000),
         BUTTON_EVENT_MOUSE_MODE = DECPrivateMode(1002),
         ANY_EVENT_MOUSE_MODE = DECPrivateMode(1003),
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index 5d27e7bb246..24cce0620fa 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -54,6 +54,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual bool SetKeypadMode(const bool applicationMode) = 0; // DECKPAM, DECKPNM
     virtual bool SetAnsiMode(const bool ansiMode) = 0; // DECANM
     virtual bool SetTopBottomScrollingMargins(const VTInt topMargin, const VTInt bottomMargin) = 0; // DECSTBM
+    virtual bool SetLeftRightScrollingMargins(const VTInt leftMargin, const VTInt rightMargin) = 0; // DECSLRM
     virtual bool WarningBell() = 0; // BEL
     virtual bool CarriageReturn() = 0; // CR
     virtual bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) = 0; // IND, NEL, LF, FF, VT
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 884b2af1f32..64d88723ff2 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -77,12 +77,22 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
     const auto wrapAtEOL = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap);
     const auto attributes = textBuffer.GetCurrentAttributes();
 
+    const auto viewport = _api.GetViewport();
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(textBuffer.GetSize().Width());
+
+    auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y);
+    if (cursorPosition.x <= rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin)
+    {
+        lineWidth = std::min(lineWidth, rightMargin + 1);
+    }
+
     // Turn off the cursor until we're done, so it isn't refreshed unnecessarily.
     cursor.SetIsOn(false);
 
     RowWriteState state{
         .text = string,
-        .columnLimit = textBuffer.GetLineWidth(cursorPosition.y),
+        .columnLimit = lineWidth,
     };
 
     while (!state.text.empty())
@@ -98,7 +108,12 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
                 _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);
+                lineWidth = textBuffer.GetLineWidth(cursorPosition.y);
+                if (cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin)
+                {
+                    lineWidth = std::min(lineWidth, rightMargin + 1);
+                }
+                state.columnLimit = lineWidth;
             }
         }
 
@@ -278,6 +293,29 @@ std::pair<int, int> AdaptDispatch::_GetVerticalMargins(const til::rect& viewport
     return { topMargin, bottomMargin };
 }
 
+// Routine Description:
+// - Returns the coordinates of the horizontal scroll margins.
+// Arguments:
+// - bufferWidth - The width of the buffer
+// Return Value:
+// - A std::pair containing the left and right coordinates (inclusive).
+std::pair<int, int> AdaptDispatch::_GetHorizontalMargins(const til::CoordType bufferWidth) noexcept
+{
+    // If the left is out of range, reset the margins completely.
+    const auto rightmostColumn = bufferWidth - 1;
+    if (_scrollMargins.left >= rightmostColumn)
+    {
+        _scrollMargins.left = _scrollMargins.right = 0;
+    }
+    // If margins aren't set, use the full extent of the buffer.
+    const auto marginsSet = _scrollMargins.left < _scrollMargins.right;
+    auto leftMargin = marginsSet ? _scrollMargins.left : 0;
+    auto rightMargin = marginsSet ? _scrollMargins.right : rightmostColumn;
+    // If the right is out of range, clamp it to the rightmost column.
+    rightMargin = std::min(rightMargin, rightmostColumn);
+    return { leftMargin, rightMargin };
+}
+
 // Routine Description:
 // - Generalizes cursor movement to a specific position, which can be absolute or relative.
 // Arguments:
@@ -292,8 +330,10 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col
     const auto viewport = _api.GetViewport();
     auto& textBuffer = _api.GetTextBuffer();
     auto& cursor = textBuffer.GetCursor();
+    const auto bufferWidth = textBuffer.GetSize().Width();
     const auto cursorPosition = cursor.GetPosition();
     const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth);
 
     // For relative movement, the given offsets will be relative to
     // the current cursor position.
@@ -307,39 +347,54 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col
         row = _modes.test(Mode::Origin) ? topMargin : viewport.top;
     }
 
-    // And if the column is absolute, it'll be relative to column 0.
+    // And if the column is absolute, it'll be relative to column 0,
+    // or the left margin, depending on the origin mode.
     // Horizontal positions are not affected by the viewport.
     if (colOffset.IsAbsolute)
     {
-        col = 0;
+        col = _modes.test(Mode::Origin) ? leftMargin : 0;
     }
 
     // Adjust the base position by the given offsets and clamp the results.
     // The row is constrained within the viewport's vertical boundaries,
     // while the column is constrained by the buffer width.
     row = std::clamp(row + rowOffset.Value, viewport.top, viewport.bottom - 1);
-    col = std::clamp(col + colOffset.Value, 0, textBuffer.GetSize().Width() - 1);
+    col = std::clamp(col + colOffset.Value, 0, bufferWidth - 1);
 
     // If the operation needs to be clamped inside the margins, or the origin
     // mode is relative (which always requires margin clamping), then the row
-    // may need to be adjusted further.
+    // and column may need to be adjusted further.
     if (clampInMargins || _modes.test(Mode::Origin))
     {
-        // See microsoft/terminal#2929 - If the cursor is _below_ the top
-        // margin, it should stay below the top margin. If it's _above_ the
-        // bottom, it should stay above the bottom. Cursor movements that stay
-        // outside the margins shouldn't necessarily be affected. For example,
-        // moving up while below the bottom margin shouldn't just jump straight
-        // to the bottom margin. See
-        // ScreenBufferTests::CursorUpDownOutsideMargins for a test of that
-        // behavior.
-        if (cursorPosition.y >= topMargin)
+        // Vertical margins only apply if the original position is inside the
+        // horizontal margins. Also, the cursor will only be clamped inside the
+        // top margin if it was already below the top margin to start with, and
+        // it will only be clamped inside the bottom margin if it was already
+        // above the bottom margin to start with.
+        if (cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin)
         {
-            row = std::max(row, topMargin);
+            if (cursorPosition.y >= topMargin)
+            {
+                row = std::max(row, topMargin);
+            }
+            if (cursorPosition.y <= bottomMargin)
+            {
+                row = std::min(row, bottomMargin);
+            }
         }
-        if (cursorPosition.y <= bottomMargin)
+        // Similarly, horizontal margins only apply if the new row is inside the
+        // vertical margins. And the cursor is only clamped inside the horizontal
+        // margins if it was already inside to start with.
+        if (row >= topMargin && row <= bottomMargin)
         {
-            row = std::min(row, bottomMargin);
+            if (cursorPosition.x >= leftMargin)
+            {
+                col = std::max(col, leftMargin);
+            }
+            if (cursorPosition.x <= rightMargin)
+            {
+                col = std::min(col, rightMargin);
+            }
         }
     }
 
@@ -448,6 +503,7 @@ bool AdaptDispatch::CursorSaveState()
     // Although if origin mode is set, the cursor is relative to the margin origin.
     if (_modes.test(Mode::Origin))
     {
+        cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first;
         cursorPosition.y -= _GetVerticalMargins(viewport, false).first;
     }
 
@@ -520,14 +576,35 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec
     const auto absoluteDelta = std::min(std::abs(delta), scrollRect.height());
     if (absoluteDelta < scrollRect.height())
     {
-        // For now we're assuming the scrollRect is always the full width of the
-        // buffer, but this will likely need to be extended to support scrolling
-        // of arbitrary widths at some point in the future.
         const auto top = delta > 0 ? scrollRect.top : scrollRect.top + absoluteDelta;
+        const auto width = scrollRect.width();
         const auto height = scrollRect.height() - absoluteDelta;
         const auto actualDelta = delta > 0 ? absoluteDelta : -absoluteDelta;
-        textBuffer.ScrollRows(top, height, actualDelta);
-        textBuffer.TriggerRedraw(Viewport::FromExclusive(scrollRect));
+        if (width == textBuffer.GetSize().Width())
+        {
+            // If the scrollRect is the full width of the buffer, we can scroll
+            // more efficiently by rotating the row storage.
+            textBuffer.ScrollRows(top, height, actualDelta);
+            textBuffer.TriggerRedraw(Viewport::FromExclusive(scrollRect));
+        }
+        else
+        {
+            // Otherwise we have to move the content up or down by copying the
+            // requested buffer range one cell at a time.
+            const auto srcOrigin = til::point{ scrollRect.left, top };
+            const auto dstOrigin = til::point{ scrollRect.left, top + actualDelta };
+            const auto srcView = Viewport::FromDimensions(srcOrigin, width, height);
+            const auto dstView = Viewport::FromDimensions(dstOrigin, width, height);
+            const auto walkDirection = Viewport::DetermineWalkDirection(srcView, dstView);
+            auto srcPos = srcView.GetWalkOrigin(walkDirection);
+            auto dstPos = dstView.GetWalkOrigin(walkDirection);
+            do
+            {
+                const auto current = OutputCell(*textBuffer.GetCellDataAt(srcPos));
+                textBuffer.WriteLine(OutputCellIterator({ &current, 1 }), dstPos);
+                srcView.WalkInBounds(srcPos, walkDirection);
+            } while (dstView.WalkInBounds(dstPos, walkDirection));
+        }
     }
 
     // Rows revealed by the scroll are filled with standard erase attributes.
@@ -597,13 +674,21 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r
 // - <none>
 void AdaptDispatch::_InsertDeleteCharacterHelper(const VTInt delta)
 {
+    const auto viewport = _api.GetViewport();
     auto& textBuffer = _api.GetTextBuffer();
     const auto row = textBuffer.GetCursor().GetPosition().y;
-    const auto startCol = textBuffer.GetCursor().GetPosition().x;
-    const auto endCol = textBuffer.GetLineWidth(row);
-    _ScrollRectHorizontally(textBuffer, { startCol, row, endCol, row + 1 }, delta);
-    // The ICH and DCH controls are expected to reset the delayed wrap flag.
-    textBuffer.GetCursor().ResetDelayEOLWrap();
+    const auto col = textBuffer.GetCursor().GetPosition().x;
+    const auto lineWidth = textBuffer.GetLineWidth(row);
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+    const auto [leftMargin, rightMargin] = (row >= topMargin && row <= bottomMargin) ?
+                                               _GetHorizontalMargins(lineWidth) :
+                                               std::make_pair(0, lineWidth - 1);
+    if (col >= leftMargin && col <= rightMargin)
+    {
+        _ScrollRectHorizontally(textBuffer, { col, row, rightMargin + 1, row + 1 }, delta);
+        // The ICH and DCH controls are expected to reset the delayed wrap flag.
+        textBuffer.GetCursor().ResetDelayEOLWrap();
+    }
 }
 
 // Routine Description:
@@ -972,15 +1057,17 @@ til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, c
     // We start by calculating the margin offsets and maximum dimensions.
     // If the origin mode isn't set, we use the viewport extent.
     const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, false);
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferSize.width);
     const auto yOffset = _modes.test(Mode::Origin) ? topMargin : 0;
     const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : viewport.height();
-    const auto xMaximum = bufferSize.width;
+    const auto xOffset = _modes.test(Mode::Origin) ? leftMargin : 0;
+    const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : bufferSize.width;
 
     auto fillRect = til::inclusive_rect{};
-    fillRect.left = left;
+    fillRect.left = left + xOffset;
     fillRect.top = top + yOffset;
     // Right and bottom default to the maximum dimensions.
-    fillRect.right = (right ? right : xMaximum);
+    fillRect.right = (right ? right + xOffset : xMaximum);
     fillRect.bottom = (bottom ? bottom + yOffset : yMaximum);
 
     // We also clamp everything to the maximum dimensions, and subtract 1
@@ -1339,13 +1426,17 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p
 // - True.
 bool AdaptDispatch::SetLineRendition(const LineRendition rendition)
 {
-    auto& textBuffer = _api.GetTextBuffer();
-    textBuffer.SetCurrentLineRendition(rendition);
-    // There is some variation in how this was handled by the different DEC
-    // terminals, but the STD 070 reference (on page D-13) makes it clear that
-    // the delayed wrap (aka the Last Column Flag) was expected to be reset when
-    // line rendition controls were executed.
-    textBuffer.GetCursor().ResetDelayEOLWrap();
+    // The line rendition can't be changed if left/right margins are allowed.
+    if (!_modes.test(Mode::AllowDECSLRM))
+    {
+        auto& textBuffer = _api.GetTextBuffer();
+        textBuffer.SetCurrentLineRendition(rendition);
+        // There is some variation in how this was handled by the different DEC
+        // terminals, but the STD 070 reference (on page D-13) makes it clear that
+        // the delayed wrap (aka the Last Column Flag) was expected to be reset when
+        // line rendition controls were executed.
+        textBuffer.GetCursor().ResetDelayEOLWrap();
+    }
     return true;
 }
 
@@ -1526,11 +1617,11 @@ void AdaptDispatch::_CursorPositionReport(const bool extendedReport)
     cursorPosition.x++;
     cursorPosition.y++;
 
-    // If the origin mode is relative, line numbers start at top margin of the scrolling region.
+    // If the origin mode is set, the cursor is relative to the margin origin.
     if (_modes.test(Mode::Origin))
     {
-        const auto topMargin = _GetVerticalMargins(viewport, false).first;
-        cursorPosition.y -= topMargin;
+        cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first;
+        cursorPosition.y -= _GetVerticalMargins(viewport, false).first;
     }
 
     // Now send it back into the input channel of the console.
@@ -1590,7 +1681,8 @@ void AdaptDispatch::_ScrollMovement(const VTInt delta)
     auto& textBuffer = _api.GetTextBuffer();
     const auto bufferWidth = textBuffer.GetSize().Width();
     const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
-    _ScrollRectVertically(textBuffer, { 0, topMargin, bufferWidth, bottomMargin + 1 }, delta);
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth);
+    _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta);
 }
 
 // Routine Description:
@@ -1634,10 +1726,11 @@ void AdaptDispatch::_SetColumnMode(const bool enable)
         const auto viewportWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns);
         _api.ResizeWindow(viewportWidth, viewportHeight);
         _modes.set(Mode::Column, enable);
-        _modes.reset(Mode::Origin);
+        _modes.reset(Mode::Origin, Mode::AllowDECSLRM);
         CursorPosition(1, 1);
         EraseInDisplay(DispatchTypes::EraseType::All);
         _DoSetTopBottomScrollingMargins(0, 0);
+        _DoSetLeftRightScrollingMargins(0, 0);
     }
 }
 
@@ -1753,6 +1846,17 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
     case DispatchTypes::ModeParams::DECBKM_BackarrowKeyMode:
         _terminalInput.SetInputMode(TerminalInput::Mode::BackarrowKey, enable);
         return !_PassThroughInputModes();
+    case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode:
+        _modes.set(Mode::AllowDECSLRM, enable);
+        _DoSetLeftRightScrollingMargins(0, 0);
+        if (enable)
+        {
+            // If we've allowed left/right margins, we can't have line renditions.
+            const auto viewport = _api.GetViewport();
+            auto& textBuffer = _api.GetTextBuffer();
+            textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom);
+        }
+        return true;
     case DispatchTypes::ModeParams::VT200_MOUSE_MODE:
         _terminalInput.SetInputMode(TerminalInput::Mode::DefaultMouseTracking, enable);
         return !_PassThroughInputModes();
@@ -1881,6 +1985,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param)
     case DispatchTypes::ModeParams::DECBKM_BackarrowKeyMode:
         enabled = _terminalInput.GetInputMode(TerminalInput::Mode::BackarrowKey);
         break;
+    case DispatchTypes::ModeParams::DECLRMM_LeftRightMarginMode:
+        enabled = _modes.test(Mode::AllowDECSLRM);
+        break;
     case DispatchTypes::ModeParams::VT200_MOUSE_MODE:
         enabled = _terminalInput.GetInputMode(TerminalInput::Mode::DefaultMouseTracking);
         break;
@@ -1951,17 +2058,18 @@ void AdaptDispatch::_InsertDeleteLineHelper(const int32_t delta)
     const auto bufferWidth = textBuffer.GetSize().Width();
 
     auto& cursor = textBuffer.GetCursor();
+    const auto col = cursor.GetPosition().x;
     const auto row = cursor.GetPosition().y;
 
     const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
-    if (row >= topMargin && row <= bottomMargin)
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth);
+    if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin)
     {
         // We emulate inserting and deleting by scrolling the area between the cursor and the bottom margin.
-        _ScrollRectVertically(textBuffer, { 0, row, bufferWidth, bottomMargin + 1 }, delta);
+        _ScrollRectVertically(textBuffer, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta);
 
         // The IL and DL controls are also expected to move the cursor to the left margin.
-        // For now this is just column 0, since we don't yet support DECSLRM.
-        cursor.SetXPosition(0);
+        cursor.SetXPosition(leftMargin);
         _ApplyCursorMovementFlags(cursor);
     }
 }
@@ -2025,10 +2133,12 @@ bool AdaptDispatch::SetAnsiMode(const bool ansiMode)
 // Arguments:
 // - topMargin - the line number for the top margin.
 // - bottomMargin - the line number for the bottom margin.
+// - homeCursor - move the cursor to the home position.
 // Return Value:
 // - <none>
 void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin,
-                                                    const VTInt bottomMargin)
+                                                    const VTInt bottomMargin,
+                                                    const bool homeCursor)
 {
     // so notes time: (input -> state machine out -> adapter out -> conhost internal)
     // having only a top param is legal         ([3;r   -> 3,0   -> 3,h  -> 3,h,true)
@@ -2071,6 +2181,12 @@ void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin,
         }
         _scrollMargins.top = actualTop;
         _scrollMargins.bottom = actualBottom;
+        // If requested, we may also need to move the cursor to the home
+        // position, but only if the requested margins were valid.
+        if (homeCursor)
+        {
+            CursorPosition(1, 1);
+        }
     }
 }
 
@@ -2087,10 +2203,90 @@ void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin,
 bool AdaptDispatch::SetTopBottomScrollingMargins(const VTInt topMargin,
                                                  const VTInt bottomMargin)
 {
-    // When this is called, the cursor should also be moved to home.
-    // Other functions that only need to set/reset the margins should call _DoSetTopBottomScrollingMargins
-    _DoSetTopBottomScrollingMargins(topMargin, bottomMargin);
-    CursorPosition(1, 1);
+    _DoSetTopBottomScrollingMargins(topMargin, bottomMargin, true);
+    return true;
+}
+
+// Routine Description:
+// - DECSLRM - Set Scrolling Region
+// This control function sets the left and right margins for the current page.
+//  You cannot perform scrolling outside the margins.
+//  Default: Margins are at the page limits.
+// Arguments:
+// - leftMargin - the column number for the left margin.
+// - rightMargin - the column number for the right margin.
+// - homeCursor - move the cursor to the home position.
+// Return Value:
+// - <none>
+void AdaptDispatch::_DoSetLeftRightScrollingMargins(const VTInt leftMargin,
+                                                    const VTInt rightMargin,
+                                                    const bool homeCursor)
+{
+    til::CoordType actualLeft = leftMargin;
+    til::CoordType actualRight = rightMargin;
+
+    const auto& textBuffer = _api.GetTextBuffer();
+    const auto bufferWidth = textBuffer.GetSize().Width();
+    // The default left margin is column 1
+    if (actualLeft == 0)
+    {
+        actualLeft = 1;
+    }
+    // The default right margin is the buffer width
+    if (actualRight == 0)
+    {
+        actualRight = bufferWidth;
+    }
+    // The left margin must be less than the right margin, and the
+    // right margin must be less than or equal to the buffer width
+    if (actualLeft < actualRight && actualRight <= bufferWidth)
+    {
+        if (actualLeft == 1 && actualRight == bufferWidth)
+        {
+            // Client requests setting margins to the entire screen
+            //    - clear them instead of setting them.
+            actualLeft = 0;
+            actualRight = 0;
+        }
+        else
+        {
+            // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1.
+            actualLeft -= 1;
+            actualRight -= 1;
+        }
+        _scrollMargins.left = actualLeft;
+        _scrollMargins.right = actualRight;
+        // If requested, we may also need to move the cursor to the home
+        // position, but only if the requested margins were valid.
+        if (homeCursor)
+        {
+            CursorPosition(1, 1);
+        }
+    }
+}
+
+// Routine Description:
+// - DECSLRM - Set Scrolling Region
+// This control function sets the left and right margins for the current page.
+//  You cannot perform scrolling outside the margins.
+//  Default: Margins are at the page limits.
+// Arguments:
+// - leftMargin - the column number for the left margin.
+// - rightMargin - the column number for the right margin.
+// Return Value:
+// - True.
+bool AdaptDispatch::SetLeftRightScrollingMargins(const VTInt leftMargin,
+                                                 const VTInt rightMargin)
+{
+    if (_modes.test(Mode::AllowDECSLRM))
+    {
+        _DoSetLeftRightScrollingMargins(leftMargin, rightMargin, true);
+    }
+    else
+    {
+        // When DECSLRM isn't allowed, `CSI s` is interpreted as ANSISYSSC.
+        CursorSaveState();
+    }
     return true;
 }
 
@@ -2130,9 +2326,10 @@ bool AdaptDispatch::CarriageReturn()
 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();
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth);
 
     auto& cursor = textBuffer.GetCursor();
     const auto currentPosition = cursor.GetPosition();
@@ -2142,18 +2339,30 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c
     // When explicitly moving down a row, clear the wrap status.
     textBuffer.GetRowByOffset(currentPosition.y).SetWrapForced(wrapForced);
 
-    if (currentPosition.y != bottomMargin)
+    // If a carriage return was requested, we move to the leftmost column or
+    // the left margin, depending on whether we started within the margins.
+    if (withReturn)
     {
-        // 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.
+        const auto clampToMargin = currentPosition.y >= topMargin &&
+                                   currentPosition.y <= bottomMargin &&
+                                   currentPosition.x >= leftMargin;
+        newPosition.x = clampToMargin ? leftMargin : 0;
+    }
+
+    if (currentPosition.y != bottomMargin || newPosition.x < leftMargin || newPosition.x > rightMargin)
+    {
+        // If we're not at the bottom margin, or outside the horizontal margins,
+        // 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)
+    else if (topMargin > viewport.top || leftMargin > 0 || rightMargin < bufferWidth - 1)
     {
-        // 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);
+        // If the top margin isn't at the top of the viewport, or the
+        // horizontal margins are set, then we're just scrolling the margin
+        // area and the cursor stays where it is.
+        _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1);
     }
     else if (viewport.bottom < bufferHeight)
     {
@@ -2200,9 +2409,6 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c
         }
     }
 
-    // If a carriage return was requested, we also move to the leftmost column.
-    newPosition.x = withReturn ? 0 : newPosition.x;
-
     cursor.SetPosition(newPosition);
     _ApplyCursorMovementFlags(cursor);
 }
@@ -2246,14 +2452,15 @@ bool AdaptDispatch::ReverseLineFeed()
     auto& textBuffer = _api.GetTextBuffer();
     auto& cursor = textBuffer.GetCursor();
     const auto cursorPosition = cursor.GetPosition();
+    const auto bufferWidth = textBuffer.GetSize().Width();
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth);
     const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
 
     // If the cursor is at the top of the margin area, we shift the buffer
     // contents down, to emulate inserting a line at that point.
-    if (cursorPosition.y == topMargin)
+    if (cursorPosition.y == topMargin && cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin)
     {
-        const auto bufferWidth = textBuffer.GetSize().Width();
-        _ScrollRectVertically(textBuffer, { 0, topMargin, bufferWidth, bottomMargin + 1 }, 1);
+        _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1);
     }
     else if (cursorPosition.y > viewport.top)
     {
@@ -2307,12 +2514,19 @@ bool AdaptDispatch::ForwardTab(const VTInt numTabs)
 {
     auto& textBuffer = _api.GetTextBuffer();
     auto& cursor = textBuffer.GetCursor();
-    const auto width = textBuffer.GetLineWidth(cursor.GetPosition().y);
     auto column = cursor.GetPosition().x;
+    const auto row = cursor.GetPosition().y;
+    const auto width = textBuffer.GetLineWidth(row);
     auto tabsPerformed = 0;
 
+    const auto viewport = _api.GetViewport();
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width);
+    const auto clampToMargin = row >= topMargin && row <= bottomMargin && column <= rightMargin;
+    const auto maxColumn = clampToMargin ? rightMargin : width - 1;
+
     _InitTabStopsForWidth(width);
-    while (column + 1 < width && tabsPerformed < numTabs)
+    while (column < maxColumn && tabsPerformed < numTabs)
     {
         column++;
         if (til::at(_tabStopColumns, column))
@@ -2349,12 +2563,19 @@ bool AdaptDispatch::BackwardsTab(const VTInt numTabs)
 {
     auto& textBuffer = _api.GetTextBuffer();
     auto& cursor = textBuffer.GetCursor();
-    const auto width = textBuffer.GetLineWidth(cursor.GetPosition().y);
     auto column = cursor.GetPosition().x;
+    const auto row = cursor.GetPosition().y;
+    const auto width = textBuffer.GetLineWidth(row);
     auto tabsPerformed = 0;
 
+    const auto viewport = _api.GetViewport();
+    const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true);
+    const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width);
+    const auto clampToMargin = row >= topMargin && row <= bottomMargin && column >= leftMargin;
+    const auto minColumn = clampToMargin ? leftMargin : 0;
+
     _InitTabStopsForWidth(width);
-    while (column > 0 && tabsPerformed < numTabs)
+    while (column > minColumn && tabsPerformed < numTabs)
     {
         column--;
         if (til::at(_tabStopColumns, column))
@@ -2606,13 +2827,18 @@ bool AdaptDispatch::AcceptC1Controls(const bool enabled)
 bool AdaptDispatch::SoftReset()
 {
     _api.GetTextBuffer().GetCursor().SetIsVisible(true); // Cursor enabled.
-    _modes.reset(Mode::InsertReplace, Mode::Origin); // Replace mode; Absolute cursor addressing.
+
+    // Replace mode; Absolute cursor addressing; Disallow left/right margins.
+    _modes.reset(Mode::InsertReplace, Mode::Origin, Mode::AllowDECSLRM);
+
     _api.SetSystemMode(ITerminalApi::Mode::AutoWrap, true); // Wrap at end of line.
     _terminalInput.SetInputMode(TerminalInput::Mode::CursorKey, false); // Normal characters.
     _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, false); // Numeric characters.
 
     // Top margin = 1; bottom margin = page length.
     _DoSetTopBottomScrollingMargins(0, 0);
+    // Left margin = 1; right margin = page width.
+    _DoSetLeftRightScrollingMargins(0, 0);
 
     _termOutput = {}; // Reset all character set designations.
     if (_initialCodePage.has_value())
@@ -2744,10 +2970,11 @@ bool AdaptDispatch::ScreenAlignmentPattern()
     auto attr = textBuffer.GetCurrentAttributes();
     attr.SetStandardErase();
     _api.SetTextAttributes(attr);
-    // Reset the origin mode to absolute.
-    _modes.reset(Mode::Origin);
+    // Reset the origin mode to absolute, and disallow left/right margins.
+    _modes.reset(Mode::Origin, Mode::AllowDECSLRM);
     // Clear the scrolling margins.
     _DoSetTopBottomScrollingMargins(0, 0);
+    _DoSetLeftRightScrollingMargins(0, 0);
     // Set the cursor position to home.
     CursorPosition(1, 1);
 
@@ -3649,6 +3876,9 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting()
             case VTID("r"):
                 _ReportDECSTBMSetting();
                 break;
+            case VTID("s"):
+                _ReportDECSLRMSetting();
+                break;
             case VTID("\"q"):
                 _ReportDECSCASetting();
                 break;
@@ -3777,6 +4007,30 @@ void AdaptDispatch::_ReportDECSTBMSetting()
     _api.ReturnResponse({ response.data(), response.size() });
 }
 
+// Method Description:
+// - Reports the DECSLRM margin range in response to a DECRQSS query.
+// Arguments:
+// - None
+// Return Value:
+// - None
+void AdaptDispatch::_ReportDECSLRMSetting()
+{
+    using namespace std::string_view_literals;
+
+    // A valid response always starts with DCS 1 $ r.
+    fmt::basic_memory_buffer<wchar_t, 64> response;
+    response.append(L"\033P1$r"sv);
+
+    const auto bufferWidth = _api.GetTextBuffer().GetSize().Width();
+    const auto [marginLeft, marginRight] = _GetHorizontalMargins(bufferWidth);
+    // VT origin is at 1,1 so we need to add 1 to these margins.
+    fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginLeft + 1, marginRight + 1);
+
+    // The 's' indicates this is an DECSLRM response, and ST ends the sequence.
+    response.append(L"s\033\\"sv);
+    _api.ReturnResponse({ response.data(), response.size() });
+}
+
 // Method Description:
 // - Reports the DECSCA protected attribute in response to a DECRQSS query.
 // Arguments:
@@ -3925,9 +4179,10 @@ void AdaptDispatch::_ReportCursorInformation()
     cursorPosition.x++;
     cursorPosition.y++;
 
-    // If the origin mode is relative, line numbers start at top of the scrolling region.
+    // If the origin mode is set, the cursor is relative to the margin origin.
     if (_modes.test(Mode::Origin))
     {
+        cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first;
         cursorPosition.y -= _GetVerticalMargins(viewport, false).first;
     }
 
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index c577f8a3147..050ea68dcd0 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -90,6 +90,8 @@ namespace Microsoft::Console::VirtualTerminal
         bool SetAnsiMode(const bool ansiMode) override; // DECANM
         bool SetTopBottomScrollingMargins(const VTInt topMargin,
                                           const VTInt bottomMargin) override; // DECSTBM
+        bool SetLeftRightScrollingMargins(const VTInt leftMargin,
+                                          const VTInt rightMargin) override; // DECSLRM
         bool WarningBell() override; // BEL
         bool CarriageReturn() override; // CR
         bool LineFeed(const DispatchTypes::LineFeedType lineFeedType) override; // IND, NEL, LF, FF, VT
@@ -163,6 +165,7 @@ namespace Microsoft::Console::VirtualTerminal
             Origin,
             Column,
             AllowDECCOLM,
+            AllowDECSLRM,
             RectangularChangeExtent
         };
         enum class ScrollDirection
@@ -201,6 +204,7 @@ namespace Microsoft::Console::VirtualTerminal
 
         void _WriteToBuffer(const std::wstring_view string);
         std::pair<int, int> _GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept;
+        std::pair<int, int> _GetHorizontalMargins(const til::CoordType bufferWidth) 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);
@@ -217,7 +221,12 @@ namespace Microsoft::Console::VirtualTerminal
         void _ScrollMovement(const VTInt delta);
 
         void _DoSetTopBottomScrollingMargins(const VTInt topMargin,
-                                             const VTInt bottomMargin);
+                                             const VTInt bottomMargin,
+                                             const bool homeCursor = false);
+        void _DoSetLeftRightScrollingMargins(const VTInt leftMargin,
+                                             const VTInt rightMargin,
+                                             const bool homeCursor = false);
+
         void _DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced);
 
         void _OperatingStatus() const;
@@ -239,6 +248,7 @@ namespace Microsoft::Console::VirtualTerminal
 
         void _ReportSGRSetting() const;
         void _ReportDECSTBMSetting();
+        void _ReportDECSLRMSetting();
         void _ReportDECSCASetting() const;
         void _ReportDECSACESetting() const;
         void _ReportDECACSetting(const VTInt itemNumber) const;
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 9069c892b7b..b8f1fc033a5 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -47,6 +47,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool SetKeypadMode(const bool /*applicationMode*/) override { return false; } // DECKPAM, DECKPNM
     bool SetAnsiMode(const bool /*ansiMode*/) override { return false; } // DECANM
     bool SetTopBottomScrollingMargins(const VTInt /*topMargin*/, const VTInt /*bottomMargin*/) override { return false; } // DECSTBM
+    bool SetLeftRightScrollingMargins(const VTInt /*leftMargin*/, const VTInt /*rightMargin*/) override { return false; } // DECSLRM
     bool WarningBell() override { return false; } // BEL
     bool CarriageReturn() override { return false; } // CR
     bool LineFeed(const DispatchTypes::LineFeedType /*lineFeedType*/) override { return false; } // IND, NEL, LF, FF, VT
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index 6f719da6541..53d7e9e4bc6 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -1613,6 +1613,22 @@ class AdapterTest
         requestSetting(L"r");
         _testGetSet->ValidateInputEvent(L"\033P1$r1;25r\033\\");
 
+        Log::Comment(L"Requesting DECSLRM margins (5 to 10).");
+        _testGetSet->PrepData();
+        // We need to enable DECLRMM for horizontal margins to work.
+        _pDispatch->SetMode(DispatchTypes::DECLRMM_LeftRightMarginMode);
+        _pDispatch->SetLeftRightScrollingMargins(5, 10);
+        requestSetting(L"s");
+        _testGetSet->ValidateInputEvent(L"\033P1$r5;10s\033\\");
+
+        Log::Comment(L"Requesting DECSLRM margins (full width).");
+        _testGetSet->PrepData();
+        _pDispatch->SetLeftRightScrollingMargins(0, 0);
+        requestSetting(L"s");
+        _testGetSet->ValidateInputEvent(L"\033P1$r1;100s\033\\");
+        // Reset DECLRMM once we're done with horizontal margin testing.
+        _pDispatch->ResetMode(DispatchTypes::DECLRMM_LeftRightMarginMode);
+
         Log::Comment(L"Requesting SGR attributes (default).");
         _testGetSet->PrepData();
         TextAttribute attribute = {};
@@ -1779,7 +1795,7 @@ class AdapterTest
         // and DECRQM would not then be applicable.
 
         BEGIN_TEST_METHOD_PROPERTIES()
-            TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 8, 12, 25, 40, 66, 67, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 2004, 9001}")
+            TEST_METHOD_PROPERTY(L"Data:modeNumber", L"{1, 3, 5, 6, 7, 8, 12, 25, 40, 66, 67, 69, 1000, 1002, 1003, 1004, 1005, 1006, 1007, 1049, 2004, 9001}")
         END_TEST_METHOD_PROPERTIES()
 
         VTInt modeNumber;
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 56eb7b0f091..1dd61fd1f9f 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -491,10 +491,15 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         success = _dispatch->CursorPosition(parameters.at(0), parameters.at(1));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::CUP);
         break;
-    case CsiActionCodes::DECSTBM_SetScrollingRegion:
+    case CsiActionCodes::DECSTBM_SetTopBottomMargins:
         success = _dispatch->SetTopBottomScrollingMargins(parameters.at(0).value_or(0), parameters.at(1).value_or(0));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTBM);
         break;
+    case CsiActionCodes::DECSLRM_SetLeftRightMargins:
+        // Note that this can also be ANSISYSSC, depending on the state of DECLRMM.
+        success = _dispatch->SetLeftRightScrollingMargins(parameters.at(0).value_or(0), parameters.at(1).value_or(0));
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTBM);
+        break;
     case CsiActionCodes::ICH_InsertCharacter:
         success = _dispatch->InsertCharacter(parameters.at(0));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::ICH);
@@ -588,12 +593,8 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         success = _dispatch->ScrollDown(parameters.at(0));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::SD);
         break;
-    case CsiActionCodes::ANSISYSSC_CursorSave:
-        success = parameters.empty() && _dispatch->CursorSaveState();
-        TermTelemetry::Instance().Log(TermTelemetry::Codes::ANSISYSSC);
-        break;
     case CsiActionCodes::ANSISYSRC_CursorRestore:
-        success = parameters.empty() && _dispatch->CursorRestoreState();
+        success = _dispatch->CursorRestoreState();
         TermTelemetry::Instance().Log(TermTelemetry::Codes::ANSISYSRC);
         break;
     case CsiActionCodes::IL_InsertLine:
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index 468cf4e6700..97b1237637b 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -137,8 +137,8 @@ namespace Microsoft::Console::VirtualTerminal
             SGR_SetGraphicsRendition = VTID("m"),
             DSR_DeviceStatusReport = VTID("n"),
             DSR_PrivateDeviceStatusReport = VTID("?n"),
-            DECSTBM_SetScrollingRegion = VTID("r"),
-            ANSISYSSC_CursorSave = VTID("s"), // NOTE: Overlaps with DECLRMM/DECSLRM. Fix when/if implemented.
+            DECSTBM_SetTopBottomMargins = VTID("r"),
+            DECSLRM_SetLeftRightMargins = VTID("s"),
             DTTERM_WindowManipulation = VTID("t"), // NOTE: Overlaps with DECSLPP. Fix when/if implemented.
             ANSISYSRC_CursorRestore = VTID("u"),
             DECREQTPARM_RequestTerminalParameters = VTID("x"),
diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp
index f5dbc68ecd6..79e3c6f2da6 100644
--- a/src/terminal/parser/telemetry.cpp
+++ b/src/terminal/parser/telemetry.cpp
@@ -241,9 +241,9 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[DL], "DL"),
                                       TraceLoggingUInt32(_uiTimesUsed[SU], "SU"),
                                       TraceLoggingUInt32(_uiTimesUsed[SD], "SD"),
-                                      TraceLoggingUInt32(_uiTimesUsed[ANSISYSSC], "ANSISYSSC"),
                                       TraceLoggingUInt32(_uiTimesUsed[ANSISYSRC], "ANSISYSRC"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECSTBM], "DECSTBM"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECSLRM], "DECSLRM"),
                                       TraceLoggingUInt32(_uiTimesUsed[NEL], "NEL"),
                                       TraceLoggingUInt32(_uiTimesUsed[IND], "IND"),
                                       TraceLoggingUInt32(_uiTimesUsed[RI], "RI"),
diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp
index 79132ec8241..60ad7e34215 100644
--- a/src/terminal/parser/telemetry.hpp
+++ b/src/terminal/parser/telemetry.hpp
@@ -66,11 +66,11 @@ namespace Microsoft::Console::VirtualTerminal
             DCH,
             SU,
             SD,
-            ANSISYSSC,
             ANSISYSRC,
             IL,
             DL,
             DECSTBM,
+            DECSLRM,
             NEL,
             IND,
             RI,
diff --git a/src/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp
index 6fc04c62f72..db3a6f14588 100644
--- a/src/terminal/parser/ut_parser/OutputEngineTest.cpp
+++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp
@@ -1657,12 +1657,10 @@ class StateMachineExternalTest final
 
         pDispatch->ClearState();
 
-        mach.ProcessCharacter(AsciiChars::ESC);
-        mach.ProcessCharacter(L'[');
-        mach.ProcessCharacter(L's');
-        VERIFY_IS_TRUE(pDispatch->_cursorSave);
-
-        pDispatch->ClearState();
+        // Note that CSI s is dispatched as SetLeftRightScrollingMargins rather
+        // than CursorSaveState, so we don't test that here. The CursorSaveState
+        // will only be triggered by this sequence (in AdaptDispatch) when the
+        // Left-Right-Margin mode (DECLRMM) is disabled.
 
         mach.ProcessCharacter(AsciiChars::ESC);
         mach.ProcessCharacter(L'[');


Test patch: