Repository: microsoft/terminal (language: cpp)

Issue:
WriteCharsLegacy: Accumulated bugs
Inserting a tab character that would put us at the right edge of the buffer inserts two tabs (one to get us to the right edge of the buffer, one more after we wrap)

<details>
<summary>Code and example</summary>

```c++
#include <windows.h>
int main() {
        CONSOLE_SCREEN_BUFFER_INFOEX csbiex{};
        csbiex.cbSize = sizeof(csbiex);
        GetConsoleScreenBufferInfoEx(GetStdHandle(STD_OUTPUT_HANDLE), &csbiex);
        SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), COORD{(SHORT)(csbiex.dwSize.X - 9), csbiex.dwCursorPosition.Y});
        WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), L"|\t|\n", 4, nullptr, nullptr);
        return 0;
}
```

![image](https://user-images.githubusercontent.com/189190/127206696-16a6dd41-37f7-43d7-892a-621856b57b57.png)
</details>

Inserting a printable control character at the right edge of the screen (ala `^A`) when only the `^` would fit emits a `^` and a space.

<details>
<summary>Example</summary>

![image](https://user-images.githubusercontent.com/189190/127206882-e0d6bd1b-b866-4e85-abf6-60f6ea0bf81f.png)
(here, `^D`)

</details>

Inserting a printable control character (`^D`) at the right edge of the screen when both `^` and `D` fit and then backspacing it deletes the `^`, not the `D`:

<details>
<summary>Example</summary>

![image](https://user-images.githubusercontent.com/189190/127206980-661c5abe-d1ce-4e46-b134-7a8191506507.png)

</details>


Discussion / hints:
Tab bug: the measurer "eats" the space before it decides that the Tab does not fit. The bulk text inserter then uses the _estimated_ position (which was consumed by the tab we aborted inserting) to move the cursor. Then when WCL loops back around to insert the tab on the next line it measures back out at 8 characters and inserts it again.
Control char bug 1: WCL does not consider a printable control char to take up 2 spaces when it is trying to fit it up against the edge of the screen. Correct behavior: treat `^D` as two characters and push the whole sequence down to the next line, or print `^` and `D` on separate lines and be sure to delete them both.

Fix patch:
diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp
index 2d6b3e7b8d5..93be3886c7f 100644
--- a/src/buffer/out/Row.cpp
+++ b/src/buffer/out/Row.cpp
@@ -117,6 +117,12 @@ LineRendition ROW::GetLineRendition() const noexcept
     return _lineRendition;
 }
 
+uint16_t ROW::GetLineWidth() const noexcept
+{
+    const auto scale = _lineRendition != LineRendition::SingleWidth ? 1 : 0;
+    return _columnCount >> scale;
+}
+
 // Routine Description:
 // - Sets all properties of the ROW to default values
 // Arguments:
@@ -882,6 +888,17 @@ std::wstring_view ROW::GetText() const noexcept
     return { _chars.data(), _charSize() };
 }
 
+std::wstring_view ROW::GetText(til::CoordType columnBegin, til::CoordType columnEnd) const noexcept
+{
+    const til::CoordType columns = _columnCount;
+    const auto colBeg = std::max(0, std::min(columns, columnBegin));
+    const auto colEnd = std::max(colBeg, std::min(columns, columnEnd));
+    const size_t chBeg = _uncheckedCharOffset(gsl::narrow_cast<size_t>(colBeg));
+    const size_t chEnd = _uncheckedCharOffset(gsl::narrow_cast<size_t>(colEnd));
+#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
+    return { _chars.data() + chBeg, chEnd - chBeg };
+}
+
 DelimiterClass ROW::DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept
 {
     const auto col = _clampedColumn(column);
diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp
index 1ce38c0668e..3733a11ffc7 100644
--- a/src/buffer/out/Row.hpp
+++ b/src/buffer/out/Row.hpp
@@ -123,6 +123,7 @@ class ROW final
     bool WasDoubleBytePadded() const noexcept;
     void SetLineRendition(const LineRendition lineRendition) noexcept;
     LineRendition GetLineRendition() const noexcept;
+    uint16_t GetLineWidth() const noexcept;
 
     void Reset(const TextAttribute& attr) noexcept;
     void TransferAttributes(const til::small_rle<TextAttribute, uint16_t, 1>& attr, til::CoordType newWidth);
@@ -151,6 +152,7 @@ class ROW final
     std::wstring_view GlyphAt(til::CoordType column) const noexcept;
     DbcsAttribute DbcsAttrAt(til::CoordType column) const noexcept;
     std::wstring_view GetText() const noexcept;
+    std::wstring_view GetText(til::CoordType columnBegin, til::CoordType columnEnd) const noexcept;
     DelimiterClass DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept;
 
     auto AttrBegin() const noexcept { return _attr.begin(); }
diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp
index fc737e1b26d..f41a1442721 100644
--- a/src/buffer/out/textBuffer.cpp
+++ b/src/buffer/out/textBuffer.cpp
@@ -450,18 +450,11 @@ void TextBuffer::ConsumeGrapheme(std::wstring_view& chars) noexcept
 
 // This function is intended for writing regular "lines" of text as it'll set the wrap flag on the given row.
 // You can continue calling the function on the same row as long as state.columnEnd < state.columnLimit.
-void TextBuffer::WriteLine(til::CoordType row, bool wrapAtEOL, const TextAttribute& attributes, RowWriteState& state)
+void TextBuffer::Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state)
 {
     auto& r = GetRowByOffset(row);
-
     r.ReplaceText(state);
     r.ReplaceAttributes(state.columnBegin, state.columnEnd, attributes);
-
-    if (state.columnEnd >= state.columnLimit)
-    {
-        r.SetWrapForced(wrapAtEOL);
-    }
-
     TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, state.columnEndDirty, row + 1 }));
 }
 
@@ -946,7 +939,7 @@ const Cursor& TextBuffer::GetCursor() const noexcept
     return _cursor;
 }
 
-[[nodiscard]] TextAttribute TextBuffer::GetCurrentAttributes() const noexcept
+const TextAttribute& TextBuffer::GetCurrentAttributes() const noexcept
 {
     return _currentAttributes;
 }
@@ -956,6 +949,11 @@ void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) no
     _currentAttributes = currentAttributes;
 }
 
+void TextBuffer::SetWrapForced(const til::CoordType y, bool wrap)
+{
+    GetRowByOffset(y).SetWrapForced(wrap);
+}
+
 void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes)
 {
     const auto cursorPosition = GetCursor().GetPosition();
diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp
index b98f8d231cd..5efeaaa741d 100644
--- a/src/buffer/out/textBuffer.hpp
+++ b/src/buffer/out/textBuffer.hpp
@@ -98,7 +98,7 @@ class TextBuffer final
 
     // Text insertion functions
     static void ConsumeGrapheme(std::wstring_view& chars) noexcept;
-    void WriteLine(til::CoordType row, bool wrapAtEOL, const TextAttribute& attributes, RowWriteState& state);
+    void Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state);
     void FillRect(const til::rect& rect, const std::wstring_view& fill, const TextAttribute& attributes);
 
     OutputCellIterator Write(const OutputCellIterator givenIt);
