Repository: microsoft/terminal (language: cpp)

Issue:
Cursor droppings are back in conhost
### Windows Terminal version

OpenConsole 8a34a0e59ac41bc805c308fb82bc7683e583d45d

### Windows build number

10.0.19041.1415

### Other Software

_No response_

### Steps to reproduce

1. You must be using a somewhat recent version of OpenConsole.
2. Open a WSL bash shell.
3. Execute the following: `printf "\e[2 q\e[9999C "; sleep 1`

The `CSI 2 q` is to get a non-blinking cursor which makes the issue easier to produce. The `CSI 9999C` moves the cursor to the end of the line, and the space initiates a delayed wrap. The sleep then gives the cursor a chance to be rendered before the next output will trigger the actual wrap.


### Expected Behavior

The output should wrap to the next line without leaving a visible cursor block behind.

### Actual Behavior

The cursor at the end of the line isn't cleared when the output wraps, so you end up with two cursors visible on the screen.

![image](https://user-images.githubusercontent.com/4181424/159455638-1195fc5b-bea1-4e0f-8820-75bfd8d10318.png)

This doesn't occur in my inbox version of conhost, so I think it might be a regression.


Discussion / hints:
DROP EVERYTHING, THEY'RE BACK. 

Thanks for noticing! Definitely can't let these back into the world 😝 
I think this was broken by the deferred cursor drawing introduced in PR #10394. I can fix it by removing the `StartDeferDrawing` and `EndDeferDrawing` here:

https://github.com/microsoft/terminal/blob/f936c4443d4b7b11da32077b0dc0a9aa8652bc77/src/host/outputStream.cpp#L45-L55

But then we lose the performance improvements we were getting from the PR. Hopefully there's a way to fix it without undoing that completely.

I've got a possible solution that doesn't require undoing the deferred drawing. We save the current cursor position in `StartDeferDrawing`, and then trigger a redraw for both the old and new positions in `EndDeferDrawing` (assuming the position has changed).

Hopefully the overhead of that additional maintenance doesn't outweigh the benefits we're getting from the deferred drawing, but I'm not the best person to evaluate the performance issues here. I'm also not positive this is going to work in all scenarios - need to play around with it some more.
Fixing #12917 doesn't magically fix this 😢 

Note: only repros when you're above the bottom of the viewport, so the view doesn't scroll
Might get fixed by snapshots

Fix patch:
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index a8331a881ba..c3b5b84f2d8 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -1078,83 +1078,6 @@ Viewport Terminal::_GetVisibleViewport() const noexcept
                                     size);
 }
 
