diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 760f8cf501fe..34189a2b2b24 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -266,11 +266,16 @@ void ROW::TransferAttributes(const til::small_rle& a void ROW::CopyFrom(const ROW& source) { - RowCopyTextFromState state{ .source = source }; - CopyTextFrom(state); - TransferAttributes(source.Attributes(), _columnCount); _lineRendition = source._lineRendition; _wrapForced = source._wrapForced; + + RowCopyTextFromState state{ + .source = source, + .columnLimit = LineRenditionColumns(), + }; + CopyTextFrom(state); + + TransferAttributes(source.Attributes(), _columnCount); } // Returns the previous possible cursor position, preceding the given column. @@ -284,7 +289,17 @@ til::CoordType ROW::NavigateToPrevious(til::CoordType column) const noexcept // Returns the row width if column is beyond the width of the row. til::CoordType ROW::NavigateToNext(til::CoordType column) const noexcept { - return _adjustForward(_clampedColumn(column + 1)); + return _adjustForward(_clampedColumnInclusive(column + 1)); +} + +til::CoordType ROW::AdjustBackward(til::CoordType column) const noexcept +{ + return _adjustBackward(_clampedColumn(column)); +} + +til::CoordType ROW::AdjustForward(til::CoordType column) const noexcept +{ + return _adjustForward(_clampedColumnInclusive(column)); } uint16_t ROW::_adjustBackward(uint16_t column) const noexcept @@ -641,11 +656,12 @@ try if (sourceColBeg < sourceColLimit) { charOffsets = source._charOffsets.subspan(sourceColBeg, static_cast(sourceColLimit) - sourceColBeg + 1); - const auto charsOffset = charOffsets.front() & CharOffsetsMask; + const auto beg = size_t{ charOffsets.front() } & CharOffsetsMask; + const auto end = size_t{ charOffsets.back() } & CharOffsetsMask; // We _are_ using span. But C++ decided that string_view and span aren't convertible. // _chars is a std::span for performance and because it refers to raw, shared memory. #pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). - chars = { source._chars.data() + charsOffset, source._chars.size() - charsOffset }; + chars = { source._chars.data() + beg, end - beg }; } WriteHelper h{ *this, state.columnBegin, state.columnLimit, chars }; @@ -867,6 +883,16 @@ til::CoordType ROW::MeasureLeft() const noexcept til::CoordType ROW::MeasureRight() const noexcept { + if (_wrapForced) + { + auto width = _columnCount; + if (_doubleBytePadded) + { + width--; + } + return width; + } + const auto text = GetText(); const auto beg = text.begin(); const auto end = text.end(); diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index c19f343f2353..2812d3cfa632 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -114,6 +114,8 @@ class ROW final til::CoordType NavigateToPrevious(til::CoordType column) const noexcept; til::CoordType NavigateToNext(til::CoordType column) const noexcept; + til::CoordType AdjustBackward(til::CoordType column) const noexcept; + til::CoordType AdjustForward(til::CoordType column) const noexcept; void ClearCell(til::CoordType column); OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional wrap = std::nullopt, std::optional limitRight = std::nullopt); diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 7854ae2def5f..eb8fdcd1db9c 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -810,9 +810,9 @@ void TextBuffer::IncrementCircularBuffer(const TextAttribute& fillAttributes) // - The viewport //Return value: // - Coordinate position (relative to the text buffer) -til::point TextBuffer::GetLastNonSpaceCharacter(std::optional viewOptional) const +til::point TextBuffer::GetLastNonSpaceCharacter(const Viewport* viewOptional) const { - const auto viewport = viewOptional.has_value() ? viewOptional.value() : GetSize(); + const auto viewport = viewOptional ? *viewOptional : GetSize(); til::point coordEndOfText; // Search the given viewport by starting at the bottom. @@ -1069,46 +1069,40 @@ void TextBuffer::Reset() noexcept // - newSize - new size of screen. // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. -[[nodiscard]] NTSTATUS TextBuffer::ResizeTraditional(til::size newSize) noexcept +void TextBuffer::ResizeTraditional(til::size newSize) { // Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text. newSize.width = std::max(newSize.width, 1); newSize.height = std::max(newSize.height, 1); - try - { - TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer }; - const auto cursorRow = GetCursor().GetPosition().y; - const auto copyableRows = std::min(_height, newSize.height); - til::CoordType srcRow = 0; - til::CoordType dstRow = 0; - - if (cursorRow >= newSize.height) - { - srcRow = cursorRow - newSize.height + 1; - } + TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer }; + const auto cursorRow = GetCursor().GetPosition().y; + const auto copyableRows = std::min(_height, newSize.height); + til::CoordType srcRow = 0; + til::CoordType dstRow = 0; - for (; dstRow < copyableRows; ++dstRow, ++srcRow) - { - newBuffer.GetRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow)); - } - - // NOTE: Keep this in sync with _reserve(). - _buffer = std::move(newBuffer._buffer); - _bufferEnd = newBuffer._bufferEnd; - _commitWatermark = newBuffer._commitWatermark; - _initialAttributes = newBuffer._initialAttributes; - _bufferRowStride = newBuffer._bufferRowStride; - _bufferOffsetChars = newBuffer._bufferOffsetChars; - _bufferOffsetCharOffsets = newBuffer._bufferOffsetCharOffsets; - _width = newBuffer._width; - _height = newBuffer._height; + if (cursorRow >= newSize.height) + { + srcRow = cursorRow - newSize.height + 1; + } - _SetFirstRowIndex(0); + for (; dstRow < copyableRows; ++dstRow, ++srcRow) + { + newBuffer.GetRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow)); } - CATCH_RETURN(); - return S_OK; + // NOTE: Keep this in sync with _reserve(). + _buffer = std::move(newBuffer._buffer); + _bufferEnd = newBuffer._bufferEnd; + _commitWatermark = newBuffer._commitWatermark; + _initialAttributes = newBuffer._initialAttributes; + _bufferRowStride = newBuffer._bufferRowStride; + _bufferOffsetChars = newBuffer._bufferOffsetChars; + _bufferOffsetCharOffsets = newBuffer._bufferOffsetCharOffsets; + _width = newBuffer._width; + _height = newBuffer._height; + + _SetFirstRowIndex(0); } void TextBuffer::SetAsActiveBuffer(const bool isActiveBuffer) noexcept @@ -2411,204 +2405,146 @@ void TextBuffer::_AppendRTFText(std::ostringstream& contentBuilder, const std::w // the new buffer. The rows's new value is placed back into this parameter. // Return Value: // - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT. -HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, - TextBuffer& newBuffer, - const std::optional lastCharacterViewport, - std::optional> positionInfo) -try +void TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Viewport* lastCharacterViewport, PositionInformation* positionInfo) { const auto& oldCursor = oldBuffer.GetCursor(); auto& newCursor = newBuffer.GetCursor(); - // We need to save the old cursor position so that we can - // place the new cursor back on the equivalent character in - // the new buffer. - const auto cOldCursorPos = oldCursor.GetPosition(); - const auto cOldLastChar = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport); - - const auto cOldRowsTotal = cOldLastChar.y + 1; - - til::point cNewCursorPos; - auto fFoundCursorPos = false; - auto foundOldMutable = false; - auto foundOldVisible = false; - // Loop through all the rows of the old buffer and reprint them into the new buffer - til::CoordType iOldRow = 0; - for (; iOldRow < cOldRowsTotal; iOldRow++) - { - // Fetch the row and its "right" which is the last printable character. - const auto& row = oldBuffer.GetRowByOffset(iOldRow); - const auto cOldColsTotal = oldBuffer.GetLineWidth(iOldRow); - auto iRight = row.MeasureRight(); - - // If we're starting a new row, try and preserve the line rendition - // from the row in the original buffer. - const auto newBufferPos = newCursor.GetPosition(); - if (newBufferPos.x == 0) - { - auto& newRow = newBuffer.GetRowByOffset(newBufferPos.y); - newRow.SetLineRendition(row.GetLineRendition()); - } + const auto oldCursorPos = oldCursor.GetPosition(); + til::point newCursorPos; + + const auto lastRowWithText = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport).y; + + auto mutableViewportTop = positionInfo ? positionInfo->mutableViewportTop : til::CoordTypeMax; + auto visibleViewportTop = positionInfo ? positionInfo->visibleViewportTop : til::CoordTypeMax; + + til::CoordType oldY = 0; + til::CoordType newY = 0; + til::CoordType newX = 0; + til::CoordType newWidth = newBuffer.GetSize().Width(); + const auto oldHeight = std::max(lastRowWithText, oldCursorPos.y) + 1; + const auto newHeight = newBuffer.GetSize().Height(); + const auto newWidthU16 = gsl::narrow_cast(newWidth); + + // Copy oldBuffer into newBuffer until oldBuffer has been fully consumed. + for (; oldY < oldHeight; ++oldY) + { + const auto& oldRow = oldBuffer.GetRowByOffset(oldY); - // There is a special case here. If the row has a "wrap" - // flag on it, but the right isn't equal to the width (one - // index past the final valid index in the row) then there - // were a bunch trailing of spaces in the row. - // (But the measuring functions for each row Left/Right do - // not count spaces as "displayable" so they're not - // included.) - // As such, adjust the "right" to be the width of the row - // to capture all these spaces - if (row.WasWrapForced()) + // A pair of double height rows should optimally wrap as a union (i.e. after wrapping there should be 4 lines). + // But for this initial implementation I chose the alternative approach: Just truncate them. + if (oldRow.GetLineRendition() != LineRendition::SingleWidth) { - iRight = cOldColsTotal; - - // And a combined special case. - // If we wrapped off the end of the row by adding a - // piece of padding because of a double byte LEADING - // character, then remove one from the "right" to - // leave this padding out of the copy process. - if (row.WasDoubleBytePadded()) + // Since rows with a non-standard line rendition should be truncated it's important + // that we pretend as if the previous row ended in a newline, even if it didn't. + // This is what this if does: It newlines. + if (newX) { - iRight--; + newX = 0; + newY++; } - } - // Loop through every character in the current row (up to - // the "right" boundary, which is one past the final valid - // character) - til::CoordType iOldCol = 0; - const auto copyRight = iRight; - for (; iOldCol < copyRight; iOldCol++) - { - if (iOldCol == cOldCursorPos.x && iOldRow == cOldCursorPos.y) + auto& newRow = newBuffer.GetRowByOffset(newY); + + // See the comment marked with "REFLOW_RESET". + if (newY >= newHeight) { - cNewCursorPos = newCursor.GetPosition(); - fFoundCursorPos = true; + newRow.Reset(newBuffer._initialAttributes); } - // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... - const auto glyph = row.GlyphAt(iOldCol); - const auto dbcsAttr = row.DbcsAttrAt(iOldCol); - const auto textAttr = row.GetAttrByColumn(iOldCol); + newRow.CopyFrom(oldRow); + newRow.SetWrapForced(false); - newBuffer.InsertCharacter(glyph, dbcsAttr, textAttr); - } + if (oldY == oldCursorPos.y) + { + newCursorPos = { oldCursorPos.x, newY }; + } + if (oldY >= mutableViewportTop) + { + positionInfo->mutableViewportTop = newY; + mutableViewportTop = til::CoordTypeMax; + } + if (oldY >= visibleViewportTop) + { + positionInfo->visibleViewportTop = newY; + visibleViewportTop = til::CoordTypeMax; + } - // GH#32: Copy the attributes from the rest of the row into this new buffer. - // From where we are in the old buffer, to the end of the row, copy the - // remaining attributes. - // - if the old buffer is smaller than the new buffer, then just copy - // what we have, as it was. We already copied all _text_ with colors, - // but it's possible for someone to just put some color into the - // buffer to the right of that without any text (as just spaces). The - // buffer looks weird to the user when we resize and it starts losing - // those colors, so we need to copy them over too... as long as there - // is space. The last attr in the row will be extended to the end of - // the row in the new buffer. - // - if the old buffer is WIDER, than we might have wrapped onto a new - // line. Use the cursor's position's Y so that we know where the new - // row is, and start writing at the cursor position. Again, the attr - // in the last column of the old row will be extended to the end of the - // row that the text was flowed onto. - // - if the text in the old buffer didn't actually fill the whole - // line in the new buffer, then we didn't wrap. That's fine. just - // copy attributes from the old row till the end of the new row, and - // move on. - const auto newRowY = newCursor.GetPosition().y; - auto& newRow = newBuffer.GetRowByOffset(newRowY); - auto newAttrColumn = newCursor.GetPosition().x; - const auto newWidth = newBuffer.GetLineWidth(newRowY); - // Stop when we get to the end of the buffer width, or the new position - // for inserting an attr would be past the right of the new buffer. - for (auto copyAttrCol = iOldCol; - copyAttrCol < cOldColsTotal && newAttrColumn < newWidth; - copyAttrCol++, newAttrColumn++) - { - // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... - const auto textAttr = row.GetAttrByColumn(copyAttrCol); - newRow.SetAttrToEnd(newAttrColumn, textAttr); + newY++; + continue; } - // If we found the old row that the caller was interested in, set the - // out value of that parameter to the cursor's current Y position (the - // new location of the _end_ of that row in the buffer). - if (positionInfo.has_value()) + // Rows don't store any information for what column the last written character is in. + // We simply truncate all trailing whitespace in this implementation. + const auto oldRowLimit = oldRow.MeasureRight(); + til::CoordType oldX = 0; + + // Copy oldRow into newBuffer until oldRow has been fully consumed. + // We use a do-while loop to ensure that line wrapping occurs and + // that attributes are copied over even for seemingly empty rows. + do { - if (!foundOldMutable) + // This if condition handles line wrapping. + // Only if we write past the last column we should wrap and as such this if + // condition is in front of the text insertion code instead of behind it. + // A SetWrapForced of false implies an explicit newline, which is the default. + if (newX >= newWidth) { - if (iOldRow >= positionInfo.value().get().mutableViewportTop) - { - positionInfo.value().get().mutableViewportTop = newCursor.GetPosition().y; - foundOldMutable = true; - } + newBuffer.GetRowByOffset(newY).SetWrapForced(true); + newX = 0; + newY++; } - if (!foundOldVisible) + // REFLOW_RESET: + // If we shrink the buffer vertically, for instance from 100 rows to 90 rows, we will write 10 rows in the + // new buffer twice. We need to reset them before copying text, or otherwise we'll see the previous contents. + // We don't need to be smart about this. Reset() is fast and shrinking doesn't occur often. + if (newY >= newHeight && newX == 0) { - if (iOldRow >= positionInfo.value().get().visibleViewportTop) - { - positionInfo.value().get().visibleViewportTop = newCursor.GetPosition().y; - foundOldVisible = true; - } + newBuffer.GetRowByOffset(newY).Reset(newBuffer._initialAttributes); } - } - // If we didn't have a full row to copy, insert a new - // line into the new buffer. - // Only do so if we were not forced to wrap. If we did - // force a word wrap, then the existing line break was - // only because we ran out of space. - if (iRight < cOldColsTotal && !row.WasWrapForced()) - { - if (!fFoundCursorPos && (iRight == cOldCursorPos.x && iOldRow == cOldCursorPos.y)) + auto& newRow = newBuffer.GetRowByOffset(newY); + + RowCopyTextFromState state{ + .source = oldRow, + .columnBegin = newX, + .columnLimit = til::CoordTypeMax, + .sourceColumnBegin = oldX, + .sourceColumnLimit = oldRowLimit, + }; + newRow.CopyTextFrom(state); + + const auto& oldAttr = oldRow.Attributes(); + auto& newAttr = newRow.Attributes(); + const auto attributes = oldAttr.slice(gsl::narrow_cast(oldX), oldAttr.size()); + newAttr.replace(gsl::narrow_cast(newX), newAttr.size(), attributes); + newAttr.resize_trailing_extent(newWidthU16); + + if (oldY == oldCursorPos.y && oldCursorPos.x >= oldX) { - cNewCursorPos = newCursor.GetPosition(); - fFoundCursorPos = true; + newCursorPos = { newRow.AdjustForward(oldCursorPos.x - oldX + newX), newY }; } - // Only do this if it's not the final line in the buffer. - // On the final line, we want the cursor to sit - // where it is done printing for the cursor - // adjustment to follow. - if (iOldRow < cOldRowsTotal - 1) + if (oldY >= mutableViewportTop) { - newBuffer.NewlineCursor(); + positionInfo->mutableViewportTop = newY; + mutableViewportTop = til::CoordTypeMax; } - else + if (oldY >= visibleViewportTop) { - // If we are on the final line of the buffer, we have one more check. - // We got into this code path because we are at the right most column of a row in the old buffer - // that had a hard return (no wrap was forced). - // However, as we're inserting, the old row might have just barely fit into the new buffer and - // caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below. - // We need to preserve the memory of the hard return at this point by inserting one additional - // hard newline, otherwise we've lost that information. - // We only do this when the cursor has just barely poured over onto the next line so the hard return - // isn't covered by the soft one. - // e.g. - // The old line was: - // |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a. - // The cursor was here ^ - // And the new line will be: - // |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end - // | | - // ^ and the cursor is now there. - // If we leave it like this, we've lost the newline information. - // So we insert one more newline so a continued reflow of this buffer by resizing larger will - // continue to look as the original output intended with the newline data. - // After this fix, it looks like this: - // |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline) - // | | - // ^ and the cursor is now here. - const auto coordNewCursor = newCursor.GetPosition(); - if (coordNewCursor.x == 0 && coordNewCursor.y > 0) - { - if (newBuffer.GetRowByOffset(coordNewCursor.y - 1).WasWrapForced()) - { - newBuffer.NewlineCursor(); - } - } + positionInfo->visibleViewportTop = newY; + visibleViewportTop = til::CoordTypeMax; } + + oldX = state.sourceColumnEnd; + newX = state.columnEnd; + } while (oldX < oldRowLimit); + + // If the row had an explicit newline we also need to newline. :) + if (!oldRow.WasWrapForced()) + { + newX = 0; + newY++; } } @@ -2616,86 +2552,68 @@ try // printable character. This is to fix the `color 2f` scenario, where you // change the buffer colors then resize and everything below the last // printable char gets reset. See GH #12567 - auto newRowY = newCursor.GetPosition().y + 1; - const auto newHeight = newBuffer.GetSize().Height(); - const auto oldHeight = oldBuffer._estimateOffsetOfLastCommittedRow() + 1; - for (; - iOldRow < oldHeight && newRowY < newHeight; - iOldRow++) + const auto initializedRowsEnd = oldBuffer._estimateOffsetOfLastCommittedRow() + 1; + for (; oldY < initializedRowsEnd && newY < newHeight; oldY++, newY++) { - const auto& row = oldBuffer.GetRowByOffset(iOldRow); - - // Optimization: Since all these rows are below the last printable char, - // we can reasonably assume that they are filled with just spaces. - // That's convenient, we can just copy the attr row from the old buffer - // into the new one, and resize the row to match. We'll rely on the - // behavior of ATTR_ROW::Resize to trim down when narrower, or extend - // the last attr when wider. - auto& newRow = newBuffer.GetRowByOffset(newRowY); - const auto newWidth = newBuffer.GetLineWidth(newRowY); - newRow.TransferAttributes(row.Attributes(), newWidth); - - newRowY++; + auto& oldRow = oldBuffer.GetRowByOffset(oldY); + auto& newRow = newBuffer.GetRowByOffset(newY); + auto& newAttr = newRow.Attributes(); + newAttr = oldRow.Attributes(); + newAttr.resize_trailing_extent(newWidthU16); } - // Finish copying remaining parameters from the old text buffer to the new one - newBuffer.CopyProperties(oldBuffer); - newBuffer.CopyHyperlinkMaps(oldBuffer); - newBuffer.CopyPatterns(oldBuffer); - - // If we found where to put the cursor while placing characters into the buffer, - // just put the cursor there. Otherwise we have to advance manually. - if (fFoundCursorPos) - { - newCursor.SetPosition(cNewCursorPos); - } - else + // This replicates the conhost behavior where the cursor is always + // within the buffer boundaries (= no delay wrap, etc.). + // In other words: If the cursor is out of bounds, we line-wrap it. + if (newCursorPos.x >= newWidth) { - // Advance the cursor to the same offset as before - // get the number of newlines and spaces between the old end of text and the old cursor, - // then advance that many newlines and chars - auto iNewlines = cOldCursorPos.y - cOldLastChar.y; - const auto iIncrements = cOldCursorPos.x - cOldLastChar.x; - const auto cNewLastChar = newBuffer.GetLastNonSpaceCharacter(); + newBuffer.GetRowByOffset(newCursorPos.y).SetWrapForced(true); + newCursorPos.x = 0; + newCursorPos.y++; - // If the last row of the new buffer wrapped, there's going to be one less newline needed, - // because the cursor is already on the next line - if (newBuffer.GetRowByOffset(cNewLastChar.y).WasWrapForced()) + // If we line-wrap the cursor past the end of the buffer we need to circle the buffer, just like we do for text. + if (newCursorPos.y >= newY) { - iNewlines = std::max(iNewlines - 1, 0); + newBuffer.GetRowByOffset(newY).Reset(newBuffer._initialAttributes); + newX = 0; + newY++; + // Fundamentally, newY is a past-the-end index (it equals the line count that we copied), whereas newCursorPos isn't. + // As such, the above if condition might as well been newCursorPos.y == newY. This assertion ensures the math is right. + assert(newCursorPos.y < newY); } - else + } + // We recalculate newCursorPos.y relative to the end of the buffer, so that a + // cursor 10 lines above the end of the buffer stays 10 lines above the end. + // NOTE: If newCursorPos.y >= newHeight, then also newY > newHeight. + if (newCursorPos.y >= newHeight) + { + newCursorPos.y += newHeight - newY; + if (newCursorPos.y < 0) { - // if this buffer didn't wrap, but the old one DID, then the d(columns) of the - // old buffer will be one more than in this buffer, so new need one LESS. - if (oldBuffer.GetRowByOffset(cOldLastChar.y).WasWrapForced()) - { - iNewlines = std::max(iNewlines - 1, 0); - } + newCursorPos = {}; } + } - for (auto r = 0; r < iNewlines; r++) - { - newBuffer.NewlineCursor(); - } - for (auto c = 0; c < iIncrements - 1; c++) - { - newBuffer.IncrementCursor(); - } + // Since we didn't use IncrementCircularBuffer() we need to compute the proper + // _firstRow offset now, in a way that replicates IncrementCircularBuffer(). + // We need to do the same for newCursorPos.y for basically the same reason. + if (newY > newHeight) + { + newBuffer._firstRow = newY % newHeight; } - // Save old cursor size before we delete it - const auto ulSize = oldCursor.GetSize(); + newBuffer.CopyProperties(oldBuffer); + newBuffer.CopyHyperlinkMaps(oldBuffer); + newBuffer.CopyPatterns(oldBuffer); - // Set size back to real size as it will be taking over the rendering duties. - newCursor.SetSize(ulSize); + assert(newCursorPos.x >= 0 && newCursorPos.x < newWidth); + assert(newCursorPos.y >= 0 && newCursorPos.y < newHeight); + newCursor.SetSize(oldCursor.GetSize()); + newCursor.SetPosition(newCursorPos); newBuffer._marks = oldBuffer._marks; newBuffer._trimMarksOutsideBuffer(); - - return S_OK; } -CATCH_RETURN() // Method Description: // - Adds or updates a hyperlink in our hyperlink table @@ -2975,14 +2893,10 @@ void TextBuffer::AddMark(const ScrollMark& m) void TextBuffer::_trimMarksOutsideBuffer() { - const auto height = GetSize().Height(); - _marks.erase(std::remove_if(_marks.begin(), - _marks.end(), - [height](const auto& m) { - return (m.start.y < 0) || - (m.start.y >= height); - }), - _marks.end()); + const til::CoordType height = _height; + std::erase_if(_marks, [height](const auto& m) { + return (m.start.y < 0) || (m.start.y >= height); + }); } void TextBuffer::SetCurrentPromptEnd(const til::point pos) noexcept diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index fe8d7e26834e..2fddd8d5bf3d 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -159,7 +159,7 @@ class TextBuffer final // Scroll needs access to this to quickly rotate around the buffer. void IncrementCircularBuffer(const TextAttribute& fillAttributes = {}); - til::point GetLastNonSpaceCharacter(std::optional viewOptional = std::nullopt) const; + til::point GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport* viewOptional = nullptr) const; Cursor& GetCursor() noexcept; const Cursor& GetCursor() const noexcept; @@ -189,7 +189,7 @@ class TextBuffer final void Reset() noexcept; - [[nodiscard]] HRESULT ResizeTraditional(const til::size newSize) noexcept; + void ResizeTraditional(const til::size newSize); void SetAsActiveBuffer(const bool isActiveBuffer) noexcept; bool IsActiveBuffer() const noexcept; @@ -257,10 +257,7 @@ class TextBuffer final til::CoordType visibleViewportTop{ 0 }; }; - static HRESULT Reflow(TextBuffer& oldBuffer, - TextBuffer& newBuffer, - const std::optional lastCharacterViewport, - std::optional> positionInfo); + static void Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Microsoft::Console::Types::Viewport* lastCharacterViewport = nullptr, PositionInformation* positionInfo = nullptr); const size_t AddPatternRecognizer(const std::wstring_view regexString); void ClearPatternRecognizers() noexcept; diff --git a/src/buffer/out/ut_textbuffer/ReflowTests.cpp b/src/buffer/out/ut_textbuffer/ReflowTests.cpp index 2d4a5c4a1e79..618628e5de27 100644 --- a/src/buffer/out/ut_textbuffer/ReflowTests.cpp +++ b/src/buffer/out/ut_textbuffer/ReflowTests.cpp @@ -761,7 +761,7 @@ class ReflowTests static std::unique_ptr _textBufferByReflowingTextBuffer(TextBuffer& originalBuffer, const til::size newSize) { auto buffer = std::make_unique(newSize, TextAttribute{ 0x7 }, 0, false, renderer); - TextBuffer::Reflow(originalBuffer, *buffer, std::nullopt, std::nullopt); + TextBuffer::Reflow(originalBuffer, *buffer); return buffer; } diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 7d35ef502dc9..8593304097fe 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -226,6 +226,7 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept // nothing to do (the viewportSize is the same as our current size), or an // appropriate HRESULT for failing to resize. [[nodiscard]] HRESULT Terminal::UserResize(const til::size viewportSize) noexcept +try { const auto oldDimensions = _GetMutableViewport().Dimensions(); if (viewportSize == oldDimensions) @@ -233,97 +234,60 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept return S_FALSE; } - // Shortcut: if we're in the alt buffer, just resize the - // alt buffer and put off resizing the main buffer till we switch back. Fortunately, this is easy. We don't need to - // worry about the viewport and scrollback at all! The alt buffer never has - // any scrollback, so we just need to resize it and presto, we're done. + // GH#3494: We don't need to reflow the alt buffer. Apps that use the + // alt buffer will redraw themselves. This prevents graphical artifacts. + // + // This is consistent with VTE if (_inAltBuffer()) { - // stash this resize for the future. + // _deferredResize will indicate to UseMainScreenBuffer() that it needs to reflow the main buffer. + // It's unknown why we defer the reflow to a later time. Performance perhaps? _deferredResize = viewportSize; - _altBuffer->GetCursor().StartDeferDrawing(); - // we're capturing `this` here because when we exit, we want to EndDefer on the (newly created) active buffer. - auto endDefer = wil::scope_exit([this]() noexcept { _altBuffer->GetCursor().EndDeferDrawing(); }); + _altBuffer->ResizeTraditional(viewportSize); - // GH#3494: We don't need to reflow the alt buffer. Apps that use the - // alt buffer will redraw themselves. This prevents graphical artifacts. - // - // This is consistent with VTE - RETURN_IF_FAILED(_altBuffer->ResizeTraditional(viewportSize)); - - // Since the _mutableViewport is no longer the size of the actual - // viewport, then update our _altBufferSize tracker we're using to help - // us out here. _altBufferSize = viewportSize; + _altBuffer->TriggerRedrawAll(); return S_OK; } - const auto dx = viewportSize.width - oldDimensions.width; - const auto newBufferHeight = std::clamp(viewportSize.height + _scrollbackLines, 0, SHRT_MAX); - - til::size bufferSize{ viewportSize.width, newBufferHeight }; - - // This will be used to determine where the viewport should be in the new buffer. - const auto oldViewportTop = _mutableViewport.Top(); - auto newViewportTop = oldViewportTop; - auto newVisibleTop = _VisibleStartIndex(); + const auto newBufferHeight = std::clamp(viewportSize.height + _scrollbackLines, 1, SHRT_MAX); + const til::size bufferSize{ viewportSize.width, newBufferHeight }; // If the original buffer had _no_ scroll offset, then we should be at the // bottom in the new buffer as well. Track that case now. const auto originalOffsetWasZero = _scrollOffset == 0; - // skip any drawing updates that might occur until we swap _buffer with the new buffer or if we exit early. - _mainBuffer->GetCursor().StartDeferDrawing(); - // we're capturing `this` here because when we exit, we want to EndDefer on the (newly created) active buffer. - auto endDefer = wil::scope_exit([this]() noexcept { _mainBuffer->GetCursor().EndDeferDrawing(); }); + // GH#3848 - Stash away the current attributes the old text buffer is + // using. We'll initialize the new buffer with the default attributes, + // but after the resize, we'll want to make sure that the new buffer's + // current attributes (the ones used for printing new text) match the + // old buffer's. + auto newTextBuffer = std::make_unique(bufferSize, + TextAttribute{}, + 0, + _mainBuffer->IsActiveBuffer(), + _mainBuffer->GetRenderer()); + + // Build a PositionInformation to track the position of both the top of + // the mutable viewport and the top of the visible viewport in the new + // buffer. + // * the new value of mutableViewportTop will be used to figure out + // where we should place the mutable viewport in the new buffer. This + // requires a bit of trickiness to remain consistent with conpty's + // buffer (as seen below). + // * the new value of visibleViewportTop will be used to calculate the + // new scrollOffset in the new buffer, so that the visible lines on + // the screen remain roughly the same. + TextBuffer::PositionInformation positionInfo{ + .mutableViewportTop = _mutableViewport.Top(), + .visibleViewportTop = _VisibleStartIndex(), + }; - // First allocate a new text buffer to take the place of the current one. - std::unique_ptr newTextBuffer; - try - { - // GH#3848 - Stash away the current attributes the old text buffer is - // using. We'll initialize the new buffer with the default attributes, - // but after the resize, we'll want to make sure that the new buffer's - // current attributes (the ones used for printing new text) match the - // old buffer's. - const auto oldBufferAttributes = _mainBuffer->GetCurrentAttributes(); - newTextBuffer = std::make_unique(bufferSize, - TextAttribute{}, - 0, // temporarily set size to 0 so it won't render. - _mainBuffer->IsActiveBuffer(), - _mainBuffer->GetRenderer()); - - // start defer drawing on the new buffer - newTextBuffer->GetCursor().StartDeferDrawing(); - - // Build a PositionInformation to track the position of both the top of - // the mutable viewport and the top of the visible viewport in the new - // buffer. - // * the new value of mutableViewportTop will be used to figure out - // where we should place the mutable viewport in the new buffer. This - // requires a bit of trickiness to remain consistent with conpty's - // buffer (as seen below). - // * the new value of visibleViewportTop will be used to calculate the - // new scrollOffset in the new buffer, so that the visible lines on - // the screen remain roughly the same. - TextBuffer::PositionInformation oldRows{ 0 }; - oldRows.mutableViewportTop = oldViewportTop; - oldRows.visibleViewportTop = newVisibleTop; - - const std::optional oldViewStart{ oldViewportTop }; - RETURN_IF_FAILED(TextBuffer::Reflow(*_mainBuffer.get(), - *newTextBuffer.get(), - _mutableViewport, - { oldRows })); - - newViewportTop = oldRows.mutableViewportTop; - newVisibleTop = oldRows.visibleViewportTop; - - // Restore the active text attributes - newTextBuffer->SetCurrentAttributes(oldBufferAttributes); - } - CATCH_RETURN(); + TextBuffer::Reflow(*_mainBuffer.get(), *newTextBuffer.get(), &_mutableViewport, &positionInfo); + + // Restore the active text attributes + newTextBuffer->SetCurrentAttributes(_mainBuffer->GetCurrentAttributes()); // Conpty resizes a little oddly - if the height decreased, and there were // blank lines at the bottom, those lines will get trimmed. If there's not @@ -366,7 +330,7 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept const auto maxRow = std::max(newLastChar.y, newCursorPos.y); const auto proposedTopFromLastLine = maxRow - viewportSize.height + 1; - const auto proposedTopFromScrollback = newViewportTop; + const auto proposedTopFromScrollback = positionInfo.mutableViewportTop; auto proposedTop = std::max(proposedTopFromLastLine, proposedTopFromScrollback); @@ -389,17 +353,13 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept const auto proposedViewFromTop = Viewport::FromDimensions({ 0, proposedTopFromScrollback }, viewportSize); if (maxRow < proposedViewFromTop.BottomInclusive()) { - if (dx < 0 && proposedTop > 0) + if (viewportSize.width < oldDimensions.width && proposedTop > 0) { - try + const auto& row = newTextBuffer->GetRowByOffset(proposedTop - 1); + if (row.WasWrapForced()) { - const auto& row = newTextBuffer->GetRowByOffset(::base::ClampSub(proposedTop, 1)); - if (row.WasWrapForced()) - { - proposedTop--; - } + proposedTop--; } - CATCH_LOG(); } } } @@ -432,7 +392,7 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept // GH#3494: Maintain scrollbar position during resize // Make sure that we don't scroll past the mutableViewport at the bottom of the buffer - newVisibleTop = std::min(newVisibleTop, _mutableViewport.Top()); + auto newVisibleTop = std::min(positionInfo.visibleViewportTop, _mutableViewport.Top()); // Make sure we don't scroll past the top of the scrollback newVisibleTop = std::max(newVisibleTop, 0); @@ -440,16 +400,11 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept // before, and shouldn't be now either. _scrollOffset = originalOffsetWasZero ? 0 : static_cast(::base::ClampSub(_mutableViewport.Top(), newVisibleTop)); - // GH#5029 - make sure to InvalidateAll here, so that we'll paint the entire visible viewport. - try - { - _activeBuffer().TriggerRedrawAll(); - } - CATCH_LOG(); + _mainBuffer->TriggerRedrawAll(); _NotifyScrollEvent(); - return S_OK; } +CATCH_RETURN() void Terminal::Write(std::wstring_view stringView) { @@ -1034,14 +989,12 @@ int Terminal::ViewEndIndex() const noexcept // _VisibleStartIndex is the first visible line of the buffer int Terminal::_VisibleStartIndex() const noexcept { - return _inAltBuffer() ? ViewStartIndex() : - std::max(0, ViewStartIndex() - _scrollOffset); + return _inAltBuffer() ? 0 : std::max(0, _mutableViewport.Top() - _scrollOffset); } int Terminal::_VisibleEndIndex() const noexcept { - return _inAltBuffer() ? ViewEndIndex() : - std::max(0, ViewEndIndex() - _scrollOffset); + return _inAltBuffer() ? _altBufferSize.height - 1 : std::max(0, _mutableViewport.BottomInclusive() - _scrollOffset); } Viewport Terminal::_GetVisibleViewport() const noexcept diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 048e819a34ee..fca23df4c4fa 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1418,6 +1418,7 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeWithReflow(const til::size coordNewScreenSize) +try { if ((USHORT)coordNewScreenSize.width >= SHORT_MAX || (USHORT)coordNewScreenSize.height >= SHORT_MAX) { @@ -1425,26 +1426,15 @@ bool SCREEN_INFORMATION::IsMaximizedY() const return STATUS_INVALID_PARAMETER; } - // First allocate a new text buffer to take the place of the current one. - std::unique_ptr newTextBuffer; - // GH#3848 - Stash away the current attributes the old text buffer is using. // We'll initialize the new buffer with the default attributes, but after // the resize, we'll want to make sure that the new buffer's current // attributes (the ones used for printing new text) match the old buffer's. - const auto oldPrimaryAttributes = _textBuffer->GetCurrentAttributes(); - try - { - newTextBuffer = std::make_unique(coordNewScreenSize, - TextAttribute{}, - 0, // temporarily set size to 0 so it won't render. - _textBuffer->IsActiveBuffer(), - _textBuffer->GetRenderer()); - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } + auto newTextBuffer = std::make_unique(coordNewScreenSize, + TextAttribute{}, + 0, // temporarily set size to 0 so it won't render. + _textBuffer->IsActiveBuffer(), + _textBuffer->GetRenderer()); // Save cursor's relative height versus the viewport const auto sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().y - _viewport.Top(); @@ -1457,38 +1447,35 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // we're capturing _textBuffer by reference here because when we exit, we want to EndDefer on the current active buffer. auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); - auto hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get(), std::nullopt, std::nullopt); - - if (SUCCEEDED(hr)) - { - // Since the reflow doesn't preserve the virtual bottom, we try and - // estimate where it ought to be by making it the same distance from - // the cursor row as it was before the resize. However, we also need - // to make sure it is far enough down to include the last non-space - // row, and it shouldn't be less than the height of the viewport, - // otherwise the top of the virtual viewport would end up negative. - const auto cursorRow = newTextBuffer->GetCursor().GetPosition().y; - const auto lastNonSpaceRow = newTextBuffer->GetLastNonSpaceCharacter().y; - const auto estimatedBottom = cursorRow + cursorDistanceFromBottom; - const auto viewportBottom = _viewport.Height() - 1; - _virtualBottom = std::max({ lastNonSpaceRow, estimatedBottom, viewportBottom }); + TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get()); - // We can't let it extend past the bottom of the buffer either. - _virtualBottom = std::min(_virtualBottom, newTextBuffer->GetSize().BottomInclusive()); + // Since the reflow doesn't preserve the virtual bottom, we try and + // estimate where it ought to be by making it the same distance from + // the cursor row as it was before the resize. However, we also need + // to make sure it is far enough down to include the last non-space + // row, and it shouldn't be less than the height of the viewport, + // otherwise the top of the virtual viewport would end up negative. + const auto cursorRow = newTextBuffer->GetCursor().GetPosition().y; + const auto lastNonSpaceRow = newTextBuffer->GetLastNonSpaceCharacter().y; + const auto estimatedBottom = cursorRow + cursorDistanceFromBottom; + const auto viewportBottom = _viewport.Height() - 1; + _virtualBottom = std::max({ lastNonSpaceRow, estimatedBottom, viewportBottom }); - // Adjust the viewport so the cursor doesn't wildly fly off up or down. - const auto sCursorHeightInViewportAfter = cursorRow - _viewport.Top(); - til::point coordCursorHeightDiff; - coordCursorHeightDiff.y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; - LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, false)); + // We can't let it extend past the bottom of the buffer either. + _virtualBottom = std::min(_virtualBottom, newTextBuffer->GetSize().BottomInclusive()); - newTextBuffer->SetCurrentAttributes(oldPrimaryAttributes); + // Adjust the viewport so the cursor doesn't wildly fly off up or down. + const auto sCursorHeightInViewportAfter = cursorRow - _viewport.Top(); + til::point coordCursorHeightDiff; + coordCursorHeightDiff.y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; + LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, false)); - _textBuffer.swap(newTextBuffer); - } + newTextBuffer->SetCurrentAttributes(_textBuffer->GetCurrentAttributes()); - return NTSTATUS_FROM_HRESULT(hr); + _textBuffer = std::move(newTextBuffer); + return STATUS_SUCCESS; } +NT_CATCH_RETURN() // // Routine Description: @@ -1498,11 +1485,14 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeTraditional(const til::size coordNewScreenSize) +try { _textBuffer->GetCursor().StartDeferDrawing(); auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); - return NTSTATUS_FROM_HRESULT(_textBuffer->ResizeTraditional(coordNewScreenSize)); + _textBuffer->ResizeTraditional(coordNewScreenSize); + return STATUS_SUCCESS; } +NT_CATCH_RETURN() // // Routine Description: diff --git a/src/host/ut_host/ApiRoutinesTests.cpp b/src/host/ut_host/ApiRoutinesTests.cpp index 90d3fd1044b2..611eb55f14c0 100644 --- a/src/host/ut_host/ApiRoutinesTests.cpp +++ b/src/host/ut_host/ApiRoutinesTests.cpp @@ -607,7 +607,7 @@ class ApiRoutinesTests auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& si = gci.GetActiveOutputBuffer(); - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional({ 5, 5 }), L"Make the buffer small so this doesn't take forever."); + si.GetTextBuffer().ResizeTraditional({ 5, 5 }); // Tests are run both with and without the DECSTBM margins set. This should not alter // the results, since ScrollConsoleScreenBuffer should not be affected by VT margins. diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index b26801be0a1a..d2d6f6ee91df 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -2307,7 +2307,7 @@ void ScreenBufferTests::GetWordBoundary() // Make the buffer as big as our test text. const til::size newBufferSize = { gsl::narrow(length), 10 }; - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); + si.GetTextBuffer().ResizeTraditional(newBufferSize); const OutputCellIterator it(text, si.GetAttributes()); si.Write(it, { 0, 0 }); @@ -2383,7 +2383,7 @@ void ScreenBufferTests::GetWordBoundaryTrimZeros(const bool on) // Make the buffer as big as our test text. const til::size newBufferSize = { gsl::narrow(length), 10 }; - VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); + si.GetTextBuffer().ResizeTraditional(newBufferSize); const OutputCellIterator it(text, si.GetAttributes()); si.Write(it, { 0, 0 }); diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index bcb4cec4a995..091cf4c744bf 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -1795,7 +1795,7 @@ void TextBufferTests::ResizeTraditional() auto expectedSpace = UNICODE_SPACE; std::wstring_view expectedSpaceView(&expectedSpace, 1); - VERIFY_SUCCEEDED(buffer.ResizeTraditional(newSize)); + buffer.ResizeTraditional(newSize); Log::Comment(L"Verify every cell in the X dimension is still the same as when filled and the new Y row is just empty default cells."); { @@ -1861,7 +1861,7 @@ void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode() _buffer->_SetFirstRowIndex(pos.y); // Perform resize to rotate the rows around - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(bufferSize)); + _buffer->ResizeTraditional(bufferSize); // Retrieve the text at the old and new positions. const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos); @@ -1933,7 +1933,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval() // Perform resize to trim off the row of the buffer that included the emoji til::size trimmedBufferSize{ bufferSize.width, bufferSize.height - 1 }; - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); + _buffer->ResizeTraditional(trimmedBufferSize); } // This tests that columns removed from the buffer while resizing traditionally will also drop the high unicode @@ -1963,7 +1963,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() // Perform resize to trim off the column of the buffer that included the emoji til::size trimmedBufferSize{ bufferSize.width - 1, bufferSize.height }; - VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); + _buffer->ResizeTraditional(trimmedBufferSize); } void TextBufferTests::TestBurrito() diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index da6352358e86..ef421cf872c4 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -1340,7 +1340,7 @@ til::point UiaTextRangeBase::_getDocumentEnd() const { const auto optimizedBufferSize{ _getOptimizedBufferSize() }; const auto& buffer{ _pData->GetTextBuffer() }; - const auto lastCharPos{ buffer.GetLastNonSpaceCharacter(optimizedBufferSize) }; + const auto lastCharPos{ buffer.GetLastNonSpaceCharacter(&optimizedBufferSize) }; const auto cursorPos{ buffer.GetCursor().GetPosition() }; return { optimizedBufferSize.Left(), std::max(lastCharPos.y, cursorPos.y) + 1 }; }