@@ -133,10 +133,11 @@ class TextBuffer final
 
     til::CoordType TotalRowCount() const noexcept;
 
-    [[nodiscard]] TextAttribute GetCurrentAttributes() const noexcept;
+    const TextAttribute& GetCurrentAttributes() const noexcept;
 
     void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept;
 
+    void SetWrapForced(til::CoordType y, bool wrap);
     void SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes);
     void ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow);
     LineRendition GetLineRendition(const til::CoordType row) const;
diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
index cd9ac092898..9f267746511 100644
--- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
+++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp
@@ -21,7 +21,7 @@
 #include "../host/readDataCooked.hpp"
 #include "../host/output.h"
 #include "../host/_stream.h" // For WriteCharsLegacy
-#include "../host/cmdline.h" // For WC_LIMIT_BACKSPACE
+#include "../host/cmdline.h" // For WC_INTERACTIVE
 #include "test/CommonState.hpp"
 
 #include "../cascadia/TerminalCore/Terminal.hpp"
@@ -3422,7 +3422,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS()
         }
         else if (writingMethod == PrintWithWriteCharsLegacy)
         {
-            doWriteCharsLegacy(si, str, WC_LIMIT_BACKSPACE);
+            doWriteCharsLegacy(si, str, WC_INTERACTIVE);
         }
     };
 
diff --git a/src/host/CommandNumberPopup.cpp b/src/host/CommandNumberPopup.cpp
index bf77a39b37f..4ccce4d6d75 100644
--- a/src/host/CommandNumberPopup.cpp
+++ b/src/host/CommandNumberPopup.cpp
@@ -42,7 +42,7 @@ void CommandNumberPopup::_handleNumber(COOKED_READ_DATA& cookedReadData, const w
                                                       &CharsToWrite,
                                                       &NumSpaces,
                                                       cookedReadData.OriginalCursorPosition().x,
-                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                      WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                       nullptr));
         cookedReadData.ScreenInfo().SetAttributes(realAttributes);
         try
@@ -73,7 +73,7 @@ void CommandNumberPopup::_handleBackspace(COOKED_READ_DATA& cookedReadData) noex
                                                       &CharsToWrite,
                                                       &NumSpaces,
                                                       cookedReadData.OriginalCursorPosition().x,
-                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                      WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                       nullptr));
         cookedReadData.ScreenInfo().SetAttributes(realAttributes);
         _pop();
diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp
index 5bd3b675e44..d85d4e105f8 100644
--- a/src/host/_stream.cpp
+++ b/src/host/_stream.cpp
@@ -98,6 +98,46 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC
     LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible));
 }
 
+// As the name implies, this writes text without processing its control characters.
+static size_t _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const DWORD dwFlags, til::CoordType* const psScrollY, const std::wstring_view& text)
+{
+    const auto keepCursorVisible = WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE);
+    const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
+    const auto hasAccessibilityEventing = screenInfo.HasAccessibilityEventing();
+    auto& textBuffer = screenInfo.GetTextBuffer();
+    size_t numSpaces = 0;
+
+    RowWriteState state{
+        .text = text,
+        .columnLimit = textBuffer.GetSize().RightExclusive(),
+    };
+
+    while (!state.text.empty())
+    {
+        auto cursorPosition = textBuffer.GetCursor().GetPosition();
+
+        state.columnBegin = cursorPosition.x;
+        textBuffer.Write(cursorPosition.y, textBuffer.GetCurrentAttributes(), state);
+        cursorPosition.x = state.columnEnd;
+
+        numSpaces += gsl::narrow_cast<size_t>(state.columnEnd - state.columnBegin);
+
+        if (wrapAtEOL && state.columnEnd >= state.columnLimit)
+        {
+            textBuffer.SetWrapForced(cursorPosition.y, true);
+        }
+
+        if (hasAccessibilityEventing && state.columnEnd > state.columnBegin)
+        {
+            screenInfo.NotifyAccessibilityEventing(state.columnBegin, cursorPosition.y, state.columnEnd - 1, cursorPosition.y);
+        }
+
+        AdjustCursorPosition(screenInfo, cursorPosition, keepCursorVisible, psScrollY);
+    }
+
+    return numSpaces;
+}
+
 // Routine Description:
 // - This routine writes a string to the screen, processing any embedded
 //   unicode characters.  The string is also copied to the input buffer, if
@@ -111,9 +151,8 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC
 // - pcb - On input, number of bytes to write.  On output, number of bytes written.
 // - pcSpaces - On output, the number of spaces consumed by the written characters.
 // - dwFlags -
-//      WC_DESTRUCTIVE_BACKSPACE   backspace overwrites characters.
+//      WC_INTERACTIVE             backspace overwrites characters, control characters are expanded (as in, to "^X")
 //      WC_KEEP_CURSOR_VISIBLE     change window origin desirable when hit rt. edge
-//      WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X")
 // Return Value:
 // Note:
 // - This routine does not process tabs and backspace properly.  That code will be implemented as part of the line editing services.
@@ -126,565 +165,294 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC
                                         const til::CoordType sOriginalXPosition,
                                         const DWORD dwFlags,
                                         _Inout_opt_ til::CoordType* const psScrollY)
+try
 {
-    const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    static constexpr wchar_t tabSpaces[8]{ L' ', L' ', L' ', L' ', L' ', L' ', L' ', L' ' };
+
     auto& textBuffer = screenInfo.GetTextBuffer();
     auto& cursor = textBuffer.GetCursor();
-    auto CursorPosition = cursor.GetPosition();
-    auto Status = STATUS_SUCCESS;
-    til::CoordType XPosition;
-    size_t TempNumSpaces = 0;
-    const auto fUnprocessed = WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT);
-    const auto fWrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
-
-    // Must not adjust cursor here. It has to stay on for many write scenarios. Consumers should call for the
-    // cursor to be turned off if they want that.
-
-    const auto Attributes = screenInfo.GetAttributes();
-    const auto BufferSize = *pcb;
-    *pcb = 0;
-
-    auto lpString = pwchRealUnicode;
-
-    const auto coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions();
-
-    static constexpr til::CoordType LOCAL_BUFFER_SIZE = 1024;
-    WCHAR LocalBuffer[LOCAL_BUFFER_SIZE];
-
-    while (*pcb < BufferSize)
+    const auto keepCursorVisible = WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE);
+    const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
+    auto it = pwchRealUnicode;
+    const auto end = it + *pcb / sizeof(wchar_t);
+    size_t numSpaces = 0;
+
+    // In VT mode, when you have a 120-column terminal you can write 120 columns without the cursor wrapping.
+    // Whenever the cursor is in that 120th column IsDelayedEOLWrap() will return true. I'm not sure why the VT parts
+    // of the code base store this as a boolean. It's also unclear why we handle this here. The intention is likely
+    // so that when we exit VT mode and receive a write a potentially stored delayed wrap would still be handled.
+    // The way this code does it however isn't correct since it handles it like the old console APIs would and
+    // so writing a newline while being delay wrapped will print 2 newlines.
+    if (cursor.IsDelayedEOLWrap() && wrapAtEOL)
     {
-        // correct for delayed EOL
-        if (cursor.IsDelayedEOLWrap() && fWrapAtEOL)
+        auto pos = cursor.GetPosition();
+        const auto delayed = cursor.GetDelayedAtPosition();
+        cursor.ResetDelayEOLWrap();
+        if (delayed == pos)
         {
-            const auto coordDelayedAt = 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 (coordDelayedAt.x == CursorPosition.x && coordDelayedAt.y == CursorPosition.y)
-            {
-                CursorPosition.x = 0;
-                CursorPosition.y++;
+            pos.x = 0;
+            pos.y++;
+            AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
+        }
+    }
 
-                AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
+    // If ENABLE_PROCESSED_OUTPUT is set we search for C0 control characters and handle them like backspace, tab, etc.
+    // If it's not set, we can just straight up give everything to _writeCharsLegacyUnprocessed.
+    if (WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))
+    {
+        numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { it, end });
+        it = end;
+    }
 
-                CursorPosition = cursor.GetPosition();
-            }
+    while (it != end)
+    {
+        const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return !IS_GLYPH_CHAR(wch); });
+        if (nextControlChar != it)
+        {
+            numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { it, nextControlChar });
+            it = nextControlChar;
         }
 
-        // As an optimization, collect characters in buffer and print out all at once.
-        XPosition = cursor.GetPosition().x;
-        til::CoordType i = 0;
-        auto LocalBufPtr = LocalBuffer;
-        while (*pcb < BufferSize && i < LOCAL_BUFFER_SIZE && XPosition < coordScreenBufferSize.width)
+        for (; it != end && !IS_GLYPH_CHAR(*it); ++it)
         {
-#pragma prefast(suppress : 26019, "Buffer is taken in multiples of 2. Validation is ok.")
-            const auto Char = *lpString;
-            // WCL-NOTE: We believe RealUnicodeChar to be identical to Char, because we believe pwchRealUnicode
-            // WCL-NOTE: to be identical to lpString. They are incremented in lockstep, never separately, and lpString
-            // WCL-NOTE: is initialized from pwchRealUnicode.
-            const auto RealUnicodeChar = *pwchRealUnicode;
-            if (IS_GLYPH_CHAR(RealUnicodeChar) || fUnprocessed)
+            switch (*it)
             {
-                // WCL-NOTE: This operates on a single code unit instead of a whole codepoint. It will mis-measure surrogate pairs.
-                if (IsGlyphFullWidth(Char))
+            case UNICODE_NULL:
+                if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE))
                 {
-                    if (i < (LOCAL_BUFFER_SIZE - 1) && XPosition < (coordScreenBufferSize.width - 1))
-                    {
-                        *LocalBufPtr++ = Char;
-
-                        // cursor adjusted by 2 because the char is double width
-                        XPosition += 2;
-                        i += 1;
-                        pwchBuffer++;
-                    }
-                    else
-                    {
-                        goto EndWhile;
-                    }
+                    break;
                 }
-                else
+                numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &tabSpaces[0], 1 });
+                continue;
+            case UNICODE_BELL:
+                if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE))
                 {
-                    *LocalBufPtr = Char;
-                    LocalBufPtr++;
-                    XPosition++;
-                    i++;
-                    pwchBuffer++;
+                    break;
                 }
-            }
-            else
+                std::ignore = screenInfo.SendNotifyBeep();
+                continue;
+            case UNICODE_BACKSPACE:
             {
-                FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
-                switch (RealUnicodeChar)
-                {
-                case UNICODE_BELL:
-                    if (dwFlags & WC_PRINTABLE_CONTROL_CHARS)
-                    {
-                        goto CtrlChar;
-                    }
-                    else
-                    {
-                        screenInfo.SendNotifyBeep();
-                    }
-                    break;
-                case UNICODE_BACKSPACE:
-
-                    // automatically go to EndWhile.  this is because
-                    // backspace is not destructive, so "aBkSp" prints
-                    // a with the cursor on the "a". we could achieve
-                    // this behavior staying in this loop and figuring out
-                    // the string that needs to be printed, but it would
-                    // be expensive and it's the exceptional case.
+                auto pos = cursor.GetPosition();
 
-                    goto EndWhile;
-                    break;
-                case UNICODE_TAB:
+                if (WI_IsFlagClear(dwFlags, WC_INTERACTIVE))
                 {
-                    const auto TabSize = NUMBER_OF_SPACES_IN_TAB(XPosition);
-                    XPosition = XPosition + TabSize;
-                    if (XPosition >= coordScreenBufferSize.width)
-                    {
-                        goto EndWhile;
-                    }
+                    pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(pos.x);
+                    AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
+                    continue;
+                }
 
-                    for (til::CoordType j = 0; j < TabSize && i < LOCAL_BUFFER_SIZE; j++, i++)
-                    {
-                        *LocalBufPtr = UNICODE_SPACE;
-                        LocalBufPtr++;
-                    }
+                const auto moveUp = [&]() {
+                    pos.x = -1;
+                    AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
 
-                    pwchBuffer++;
-                    break;
-                }
-                case UNICODE_LINEFEED:
-                case UNICODE_CARRIAGERETURN:
-                    goto EndWhile;
-                default:
+                    const auto y = cursor.GetPosition().y;
+                    auto& row = textBuffer.GetRowByOffset(y);
 
-                    // if char is ctrl char, write ^char.
-                    if ((dwFlags & WC_PRINTABLE_CONTROL_CHARS) && (IS_CONTROL_CHAR(RealUnicodeChar)))
+                    pos.x = textBuffer.GetSize().RightExclusive();
+                    pos.y = y;
+
+                    if (row.WasDoubleBytePadded())
                     {
-                    CtrlChar:
-                        if (i < (LOCAL_BUFFER_SIZE - 1))
-                        {
-                            // WCL-NOTE: We do not properly measure that there is space for two characters
-                            // WCL-NOTE: left on the screen.
-                            *LocalBufPtr = (WCHAR)'^';
-                            LocalBufPtr++;
-                            XPosition++;
-                            i++;
-
-                            *LocalBufPtr = (WCHAR)(RealUnicodeChar + (WCHAR)'@');
-                            LocalBufPtr++;
-                            XPosition++;
-                            i++;
-
-                            pwchBuffer++;
-                        }
-                        else
-                        {
-                            goto EndWhile;
-                        }
+                        pos.x--;
+                        numSpaces--;
                     }
-                    else
-                    {
-                        if (Char == UNICODE_NULL)
-                        {
-                            *LocalBufPtr = UNICODE_SPACE;
-                        }
-                        else
-                        {
-                            // As a special favor to incompetent apps that attempt to display control chars,
-                            // convert to corresponding OEM Glyph Chars
-                            WORD CharType;
 
-                            GetStringTypeW(CT_CTYPE1, &RealUnicodeChar, 1, &CharType);
-                            if (WI_IsFlagSet(CharType, C1_CNTRL))
-                            {
-                                ConvertOutputToUnicode(gci.OutputCP,
-                                                       (LPSTR)&RealUnicodeChar,
-                                                       1,
-                                                       LocalBufPtr,
-                                                       1);
-                            }
-                            else
-                            {
-                                // WCL-NOTE: We should never hit this.
-                                // WCL-NOTE: 1. Normal characters are handled via the early check for IS_GLYPH_CHAR
-                                // WCL-NOTE: 2. Control characters are handled via the CtrlChar label (if WC_PRINTABLE_CONTROL_CHARS is on)
-                                // WCL-NOTE:    And if they are control characters they will trigger the C1_CNTRL check above.
-                                *LocalBufPtr = Char;
-                            }
-                        }
+                    row.SetWrapForced(false);
+                    row.SetDoubleBytePadded(false);
+                };
 
-                        LocalBufPtr++;
-                        XPosition++;
-                        i++;
-                        pwchBuffer++;
-                    }
+                // We have to move up early because the tab handling code below needs to be on
+                // the row of the tab already, so that we can call GetText() for precedingText.
+                if (pos.x == 0 && pos.y != 0)
+                {
+                    moveUp();
                 }
-            }
-            lpString++;
-            pwchRealUnicode++;
-            *pcb += sizeof(WCHAR);
-        }
-    EndWhile:
-        if (i != 0)
-        {
-            CursorPosition = cursor.GetPosition();
 
-            // Make sure we don't write past the end of the buffer.
-            // WCL-NOTE: This check uses a code unit count instead of a column count. That is incorrect.
-            if (i > coordScreenBufferSize.width - CursorPosition.x)
-            {
-                i = coordScreenBufferSize.width - CursorPosition.x;
-            }
-
-            // line was wrapped if we're writing up to the end of the current row
-            OutputCellIterator it(std::wstring_view(LocalBuffer, i), Attributes);
-            const auto itEnd = screenInfo.Write(it);
+                til::CoordType glyphCount = 1;
 
-            // Notify accessibility
-            if (screenInfo.HasAccessibilityEventing())
-            {
-                screenInfo.NotifyAccessibilityEventing(CursorPosition.x, CursorPosition.y, CursorPosition.x + i - 1, CursorPosition.y);
-            }
-
-            // The number of "spaces" or "cells" we have consumed needs to be reported and stored for later
-            // when/if we need to erase the command line.
-            TempNumSpaces += itEnd.GetCellDistance(it);
-            // WCL-NOTE: We are using the "estimated" X position delta instead of the actual delta from
-            // WCL-NOTE: the iterator. It is not clear why. If they differ, the cursor ends up in the
-            // WCL-NOTE: wrong place (typically inside another character).
-            CursorPosition.x = XPosition;
+                if (pwchBuffer != pwchBufferBackupLimit)
+                {
+                    const auto lastChar = pwchBuffer[-1];
 
-            AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY);
+                    // Deleting tabs is a bit tricky, because they have a variable width between 1 and 8 spaces,
+                    // are stored as whitespace but are technically distinct from whitespace.
+                    if (lastChar == UNICODE_TAB)
+                    {
+                        const auto precedingText = textBuffer.GetRowByOffset(pos.y).GetText(pos.x - 8, pos.x);
 
-            // 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!
-            if (*pcb == BufferSize)
-            {
-                if (nullptr != pcSpaces)
-                {
-                    *pcSpaces = TempNumSpaces;
-                }
-                return STATUS_SUCCESS;
-            }
-            continue;
-        }
-        else if (*pcb >= BufferSize)
-        {
-            // WCL-NOTE: This case looks like it is never encountered, but it *is* if WC_PRINTABLE_CONTROL_CHARS is off.
-            // WCL-NOTE: If the string is entirely nonprinting control characters, there will be
-            // WCL-NOTE: no output in the buffer (LocalBuffer; i == 0) but we will have processed
-            // WCL-NOTE: "every" character. We can just bail out and report the number of spaces consumed.
-            FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
-
-            // this catches the case where the number of backspaces == the number of characters.
-            if (nullptr != pcSpaces)
-            {
-                *pcSpaces = TempNumSpaces;
-            }
-            return STATUS_SUCCESS;
-        }
+                        // First, we measure the amount of spaces that precede the cursor in the text buffer,
+                        // which is generally the amount of spaces that we end up deleting. We do it this way,
+                        // because we don't know what kind of complex mix of wide/narrow glyphs precede the tab.
+                        // Basically, by asking the text buffer we get the size information of the preceding text.
+                        if (precedingText.size() >= 2 && precedingText.back() == L' ')
+                        {
+                            auto textIt = precedingText.rbegin() + 1;
+                            const auto textEnd = precedingText.rend();
 
-        switch (*lpString)
-        {
-        case UNICODE_BACKSPACE:
-        {
-            // move cursor backwards one space. overwrite current char with blank.
-            // we get here because we have to backspace from the beginning of the line
-            TempNumSpaces -= 1;
-            if (pwchBuffer == pwchBufferBackupLimit)
-            {
-                CursorPosition.x -= 1;
-            }
-            else
-            {
-                const wchar_t* Tmp;
-                wchar_t* Tmp2 = nullptr;
-                WCHAR LastChar;
+                            for (; textIt != textEnd && *textIt == L' '; ++textIt)
+                            {
+                                glyphCount++;
+                            }
+                        }
 
-                const auto bufferSize = pwchBuffer - pwchBufferBackupLimit;
-                std::unique_ptr<wchar_t[]> buffer;
-                try
-                {
-                    buffer = std::make_unique<wchar_t[]>(bufferSize);
-                    std::fill_n(buffer.get(), bufferSize, UNICODE_NULL);
-                }
-                catch (...)
-                {
-                    return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
-                }
+                        // But there's a problem: When you print "  \t" it should delete 6 spaces and not 8.
+                        // In other words, we shouldn't delete any actual preceding whitespaces. We can ask
+                        // the "backup" buffer (= preceding text in the commandline) for this information.
+                        //
+                        // backupEnd points to the character immediately preceding the tab (LastChar).
+                        const auto backupEnd = pwchBuffer - 1;
+                        // backupLimit points to how far back we need to search. Even if we have 9000 characters in our command line,
+                        // we'll only need to check a total of 8 whitespaces. "pwchBuffer - pwchBufferBackupLimit" will
+                        // always be at least 1 because that's the \t character in the backup buffer. In other words,
+                        // backupLimit will at a minimum be equal to backupEnd, or precede it by 7 more characters.
+                        const auto backupLimit = pwchBuffer - std::min<ptrdiff_t>(8, pwchBuffer - pwchBufferBackupLimit);
+                        // Now count how many spaces precede the \t character. "backupEnd - backupBeg" will be the amount.
+                        auto backupBeg = backupEnd;
+                        for (; backupBeg != backupLimit && backupBeg[-1] == L' '; --backupBeg, --glyphCount)
+                        {
+                        }
 
-                for (i = 0, Tmp2 = buffer.get(), Tmp = pwchBufferBackupLimit;
-                     i < bufferSize;
-                     i++, Tmp++)
-                {
-                    // see 18120085, these two need to be separate if statements
-                    if (*Tmp == UNICODE_BACKSPACE)
-                    {
-                        //it is important we do nothing in the else case for
-                        //      this one instead of falling through to the below else.
-                        if (Tmp2 > buffer.get())
+                        // There's one final problem: A prompt like...
+                        //   fputs("foo: ", stdout);
+                        //   fgets(buffer, stdin);
+                        // ...has a trailing whitespace in front of our pwchBufferBackupLimit which we should not backspace over.
+                        // sOriginalXPosition stores the start of the prompt at the pwchBufferBackupLimit.
+                        if (backupBeg == pwchBufferBackupLimit)
                         {
-                            Tmp2--;
+                            glyphCount = pos.x - sOriginalXPosition;
                         }
+
+                        // Now that we finally know how many columns precede the cursor we can
+                        // subtract the previously determined amount of ' ' from the '\t'.
+                        glyphCount -= gsl::narrow_cast<til::CoordType>(backupEnd - backupBeg);
+
+                        // Can the above code leave glyphCount <= 0? Let's just not find out!
+                        glyphCount = std::max(1, glyphCount);
                     }
-                    else
+                    // Control chars in interactive mode were previously written out
+                    // as ^X for instance, so now we also need to delete 2 glyphs.
+                    else if (IS_CONTROL_CHAR(lastChar))
                     {
-                        FAIL_FAST_IF(!(Tmp2 >= buffer.get()));
-                        *Tmp2++ = *Tmp;
+                        glyphCount = 2;
                     }
                 }
-                if (Tmp2 == buffer.get())
-                {
-                    LastChar = UNICODE_SPACE;
-                }
-                else
-                {
-#pragma prefast(suppress : 26001, "This is fine. Tmp2 has to have advanced or it would equal pBuffer.")
-                    LastChar = *(Tmp2 - 1);
-                }
 
-                if (LastChar == UNICODE_TAB)
+                for (;;)
                 {
-                    CursorPosition.x -= RetrieveNumberOfSpaces(sOriginalXPosition,
-                                                               pwchBufferBackupLimit,
-                                                               pwchBuffer - pwchBufferBackupLimit - 1);
-                    if (CursorPosition.x < 0)
+                    // We've already moved up if the cursor was in the first column so
+                    // we need to start off with overwriting the text with whitespace.
+                    // It wouldn't make sense to check the cursor position again already.
                     {
-                        CursorPosition.x = (coordScreenBufferSize.width - 1) / TAB_SIZE;
-                        CursorPosition.x *= TAB_SIZE;
-                        CursorPosition.x += 1;
-                        CursorPosition.y -= 1;
-
-                        // since you just backspaced yourself back up into the previous row, unset the wrap
-                        // flag on the prev row if it was set
-                        textBuffer.GetRowByOffset(CursorPosition.y).SetWrapForced(false);
+                        const auto previousColumn = pos.x;
+                        pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(previousColumn);
+
+                        RowWriteState state{
+                            .text = { &tabSpaces[0], 8 },
+                            .columnBegin = pos.x,
+                            .columnLimit = previousColumn,
+                        };
+                        textBuffer.Write(pos.y, textBuffer.GetCurrentAttributes(), state);
+                        numSpaces -= previousColumn - pos.x;
                     }
-                }
-                else if (IS_CONTROL_CHAR(LastChar))
-                {
-                    CursorPosition.x -= 1;
-                    TempNumSpaces -= 1;
 
-                    // overwrite second character of ^x sequence.
-                    if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
+                    // The cursor movement logic is a little different for the last iteration, so we exit early here.
+                    glyphCount--;
+                    if (glyphCount <= 0)
                     {
-                        try
-                        {
-                            screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition);
-                            Status = STATUS_SUCCESS;
-                        }
-                        CATCH_LOG();
+                        break;
                     }
 
-                    CursorPosition.x -= 1;
-                }
-                else if (IsGlyphFullWidth(LastChar))
-                {
-                    CursorPosition.x -= 1;
-                    TempNumSpaces -= 1;
-
-                    AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
-                    if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
+                    // Otherwise, in case we need to delete 2 or more glyphs, we need to ensure we properly wrap lines back up.
+                    if (pos.x == 0 && pos.y != 0)
                     {
-                        try
-                        {
-                            screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition);
-                            Status = STATUS_SUCCESS;
-                        }
-                        CATCH_LOG();
+                        moveUp();
                     }
-                    CursorPosition.x -= 1;
+                }
+
+                // After the last iteration the cursor might now be in the first column after a line
+                // that was previously padded with a whitespace in the last column due to a wide glyph.
+                // Now that the wide glyph is presumably gone, we can move up a line.
+                if (pos.x == 0 && pos.y != 0 && textBuffer.GetRowByOffset(pos.y - 1).WasDoubleBytePadded())
+                {
+                    moveUp();
                 }
                 else
                 {
-                    CursorPosition.x--;
+                    AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
                 }
-            }
-            if ((dwFlags & WC_LIMIT_BACKSPACE) && (CursorPosition.x < 0))
-            {
-                CursorPosition.x = 0;
-                OutputDebugStringA(("CONSRV: Ignoring backspace to previous line\n"));
-            }
-            AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
-            if (dwFlags & WC_DESTRUCTIVE_BACKSPACE)
-            {
-                try
+
+                // Notify accessibility to read the backspaced character.
+                // See GH:12735, MSFT:31748387
+                if (screenInfo.HasAccessibilityEventing())
                 {
-                    screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), cursor.GetPosition());
+                    if (const auto pConsoleWindow = ServiceLocator::LocateConsoleWindow())
+                    {
+                        LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId));
+                    }
                 }
-                CATCH_LOG();
+                continue;
             }
-            if (cursor.GetPosition().x == 0 && fWrapAtEOL && pwchBuffer > pwchBufferBackupLimit)
+            case UNICODE_TAB:
             {
-                if (CheckBisectProcessW(screenInfo,
-                                        pwchBufferBackupLimit,
-                                        pwchBuffer + 1 - pwchBufferBackupLimit,
-                                        gsl::narrow_cast<size_t>(coordScreenBufferSize.width) - sOriginalXPosition,
-                                        sOriginalXPosition,
-                                        dwFlags & WC_PRINTABLE_CONTROL_CHARS))
-                {
-                    CursorPosition.x = coordScreenBufferSize.width - 1;
-                    CursorPosition.y = cursor.GetPosition().y - 1;
-
-                    // since you just backspaced yourself back up into the previous row, unset the wrap flag
-                    // on the prev row if it was set
-                    textBuffer.GetRowByOffset(CursorPosition.y).SetWrapForced(false);
-
-                    AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
-                }
+                const auto pos = cursor.GetPosition();
+                const auto tabCount = gsl::narrow_cast<size_t>(NUMBER_OF_SPACES_IN_TAB(pos.x));
+                numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &tabSpaces[0], tabCount });
+                continue;
             }
-            // Notify accessibility to read the backspaced character.
-            // See GH:12735, MSFT:31748387
-            if (screenInfo.HasAccessibilityEventing())
+            case UNICODE_LINEFEED:
             {
-                if (auto pConsoleWindow = ServiceLocator::LocateConsoleWindow())
+                auto pos = cursor.GetPosition();
+                if (WI_IsFlagClear(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN))
                 {
-                    LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId));
+                    pos.x = 0;
                 }
-            }
-            break;
-        }
-        case UNICODE_TAB:
-        {
-            const auto TabSize = NUMBER_OF_SPACES_IN_TAB(cursor.GetPosition().x);
-            CursorPosition.x = cursor.GetPosition().x + TabSize;
-
-            // move cursor forward to next tab stop.  fill space with blanks.
-            // we get here when the tab extends beyond the right edge of the
-            // window.  if the tab goes wraps the line, set the cursor to the first
-            // position in the next line.
-            pwchBuffer++;
-
-            TempNumSpaces += TabSize;
-            size_t NumChars = 0;
-            if (CursorPosition.x >= coordScreenBufferSize.width)
-            {
-                NumChars = gsl::narrow<size_t>(coordScreenBufferSize.width - cursor.GetPosition().x);
-                CursorPosition.x = 0;
-                CursorPosition.y = cursor.GetPosition().y + 1;
 
-                // since you just tabbed yourself past the end of the row, set the wrap
-                textBuffer.GetRowByOffset(cursor.GetPosition().y).SetWrapForced(true);
+                textBuffer.GetRowByOffset(pos.y).SetWrapForced(false);
+                pos.y = pos.y + 1;
+                AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
+                continue;
             }
-            else
+            case UNICODE_CARRIAGERETURN:
             {
-                NumChars = gsl::narrow<size_t>(CursorPosition.x - cursor.GetPosition().x);
-                CursorPosition.y = cursor.GetPosition().y;
-            }
-
-            try
-            {
-                const OutputCellIterator it(UNICODE_SPACE, Attributes, NumChars);
-                const auto done = screenInfo.Write(it, cursor.GetPosition());
-                NumChars = done.GetCellDistance(it);
+                auto pos = cursor.GetPosition();
+                pos.x = 0;
+                AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY);
+                continue;
             }
-            CATCH_LOG();
-
-            AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
-            break;
-        }
-        case UNICODE_CARRIAGERETURN:
-        {
-            // Carriage return moves the cursor to the beginning of the line.
-            // We don't need to worry about handling cr or lf for
-            // backspace because input is sent to the user on cr or lf.
-            pwchBuffer++;
-            CursorPosition.x = 0;
-            CursorPosition.y = cursor.GetPosition().y;
-            AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
-            break;
-        }
-        case UNICODE_LINEFEED:
-        {
-            // move cursor to the next line.
-            pwchBuffer++;
-
-            if (WI_IsFlagClear(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN))
-            {
-                // Traditionally, we reset the X position to 0 with a newline automatically.
-                // Some things might not want this automatic "ONLCR line discipline" (for example, things that are expecting a *NIX behavior.)
-                // They will turn it off with an output mode flag.
-                CursorPosition.x = 0;
+            default:
+                break;
             }
 
-            CursorPosition.y = cursor.GetPosition().y + 1;
-
+            if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE) && IS_CONTROL_CHAR(*it))
             {
-                // since we explicitly just moved down a row, clear the wrap status on the row we just came from
-                textBuffer.GetRowByOffset(cursor.GetPosition().y).SetWrapForced(false);
+                const wchar_t wchs[2]{ L'^', static_cast<wchar_t>(*it + L'@') };
+                numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &wchs[0], 2 });
             }
-
-            AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY);
-            break;
-        }
-        default:
-        {
-            const auto Char = *lpString;
-            if (Char >= UNICODE_SPACE &&
-                IsGlyphFullWidth(Char) &&
-                XPosition >= (coordScreenBufferSize.width - 1) &&
-                fWrapAtEOL)
+            else
             {
-                const auto TargetPoint = cursor.GetPosition();
-                auto& Row = textBuffer.GetRowByOffset(TargetPoint.y);
-
-                try
+                // As a special favor to incompetent apps that attempt to display control chars,
+                // convert to corresponding OEM Glyph Chars
+                const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP;
+                const auto ch = gsl::narrow_cast<char>(*it);
+                wchar_t wch = 0;
+                const auto result = MultiByteToWideChar(cp, MB_USEGLYPHCHARS, &ch, 1, &wch, 1);
+                if (result == 1)
                 {
-                    // If we're on top of a trailing cell, clear it and the previous cell.
-                    if (Row.DbcsAttrAt(TargetPoint.x) == DbcsAttribute::Trailing)
-                    {
-                        // Space to clear for 2 cells.
-                        OutputCellIterator it(UNICODE_SPACE, 2);
-
-                        // Back target point up one.
-                        auto writeTarget = TargetPoint;
-                        writeTarget.x--;
-
-                        // Write 2 clear cells.
-                        screenInfo.Write(it, writeTarget);
-                    }
+                    numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &wch, 1 });
                 }
-                catch (...)
-                {
-                    return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
-                }
-
-                CursorPosition.x = 0;
-                CursorPosition.y = TargetPoint.y + 1;
-
-                // since you just moved yourself down onto the next row with 1 character, that sounds like a
-                // forced wrap so set the flag
-                Row.SetWrapForced(true);
-
-                // Additionally, this padding is only called for IsConsoleFullWidth (a.k.a. when a character
-                // is too wide to fit on the current line).
-                Row.SetDoubleBytePadded(true);
-
-                AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY);
-                continue;
             }
-            break;
-        }
         }
-        if (FAILED_NTSTATUS(Status))
-        {
-            return Status;
-        }
-
-        *pcb += sizeof(WCHAR);
-        lpString++;
-        pwchRealUnicode++;
     }
 
-    if (nullptr != pcSpaces)
+    if (pcSpaces)
     {
-        *pcSpaces = TempNumSpaces;
+        *pcSpaces = numSpaces;
     }
 
-    return STATUS_SUCCESS;
+    return S_OK;
 }
+NT_CATCH_RETURN()
 
 // Routine Description:
 // - This routine writes a string to the screen, processing any embedded
@@ -699,9 +467,8 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC
 // - pcb - On input, number of bytes to write.  On output, number of bytes written.
 // - pcSpaces - On output, the number of spaces consumed by the written characters.
 // - dwFlags -
-//      WC_DESTRUCTIVE_BACKSPACE   backspace overwrites characters.
+//      WC_INTERACTIVE             backspace overwrites characters, control characters are expanded (as in, to "^X")
 //      WC_KEEP_CURSOR_VISIBLE     change window origin (viewport) desirable when hit rt. edge
-//      WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X")
 // Return Value:
 // Note:
 // - This routine does not process tabs and backspace properly.  That code will be implemented as part of the line editing services.
@@ -714,9 +481,9 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC
                                   const til::CoordType sOriginalXPosition,
                                   const DWORD dwFlags,
                                   _Inout_opt_ til::CoordType* const psScrollY)
+try
 {
-    if (!WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) ||
-        !WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))
+    if (WI_IsAnyFlagClear(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT))
     {
         return WriteCharsLegacy(screenInfo,
                                 pwchBufferBackupLimit,
@@ -729,40 +496,19 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC
                                 psScrollY);
     }
 
-    auto Status = STATUS_SUCCESS;
+    auto& machine = screenInfo.GetStateMachine();
+    const auto cch = *pcb / sizeof(WCHAR);
 
-    const auto BufferSize = *pcb;
-    *pcb = 0;
+    machine.ProcessString({ pwchRealUnicode, cch });
 
+    if (nullptr != pcSpaces)
     {
-        size_t TempNumSpaces = 0;
-
-        {
-            if (SUCCEEDED_NTSTATUS(Status))
-            {
-                FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)));
-                FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)));
-
-                // defined down in the WriteBuffer default case hiding on the other end of the state machine. See outputStream.cpp
-                // This is the only mode used by DoWriteConsole.
-                FAIL_FAST_IF(!(WI_IsFlagSet(dwFlags, WC_LIMIT_BACKSPACE)));
-
-                auto& machine = screenInfo.GetStateMachine();
-                const auto cch = BufferSize / sizeof(WCHAR);
-
-                machine.ProcessString({ pwchRealUnicode, cch });
-                *pcb += BufferSize;
-            }
-        }
-
-        if (nullptr != pcSpaces)
-        {
-            *pcSpaces = TempNumSpaces;
-        }
+        *pcSpaces = 0;
     }
 
-    return Status;
+    return STATUS_SUCCESS;
 }
+NT_CATCH_RETURN()
 
 // Routine Description:
 // - Takes the given text and inserts it into the given screen buffer.
@@ -825,7 +571,7 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC
                       pcbBuffer,
                       nullptr,
                       textBuffer.GetCursor().GetPosition().x,
-                      WC_LIMIT_BACKSPACE,
+                      0,
                       nullptr);
 }
 
diff --git a/src/host/_stream.h b/src/host/_stream.h
index d12e6622e72..58351c86cc1 100644
--- a/src/host/_stream.h
+++ b/src/host/_stream.h
@@ -54,9 +54,8 @@ Routine Description:
         bytes written.
     NumSpaces - On output, the number of spaces consumed by the written characters.
     dwFlags -
-      WC_DESTRUCTIVE_BACKSPACE   backspace overwrites characters.
+      WC_INTERACTIVE             backspace overwrites characters, control characters are expanded (as in, to "^X")
       WC_KEEP_CURSOR_VISIBLE     change window origin desirable when hit rt. edge
-      WC_PRINTABLE_CONTROL_CHARS if control characters should be expanded (as in, to "^X")
 
 Return Value:
 
diff --git a/src/host/cmdline.cpp b/src/host/cmdline.cpp
index 094ba9ba2dc..4b48f4bd24b 100644
--- a/src/host/cmdline.cpp
+++ b/src/host/cmdline.cpp
@@ -251,7 +251,7 @@ void RedrawCommandLine(COOKED_READ_DATA& cookedReadData)
                                        &cookedReadData.BytesRead(),
                                        &cookedReadData.VisibleCharCount(),
                                        cookedReadData.OriginalCursorPosition().x,
-                                       WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                       WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                        &ScrollY);
         FAIL_FAST_IF_NTSTATUS_FAILED(Status);
 
@@ -291,7 +291,7 @@ void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index) /
                                                       &cookedReadData.BytesRead(),
                                                       &cookedReadData.VisibleCharCount(),
                                                       cookedReadData.OriginalCursorPosition().x,
-                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                      WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                       &ScrollY));
         cookedReadData.OriginalCursorPosition().y += ScrollY;
     }
@@ -455,7 +455,7 @@ void CommandLine::_processHistoryCycling(COOKED_READ_DATA& cookedReadData,
                                                       &cookedReadData.BytesRead(),
                                                       &cookedReadData.VisibleCharCount(),
                                                       cookedReadData.OriginalCursorPosition().x,
-                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                      WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                       &ScrollY));
         cookedReadData.OriginalCursorPosition().y += ScrollY;
     }
@@ -490,7 +490,7 @@ void CommandLine::_setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData)
                                                           &cookedReadData.BytesRead(),
                                                           &cookedReadData.VisibleCharCount(),
                                                           cookedReadData.OriginalCursorPosition().x,
-                                                          WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                          WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                           &ScrollY));
             cookedReadData.OriginalCursorPosition().y += ScrollY;
         }
@@ -526,7 +526,7 @@ void CommandLine::_setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData)
                                                           &cookedReadData.BytesRead(),
                                                           &cookedReadData.VisibleCharCount(),
                                                           cookedReadData.OriginalCursorPosition().x,
-                                                          WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                          WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                           &ScrollY));
             cookedReadData.OriginalCursorPosition().y += ScrollY;
         }
@@ -553,7 +553,7 @@ void CommandLine::DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noex
                                                       &cookedReadData.BytesRead(),
                                                       &cookedReadData.VisibleCharCount(),
                                                       cookedReadData.OriginalCursorPosition().x,
-                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                      WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                       nullptr));
     }
 }
@@ -580,7 +580,7 @@ til::point CommandLine::_deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadDa
                                                       &cookedReadData.BytesRead(),
                                                       &cookedReadData.VisibleCharCount(),
                                                       cookedReadData.OriginalCursorPosition().x,
-                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                      WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                       nullptr));
     }
     return cookedReadData.OriginalCursorPosition();
@@ -869,7 +869,7 @@ til::point CommandLine::_moveCursorRight(COOKED_READ_DATA& cookedReadData) noexc
                                                               &CharsToWrite,
                                                               &NumSpaces,
                                                               cookedReadData.OriginalCursorPosition().x,
-                                                              WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                              WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                               &ScrollY));
                 cookedReadData.OriginalCursorPosition().y += ScrollY;
                 cookedReadData.VisibleCharCount() += NumSpaces;
@@ -912,7 +912,7 @@ void CommandLine::_insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept
                                                       &CharsToWrite,
                                                       &NumSpaces,
                                                       cookedReadData.OriginalCursorPosition().x,
-                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                      WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                       &ScrollY));
         cookedReadData.OriginalCursorPosition().y += ScrollY;
         cookedReadData.VisibleCharCount() += NumSpaces;
@@ -962,7 +962,7 @@ void CommandLine::_fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cooke
                                                               &cchCount,
                                                               &NumSpaces,
                                                               cookedReadData.OriginalCursorPosition().x,
-                                                              WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                              WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                               &ScrollY));
                 cookedReadData.OriginalCursorPosition().y += ScrollY;
                 cookedReadData.VisibleCharCount() += NumSpaces;
@@ -1007,7 +1007,7 @@ til::point CommandLine::_cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& c
                                                               &cookedReadData.BytesRead(),
                                                               &cookedReadData.VisibleCharCount(),
                                                               cookedReadData.OriginalCursorPosition().x,
-                                                              WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                              WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                               &ScrollY));
                 cookedReadData.OriginalCursorPosition().y += ScrollY;
                 cursorPosition.y += ScrollY;
@@ -1062,7 +1062,7 @@ til::point CommandLine::DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData
                                                           &cookedReadData.BytesRead(),
                                                           &cookedReadData.VisibleCharCount(),
                                                           cookedReadData.OriginalCursorPosition().x,
-                                                          WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                          WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                           nullptr));
         }
 
diff --git a/src/host/cmdline.h b/src/host/cmdline.h
index 32f3ee4e7b4..a8669edadde 100644
--- a/src/host/cmdline.h
+++ b/src/host/cmdline.h
@@ -131,18 +131,8 @@ void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateField
 void RedrawCommandLine(COOKED_READ_DATA& cookedReadData);
 
 // Values for WriteChars(), WriteCharsLegacy() dwFlags
-#define WC_DESTRUCTIVE_BACKSPACE 0x01
+#define WC_INTERACTIVE 0x01
 #define WC_KEEP_CURSOR_VISIBLE 0x02
-#define WC_PRINTABLE_CONTROL_CHARS 0x04
-
-// This is no longer necessary. The buffer will always be Unicode. We don't need to perform special work to check if we're in a raster font
-// and convert the entire buffer to match (and all insertions).
-//#define WC_FALSIFY_UNICODE       0x08
-
-#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  -  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/ft_fuzzer/fuzzmain.cpp b/src/host/ft_fuzzer/fuzzmain.cpp
index 87f2ee78897..4b8f04dea6e 100644
--- a/src/host/ft_fuzzer/fuzzmain.cpp
+++ b/src/host/ft_fuzzer/fuzzmain.cpp
@@ -142,7 +142,7 @@ extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t* data,
                            &sizeInBytes,
                            nullptr,
                            0,
-                           WC_PRINTABLE_CONTROL_CHARS | WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE,
+                           WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                            &scrollY);
     return 0;
 }
diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp
index eac6e5fb074..6da7ed899ab 100644
--- a/src/host/readDataCooked.cpp
+++ b/src/host/readDataCooked.cpp
@@ -534,7 +534,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
                                               &NumToWrite,
                                               &NumSpaces,
                                               _originalCursorPosition.x,
-                                              WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                              WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                               &ScrollY);
                     if (SUCCEEDED_NTSTATUS(status))
                     {
@@ -616,7 +616,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
                                                   &NumToWrite,
                                                   nullptr,
                                                   _originalCursorPosition.x,
-                                                  WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                  WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                   nullptr);
                         if (FAILED_NTSTATUS(status))
                         {
@@ -717,7 +717,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
             // write the new command line to the screen
             NumToWrite = _bytesRead;
 
-            DWORD dwFlags = WC_DESTRUCTIVE_BACKSPACE | WC_PRINTABLE_CONTROL_CHARS;
+            DWORD dwFlags = WC_INTERACTIVE;
             if (wch == UNICODE_CARRIAGERETURN)
             {
                 dwFlags |= WC_KEEP_CURSOR_VISIBLE;
@@ -782,7 +782,7 @@ bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig,
                                               &NumToWrite,
                                               nullptr,
                                               _originalCursorPosition.x,
-                                              WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                              WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                               nullptr);
                     if (FAILED_NTSTATUS(status))
                     {
@@ -844,7 +844,7 @@ size_t COOKED_READ_DATA::Write(const std::wstring_view wstr)
                                                       &bytesInserted,
                                                       &NumSpaces,
                                                       OriginalCursorPosition().x,
-                                                      WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS,
+                                                      WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE,
                                                       &ScrollY));
         OriginalCursorPosition().y += ScrollY;
         VisibleCharCount() += NumSpaces;
diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp
index 441394ef088..b26801be0a1 100644
--- a/src/host/ut_host/ScreenBufferTests.cpp
+++ b/src/host/ut_host/ScreenBufferTests.cpp
@@ -2895,7 +2895,7 @@ void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy()
 {
     BEGIN_TEST_METHOD_PROPERTIES()
         TEST_METHOD_PROPERTY(L"Data:writeSingly", L"{false, true}")
-        TEST_METHOD_PROPERTY(L"Data:writeCharsLegacyMode", L"{0, 1, 2, 3, 4, 5, 6, 7}")
+        TEST_METHOD_PROPERTY(L"Data:writeCharsLegacyMode", L"{0, 1, 2}")
     END_TEST_METHOD_PROPERTIES();
 
     bool writeSingly;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 180b9276ba8..16aa57f6d83 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -138,9 +138,16 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
         state.columnBegin = cursorPosition.x;
 
         const auto textPositionBefore = state.text.data();
-        textBuffer.WriteLine(cursorPosition.y, wrapAtEOL, attributes, state);
+        textBuffer.Write(cursorPosition.y, attributes, state);
         const auto textPositionAfter = state.text.data();
 
+        // TODO: A row should not be marked as wrapped just because we wrote the last column.
+        // It should be marked whenever we write _past_ it (above, _DoLineFeed call). See GH#15602.
+        if (wrapAtEOL && state.columnEnd >= state.columnLimit)
+        {
+            textBuffer.SetWrapForced(cursorPosition.y, true);
+        }
+
         if (state.columnBeginDirty != state.columnEndDirty)
         {
             const til::rect changedRect{ state.columnBeginDirty, cursorPosition.y, state.columnEndDirty, cursorPosition.y + 1 };


Test patch: