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(colBeg)); + const size_t chEnd = _uncheckedCharOffset(gsl::narrow_cast(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& 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(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 buffer; - try - { - buffer = std::make_unique(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(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(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(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(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(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(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(*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(*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 };