-// Writes a string of text to the buffer, then moves the cursor (and viewport)
-//      in accordance with the written text.
-// This method is our proverbial `WriteCharsLegacy`, and great care should be made to
-//      keep it minimal and orderly, lest it become WriteCharsLegacy2ElectricBoogaloo
-// TODO: MSFT 21006766
-//       This needs to become stream logic on the buffer itself sooner rather than later
-//       because it's otherwise impossible to avoid the Electric Boogaloo-ness here.
-//       I had to make a bunch of hacks to get Japanese and emoji to work-ish.
-void Terminal::_WriteBuffer(const std::wstring_view& stringView)
-{
-    auto& cursor = _activeBuffer().GetCursor();
-
-    // Defer the cursor drawing while we are iterating the string, for a better performance.
-    // We can not waste time displaying a cursor event when we know more text is coming right behind it.
-    cursor.StartDeferDrawing();
-
-    for (size_t i = 0; i < stringView.size(); i++)
-    {
-        const auto wch = stringView.at(i);
-        const auto cursorPosBefore = cursor.GetPosition();
-        auto proposedCursorPosition = cursorPosBefore;
-
-        // TODO: MSFT 21006766
-        // This is not great but I need it demoable. Fix by making a buffer stream writer.
-        //
-        // If wch is a surrogate character we need to read 2 code units
-        // from the stringView to form a single code point.
-        const auto isSurrogate = wch >= 0xD800 && wch <= 0xDFFF;
-        const auto view = stringView.substr(i, isSurrogate ? 2 : 1);
-        const OutputCellIterator it{ view, _activeBuffer().GetCurrentAttributes() };
-        const auto end = _activeBuffer().Write(it);
-        const auto cellDistance = end.GetCellDistance(it);
-        const auto inputDistance = end.GetInputDistance(it);
-
-        if (inputDistance > 0)
-        {
-            // If "wch" was a surrogate character, we just consumed 2 code units above.
-            // -> Increment "i" by 1 in that case and thus by 2 in total in this iteration.
-            proposedCursorPosition.x += cellDistance;
-            i += gsl::narrow_cast<size_t>(inputDistance - 1);
-        }
-        else
-        {
-            // If _WriteBuffer() is called with a consecutive string longer than the viewport/buffer width
-            // the call to _buffer->Write() will refuse to write anything on the current line.
-            // GetInputDistance() thus returns 0, which would in turn cause i to be
-            // decremented by 1 below and force the outer loop to loop forever.
-            // This if() basically behaves as if "\r\n" had been encountered above and retries the write.
-            // With well behaving shells during normal operation this safeguard should normally not be encountered.
-            proposedCursorPosition.x = 0;
-            proposedCursorPosition.y++;
-
-            // Try the character again.
-            i--;
-
-            // If we write the last cell of the row here, TextBuffer::Write will
-            // mark this line as wrapped for us. If the next character we
-            // process is a newline, the Terminal::CursorLineFeed will unmark
-            // this line as wrapped.
-
-            // TODO: GH#780 - This should really be a _deferred_ newline. If
-            // the next character to come in is a newline or a cursor
-            // movement or anything, then we should _not_ wrap this line
-            // here.
-        }
-
-        _AdjustCursorPosition(proposedCursorPosition);
-    }
-
-    // Notify UIA of new text.
-    // It's important to do this here instead of in TextBuffer, because here you have access to the entire line of text,
-    // whereas TextBuffer writes it one character at a time via the OutputCellIterator.
-    _activeBuffer().TriggerNewTextNotification(stringView);
-
-    cursor.EndDeferDrawing();
-}
-
 void Terminal::_AdjustCursorPosition(const til::point proposedPosition)
 {
 #pragma warning(suppress : 26496) // cpp core checks wants this const but it's modified below.
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index c7a3b6b50a7..ec95bf8caf6 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -105,7 +105,6 @@ class Microsoft::Terminal::Core::Terminal final :
 
 #pragma region ITerminalApi
     // These methods are defined in TerminalApi.cpp
-    void PrintString(const std::wstring_view string) override;
     void ReturnResponse(const std::wstring_view response) override;
     Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() noexcept override;
     TextBuffer& GetTextBuffer() noexcept override;
@@ -117,7 +116,7 @@ class Microsoft::Terminal::Core::Terminal final :
     void SetScrollingRegion(const til::inclusive_rect& scrollMargins) noexcept override;
     void WarningBell() override;
     bool GetLineFeedMode() const noexcept override;
-    void LineFeed(const bool withReturn) override;
+    void LineFeed(const bool withReturn, const bool wrapForced) override;
     void SetWindowTitle(const std::wstring_view title) override;
     CursorType GetUserDefaultCursorStyle() const noexcept override;
     bool ResizeWindow(const til::CoordType width, const til::CoordType height) noexcept override;
@@ -421,8 +420,6 @@ class Microsoft::Terminal::Core::Terminal final :
     Microsoft::Console::Types::Viewport _GetMutableViewport() const noexcept;
     Microsoft::Console::Types::Viewport _GetVisibleViewport() const noexcept;
 
-    void _WriteBuffer(const std::wstring_view& stringView);
-
     void _AdjustCursorPosition(const til::point proposedPosition);
 
     void _NotifyScrollEvent() noexcept;
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index fa2d3c50d5e..c4ec65d62e5 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -20,12 +20,6 @@ TRACELOGGING_DEFINE_PROVIDER(g_hCTerminalCoreProvider,
                              (0x103ac8cf, 0x97d2, 0x51aa, 0xb3, 0xba, 0x5f, 0xfd, 0x55, 0x28, 0xfa, 0x5f),
                              TraceLoggingOptionMicrosoftTelemetry());
 
-// Print puts the text in the buffer and moves the cursor
-void Terminal::PrintString(const std::wstring_view string)
-{
-    _WriteBuffer(string);
-}
-
 void Terminal::ReturnResponse(const std::wstring_view response)
 {
     if (_pfnWriteInput)
@@ -92,13 +86,13 @@ bool Terminal::GetLineFeedMode() const noexcept
     return false;
 }
 
-void Terminal::LineFeed(const bool withReturn)
+void Terminal::LineFeed(const bool withReturn, const bool wrapForced)
 {
     auto cursorPos = _activeBuffer().GetCursor().GetPosition();
 
-    // since we explicitly just moved down a row, clear the wrap status on the
-    // row we just came from
-    _activeBuffer().GetRowByOffset(cursorPos.y).SetWrapForced(false);
+    // If the line was forced to wrap, set the wrap status.
+    // When explicitly moving down a row, clear the wrap status.
+    _activeBuffer().GetRowByOffset(cursorPos.y).SetWrapForced(wrapForced);
 
     cursorPos.y++;
     if (withReturn)
diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
index fee9dc57fa8..c95eb27b20a 100644
--- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
@@ -3207,7 +3207,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottom()
 
     // GH#5839 -
     // This test does expose a real bug when using WriteCharsLegacy to emit
-    // wrapped lines in conpty without WC_DELAY_EOL_WRAP. However, this fix has
+    // wrapped lines in conpty without delayed EOL wrap. However, this fix has
     // not yet been made, so for now, we need to just skip the cases that cause
     // this.
     if (writingMethod == PrintWithWriteCharsLegacy && paintEachNewline == PaintEveryLine)
diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp
index 10eca2c8ce4..683740c155b 100644
--- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp
@@ -97,7 +97,7 @@ void TerminalApiTest::PrintStringOfSurrogatePairs()
         [](LPVOID data) -> DWORD {
             const auto& baton = *reinterpret_cast<Baton*>(data);
             Log::Comment(L"Writing data.");
-            baton.pTerm->PrintString(baton.text);
+            baton.pTerm->_stateMachine->ProcessString(baton.text);
             Log::Comment(L"Setting event.");
             SetEvent(baton.done);
             return 0;
diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp
index c0ff3eae2ac..0d4af59ee36 100644
--- a/src/host/_stream.cpp
+++ b/src/host/_stream.cpp
@@ -350,12 +350,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
 
     auto lpString = pwchRealUnicode;
 
-    auto coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions();
-    // In VT mode, the width at which we wrap is determined by the line rendition attribute.
-    if (WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING))
-    {
-        coordScreenBufferSize.width = textBuffer.GetLineWidth(CursorPosition.y);
-    }
+    const auto coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions();
 
     static constexpr til::CoordType LOCAL_BUFFER_SIZE = 1024;
     WCHAR LocalBuffer[LOCAL_BUFFER_SIZE];
@@ -376,11 +371,6 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
                 Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
 
                 CursorPosition = cursor.GetPosition();
-                // In VT mode, we need to recalculate the width when moving to a new line.
-                if (WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING))
-                {
-                    coordScreenBufferSize.width = textBuffer.GetLineWidth(CursorPosition.y);
-                }
             }
         }
 
@@ -570,22 +560,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
             // WCL-NOTE: wrong place (typically inside another character).
             CursorPosition.x = XPosition;
 
-            // enforce a delayed newline if we're about to pass the end and the WC_DELAY_EOL_WRAP flag is set.
-            if (WI_IsFlagSet(dwFlags, WC_DELAY_EOL_WRAP) && CursorPosition.x >= coordScreenBufferSize.width && fWrapAtEOL)
-            {
-                // Our cursor position as of this time is going to remain on the last position in this column.
-                CursorPosition.x = coordScreenBufferSize.width - 1;
-
-                // Update in the structures that we're still pointing to the last character in the row
-                cursor.SetPosition(CursorPosition);
-
-                // Record for the delay comparison that we're delaying on the last character in the row
-                cursor.DelayEOLWrap(CursorPosition);
-            }
-            else
-            {
-                Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
-            }
+            Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
 
             // WCL-NOTE: If we have processed the entire input string during our "fast one-line print" handler,
             // WCL-NOTE: we are done as there is nothing more to do. Neat!
diff --git a/src/host/cmdline.h b/src/host/cmdline.h
index b503ec21b15..32f3ee4e7b4 100644
--- a/src/host/cmdline.h
+++ b/src/host/cmdline.h
@@ -142,7 +142,7 @@ void RedrawCommandLine(COOKED_READ_DATA& cookedReadData);
 #define WC_LIMIT_BACKSPACE 0x10
 //#define WC_NONDESTRUCTIVE_TAB    0x20  -  This is not needed anymore, because the VT code handles tabs internally now.
 //#define WC_NEWLINE_SAVE_X        0x40  -  This has been replaced with an output mode flag instead as it's line discipline behavior that may not necessarily be coupled with VT.
-#define WC_DELAY_EOL_WRAP 0x80
+//#define WC_DELAY_EOL_WRAP        0x80  -  This is not needed anymore, because the AdaptDispatch class handles all VT output.
 
 // Word delimiters
 bool IsWordDelim(const wchar_t wch);
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index 62ade57b79b..9f382cab2af 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -24,39 +24,6 @@ ConhostInternalGetSet::ConhostInternalGetSet(_In_ IIoProvider& io) :
 {
 }
 
-// Routine Description:
-// - Handles the print action from the state machine
-// Arguments:
-// - string - The string to be printed.
-// Return Value:
-// - <none>
-void ConhostInternalGetSet::PrintString(const std::wstring_view string)
-{
-    auto dwNumBytes = string.size() * sizeof(wchar_t);
-
-    auto& cursor = _io.GetActiveOutputBuffer().GetTextBuffer().GetCursor();
-    if (!cursor.IsOn())
-    {
-        cursor.SetIsOn(true);
-    }
-
-    // Defer the cursor drawing while we are iterating the string, for a better performance.
-    // We can not waste time displaying a cursor event when we know more text is coming right behind it.
-    cursor.StartDeferDrawing();
-    const auto ntstatus = WriteCharsLegacy(_io.GetActiveOutputBuffer(),
-                                           string.data(),
-                                           string.data(),
-                                           string.data(),
-                                           &dwNumBytes,
-                                           nullptr,
-                                           _io.GetActiveOutputBuffer().GetTextBuffer().GetCursor().GetPosition().x,
-                                           WC_LIMIT_BACKSPACE | WC_DELAY_EOL_WRAP,
-                                           nullptr);
-    cursor.EndDeferDrawing();
-
-    THROW_IF_NTSTATUS_FAILED(ntstatus);
-}
-
 // - Sends a string response to the input stream of the console.
 // - Used by various commands where the program attached would like a reply to one of the commands issued.
 // - This will generate two "key presses" (one down, one up) for every character in the string and place them into the head of the console's input stream.
@@ -207,20 +174,18 @@ bool ConhostInternalGetSet::GetLineFeedMode() const
 // - Performs a line feed, possibly preceded by carriage return.
 // Arguments:
 // - withReturn - Set to true if a carriage return should be performed as well.
+// - wrapForced - Set to true is the line feed was the result of the line wrapping.
 // Return Value:
 // - <none>
-void ConhostInternalGetSet::LineFeed(const bool withReturn)
+void ConhostInternalGetSet::LineFeed(const bool withReturn, const bool wrapForced)
 {
     auto& screenInfo = _io.GetActiveOutputBuffer();
     auto& textBuffer = screenInfo.GetTextBuffer();
     auto cursorPosition = textBuffer.GetCursor().GetPosition();
 
-    // We turn the cursor on before an operation that might scroll the viewport, otherwise
-    // that can result in an old copy of the cursor being left behind on the screen.
-    textBuffer.GetCursor().SetIsOn(true);
-
-    // Since we are explicitly moving down a row, clear the wrap status on the row we're leaving
-    textBuffer.GetRowByOffset(cursorPosition.y).SetWrapForced(false);
+    // If the line was forced to wrap, set the wrap status.
+    // When explicitly moving down a row, clear the wrap status.
+    textBuffer.GetRowByOffset(cursorPosition.y).SetWrapForced(wrapForced);
 
     cursorPosition.y += 1;
     if (withReturn)
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index bc033cd39d6..f673e9110ba 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -29,7 +29,6 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
 public:
     ConhostInternalGetSet(_In_ Microsoft::Console::IIoProvider& io);
 
-    void PrintString(const std::wstring_view string) override;
     void ReturnResponse(const std::wstring_view response) override;
 
     Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() override;
@@ -47,7 +46,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
     void WarningBell() override;
 
     bool GetLineFeedMode() const override;
-    void LineFeed(const bool withReturn) override;
+    void LineFeed(const bool withReturn, const bool wrapForced) override;
 
     void SetWindowTitle(const std::wstring_view title) override;
 
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index cd064edf80c..4705f382817 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -37,7 +37,6 @@ namespace Microsoft::Console::VirtualTerminal
         ITerminalApi& operator=(const ITerminalApi&) = delete;
         ITerminalApi& operator=(ITerminalApi&&) = delete;
 
-        virtual void PrintString(const std::wstring_view string) = 0;
         virtual void ReturnResponse(const std::wstring_view response) = 0;
 
         virtual StateMachine& GetStateMachine() = 0;
@@ -55,7 +54,7 @@ namespace Microsoft::Console::VirtualTerminal
         virtual void SetScrollingRegion(const til::inclusive_rect& scrollMargins) = 0;
         virtual void WarningBell() = 0;
         virtual bool GetLineFeedMode() const = 0;
-        virtual void LineFeed(const bool withReturn) = 0;
+        virtual void LineFeed(const bool withReturn, const bool wrapForced) = 0;
         virtual void SetWindowTitle(const std::wstring_view title) = 0;
         virtual void UseAlternateScreenBuffer() = 0;
         virtual void UseMainScreenBuffer() = 0;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 4c9d60df4aa..c0efb57598e 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -40,7 +40,7 @@ void AdaptDispatch::Print(const wchar_t wchPrintable)
     // a character is only output if the DEL is translated to something else.
     if (wchTranslated != AsciiChars::DEL)
     {
-        _api.PrintString({ &wchTranslated, 1 });
+        _WriteToBuffer({ &wchTranslated, 1 });
     }
 }
 
@@ -61,14 +61,98 @@ void AdaptDispatch::PrintString(const std::wstring_view string)
         {
             buffer.push_back(_termOutput.TranslateKey(wch));
         }
-        _api.PrintString(buffer);
+        _WriteToBuffer(buffer);
     }
     else
     {
-        _api.PrintString(string);
+        _WriteToBuffer(string);
     }
 }
 
+void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
+{
+    auto& textBuffer = _api.GetTextBuffer();
+    auto& cursor = textBuffer.GetCursor();
+    auto cursorPosition = cursor.GetPosition();
+    const auto wrapAtEOL = _api.GetAutoWrapMode();
+    const auto attributes = textBuffer.GetCurrentAttributes();
+
+    // Turn off the cursor until we're done, so it isn't refreshed unnecessarily.
+    cursor.SetIsOn(false);
+
+    // The width at which we wrap is determined by the line rendition attribute.
+    auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y);
+
+    auto stringPosition = string.cbegin();
+    while (stringPosition < string.cend())
+    {
+        if (cursor.IsDelayedEOLWrap() && wrapAtEOL)
+        {
+            const auto delayedCursorPosition = cursor.GetDelayedAtPosition();
+            cursor.ResetDelayEOLWrap();
+            // Only act on a delayed EOL if we didn't move the cursor to a
+            // different position from where the EOL was marked.
+            if (delayedCursorPosition == cursorPosition)
+            {
+                _api.LineFeed(true, true);
+                cursorPosition = cursor.GetPosition();
+                // We need to recalculate the width when moving to a new line.
+                lineWidth = textBuffer.GetLineWidth(cursorPosition.y);
+            }
+        }
+
+        const OutputCellIterator it(std::wstring_view{ stringPosition, string.cend() }, attributes);
+        const auto itEnd = textBuffer.WriteLine(it, cursorPosition, wrapAtEOL, lineWidth - 1);
+
+        if (itEnd.GetInputDistance(it) == 0)
+        {
+            // If we haven't written anything out because there wasn't enough space,
+            // we move the cursor to the end of the line so that it's forced to wrap.
+            cursorPosition.x = lineWidth;
+            // But if wrapping is disabled, we also need to move to the next string
+            // position, otherwise we'll be stuck in this loop forever.
+            if (!wrapAtEOL)
+            {
+                stringPosition++;
+            }
+        }
+        else
+        {
+            const auto cellCount = itEnd.GetCellDistance(it);
+            const auto changedRect = til::rect{ cursorPosition, til::size{ cellCount, 1 } };
+            _api.NotifyAccessibilityChange(changedRect);
+
+            stringPosition += itEnd.GetInputDistance(it);
+            cursorPosition.x += cellCount;
+        }
+
+        if (cursorPosition.x >= lineWidth)
+        {
+            // If we're past the end of the line, we need to clamp the cursor
+            // back into range, and if wrapping is enabled, set the delayed wrap
+            // flag. The wrapping only occurs once another character is output.
+            cursorPosition.x = lineWidth - 1;
+            cursor.SetPosition(cursorPosition);
+            if (wrapAtEOL)
+            {
+                cursor.DelayEOLWrap(cursorPosition);
+            }
+        }
+        else
+        {
+            cursor.SetPosition(cursorPosition);
+        }
+    }
+
+    _ApplyCursorMovementFlags(cursor);
+
+    // Notify UIA of new text.
+    // It's important to do this here instead of in TextBuffer, because here you
+    // have access to the entire line of text, whereas TextBuffer writes it one
+    // character at a time via the OutputCellIterator.
+    textBuffer.TriggerNewTextNotification(string);
+}
+
 // Routine Description:
 // - CUU - Handles cursor upward movement by given distance.
 // CUU and CUD are handled separately from other CUP sequences, because they are
@@ -1882,13 +1966,13 @@ bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType)
     switch (lineFeedType)
     {
     case DispatchTypes::LineFeedType::DependsOnMode:
-        _api.LineFeed(_api.GetLineFeedMode());
+        _api.LineFeed(_api.GetLineFeedMode(), false);
         return true;
     case DispatchTypes::LineFeedType::WithoutReturn:
-        _api.LineFeed(false);
+        _api.LineFeed(false, false);
         return true;
     case DispatchTypes::LineFeedType::WithReturn:
-        _api.LineFeed(true);
+        _api.LineFeed(true, false);
         return true;
     default:
         return false;
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index bc5a0f04c25..76dbfc05f5b 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -193,6 +193,7 @@ namespace Microsoft::Console::VirtualTerminal
             std::optional<TextColor> background;
         };
 
+        void _WriteToBuffer(const std::wstring_view string);
         std::pair<int, int> _GetVerticalMargins(const til::rect& viewport, const bool absolute);
         bool _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins);
         void _ApplyCursorMovementFlags(Cursor& cursor) noexcept;
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index f4e81adf72a..45cd8715666 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -60,11 +60,6 @@ using namespace Microsoft::Console::VirtualTerminal;
 class TestGetSet final : public ITerminalApi
 {
 public:
-    void PrintString(const std::wstring_view string) override
-    {
-        _printed += string;
-    }
-
     void ReturnResponse(const std::wstring_view response) override
     {
         Log::Comment(L"ReturnResponse MOCK called...");
@@ -148,7 +143,7 @@ class TestGetSet final : public ITerminalApi
         return _getLineFeedModeResult;
     }
 
-    void LineFeed(const bool withReturn) override
+    void LineFeed(const bool withReturn, const bool /*wrapForced*/) override
     {
         Log::Comment(L"LineFeed MOCK called...");
 
@@ -316,8 +311,6 @@ class TestGetSet final : public ITerminalApi
 
         _response.clear();
         _retainResponse = false;
-
-        _printed.clear();
     }
 
     void PrepCursor(CursorX xact, CursorY yact)
@@ -409,8 +402,6 @@ class TestGetSet final : public ITerminalApi
     til::inclusive_rect _expectedScrollRegion;
     til::inclusive_rect _activeScrollRegion;
 
-    std::wstring _printed;
-
     til::point _expectedCursorPos;
 
     TextAttribute _expectedAttribute = {};
@@ -2534,44 +2525,50 @@ class AdapterTest
         setMacroText(2, L"Macro 2");
         setMacroText(63, L"Macro 63");
 
+        const auto getBufferOutput = [&]() {
+            const auto& textBuffer = _testGetSet->GetTextBuffer();
+            const auto cursorPos = textBuffer.GetCursor().GetPosition();
+            return textBuffer.GetRowByOffset(cursorPos.y).GetText().substr(0, cursorPos.x);
+        };
+
         Log::Comment(L"Simple macro invoke");
-        _testGetSet->_printed.clear();
+        _testGetSet->PrepData();
         _stateMachine->ProcessString(L"\033[2*z");
-        VERIFY_ARE_EQUAL(L"Macro 2", _testGetSet->_printed);
+        VERIFY_ARE_EQUAL(L"Macro 2", getBufferOutput());
 
         Log::Comment(L"Default macro invoke");
-        _testGetSet->_printed.clear();
+        _testGetSet->PrepData();
         _stateMachine->ProcessString(L"\033[*z");
-        VERIFY_ARE_EQUAL(L"Macro 0", _testGetSet->_printed);
+        VERIFY_ARE_EQUAL(L"Macro 0", getBufferOutput());
 
         Log::Comment(L"Maximum ID number");
-        _testGetSet->_printed.clear();
+        _testGetSet->PrepData();
         _stateMachine->ProcessString(L"\033[63*z");
-        VERIFY_ARE_EQUAL(L"Macro 63", _testGetSet->_printed);
+        VERIFY_ARE_EQUAL(L"Macro 63", getBufferOutput());
 
         Log::Comment(L"Out of range ID number");
-        _testGetSet->_printed.clear();
+        _testGetSet->PrepData();
         _stateMachine->ProcessString(L"\033[64*z");
-        VERIFY_ARE_EQUAL(L"", _testGetSet->_printed);
+        VERIFY_ARE_EQUAL(L"", getBufferOutput());
 
         Log::Comment(L"Only one ID parameter allowed");
-        _testGetSet->_printed.clear();
+        _testGetSet->PrepData();
         _stateMachine->ProcessString(L"\033[2;0;1*z");
-        VERIFY_ARE_EQUAL(L"Macro 2", _testGetSet->_printed);
+        VERIFY_ARE_EQUAL(L"Macro 2", getBufferOutput());
 
         Log::Comment(L"DECDMAC ignored when inside a macro");
         setMacroText(10, L"[\033P1;0;0!zReplace Macro 1\033\\]");
-        _testGetSet->_printed.clear();
+        _testGetSet->PrepData();
         _stateMachine->ProcessString(L"\033[10*z");
         _stateMachine->ProcessString(L"\033[1*z");
-        VERIFY_ARE_EQUAL(L"[]Macro 1", _testGetSet->_printed);
+        VERIFY_ARE_EQUAL(L"[]Macro 1", getBufferOutput());
 
         Log::Comment(L"Maximum recursive depth is 16");
         setMacroText(0, L"<\033[1*z>");
         setMacroText(1, L"[\033[0*z]");
-        _testGetSet->_printed.clear();
+        _testGetSet->PrepData();
         _stateMachine->ProcessString(L"\033[0*z");
-        VERIFY_ARE_EQUAL(L"<[<[<[<[<[<[<[<[]>]>]>]>]>]>]>]>", _testGetSet->_printed);
+        VERIFY_ARE_EQUAL(L"<[<[<[<[<[<[<[<[]>]>]>]>]>]>]>]>", getBufferOutput());
 
         _pDispatch->_macroBuffer = nullptr;
     }


Test patch: