// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "textBuffer.hpp" #include "CharRow.hpp" #include "../types/inc/convert.hpp" #pragma hdrstop using namespace Microsoft::Console::Types; // Routine Description: // - Creates a new instance of TextBuffer // Arguments: // - fontInfo - The font to use for this text buffer as specified in the global font cache // - screenBufferSize - The X by Y dimensions of the new screen buffer // - fill - Uses the .Attributes property to decide which default color to apply to all text in this buffer // - cursorSize - The height of the cursor within this buffer // Return Value: // - constructed object // Note: may throw exception TextBuffer::TextBuffer(const COORD screenBufferSize, const TextAttribute defaultAttributes, const UINT cursorSize, Microsoft::Console::Render::IRenderTarget& renderTarget) : _firstRow{ 0 }, _currentAttributes{ defaultAttributes }, _cursor{ cursorSize, *this }, _storage{}, _unicodeStorage{}, _renderTarget{ renderTarget } { // initialize ROWs for (size_t i = 0; i < static_cast(screenBufferSize.Y); ++i) { _storage.emplace_back(static_cast(i), screenBufferSize.X, _currentAttributes, this); } } // Routine Description: // - Copies properties from another text buffer into this one. // - This is primarily to copy properties that would otherwise not be specified during CreateInstance // Arguments: // - OtherBuffer - The text buffer to copy properties from // Return Value: // - void TextBuffer::CopyProperties(const TextBuffer& OtherBuffer) { GetCursor().CopyProperties(OtherBuffer.GetCursor()); } // Routine Description: // - Gets the number of rows in the buffer // Arguments: // - // Return Value: // - Total number of rows in the buffer UINT TextBuffer::TotalRowCount() const { return static_cast(_storage.size()); } // Routine Description: // - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to // the top row of the screen buffer) // Arguments: // - Number of rows down from the first row of the buffer. // Return Value: // - const reference to the requested row. Asserts if out of bounds. const ROW& TextBuffer::GetRowByOffset(const size_t index) const { const size_t totalRows = TotalRowCount(); // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows. const size_t offsetIndex = (_firstRow + index) % totalRows; return _storage[offsetIndex]; } // Routine Description: // - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to // the top row of the screen buffer) // Arguments: // - Number of rows down from the first row of the buffer. // Return Value: // - reference to the requested row. Asserts if out of bounds. ROW& TextBuffer::GetRowByOffset(const size_t index) { return const_cast(static_cast(this)->GetRowByOffset(index)); } // Routine Description: // - Retrieves read-only text iterator at the given buffer location // Arguments: // - at - X,Y position in buffer for iterator start position // Return Value: // - Read-only iterator of text data only. TextBufferTextIterator TextBuffer::GetTextDataAt(const COORD at) const { return TextBufferTextIterator(GetCellDataAt(at)); } // Routine Description: // - Retrieves read-only cell iterator at the given buffer location // Arguments: // - at - X,Y position in buffer for iterator start position // Return Value: // - Read-only iterator of cell data. TextBufferCellIterator TextBuffer::GetCellDataAt(const COORD at) const { return TextBufferCellIterator(*this, at); } // Routine Description: // - Retrieves read-only text iterator at the given buffer location // but restricted to only the specific line (Y coordinate). // Arguments: // - at - X,Y position in buffer for iterator start position // Return Value: // - Read-only iterator of text data only. TextBufferTextIterator TextBuffer::GetTextLineDataAt(const COORD at) const { return TextBufferTextIterator(GetCellLineDataAt(at)); } // Routine Description: // - Retrieves read-only cell iterator at the given buffer location // but restricted to only the specific line (Y coordinate). // Arguments: // - at - X,Y position in buffer for iterator start position // Return Value: // - Read-only iterator of cell data. TextBufferCellIterator TextBuffer::GetCellLineDataAt(const COORD at) const { SMALL_RECT limit; limit.Top = at.Y; limit.Bottom = at.Y; limit.Left = 0; limit.Right = GetSize().RightInclusive(); return TextBufferCellIterator(*this, at, Viewport::FromInclusive(limit)); } // Routine Description: // - Retrieves read-only text iterator at the given buffer location // but restricted to operate only inside the given viewport. // Arguments: // - at - X,Y position in buffer for iterator start position // - limit - boundaries for the iterator to operate within // Return Value: // - Read-only iterator of text data only. TextBufferTextIterator TextBuffer::GetTextDataAt(const COORD at, const Viewport limit) const { return TextBufferTextIterator(GetCellDataAt(at, limit)); } // Routine Description: // - Retrieves read-only cell iterator at the given buffer location // but restricted to operate only inside the given viewport. // Arguments: // - at - X,Y position in buffer for iterator start position // - limit - boundaries for the iterator to operate within // Return Value: // - Read-only iterator of cell data. TextBufferCellIterator TextBuffer::GetCellDataAt(const COORD at, const Viewport limit) const { return TextBufferCellIterator(*this, at, limit); } //Routine Description: // - Corrects and enforces consistent double byte character state (KAttrs line) within a row of the text buffer. // - This will take the given double byte information and check that it will be consistent when inserted into the buffer // at the current cursor position. // - It will correct the buffer (by erasing the character prior to the cursor) if necessary to make a consistent state. //Arguments: // - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer //Return Value: // - True if it is valid to insert a character with the given double byte attributes. False otherwise. bool TextBuffer::_AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute) { // To figure out if the sequence is valid, we have to look at the character that comes before the current one const COORD coordPrevPosition = _GetPreviousFromCursor(); ROW& prevRow = GetRowByOffset(coordPrevPosition.Y); DbcsAttribute prevDbcsAttr; try { prevDbcsAttr = prevRow.GetCharRow().DbcsAttrAt(coordPrevPosition.X); } catch (...) { LOG_HR(wil::ResultFromCaughtException()); return false; } bool fValidSequence = true; // Valid until proven otherwise bool fCorrectableByErase = false; // Can't be corrected until proven otherwise // Here's the matrix of valid items: // N = None (single byte) // L = Lead (leading byte of double byte sequence // T = Trail (trailing byte of double byte sequence // Prev Curr Result // N N OK. // N L OK. // N T Fail, uncorrectable. Trailing byte must have had leading before it. // L N Fail, OK with erase. Lead needs trailing pair. Can erase lead to correct. // L L Fail, OK with erase. Lead needs trailing pair. Can erase prev lead to correct. // L T OK. // T N OK. // T L OK. // T T Fail, uncorrectable. New trailing byte must have had leading before it. // Check for only failing portions of the matrix: if (prevDbcsAttr.IsSingle() && dbcsAttribute.IsTrailing()) { // N, T failing case (uncorrectable) fValidSequence = false; } else if (prevDbcsAttr.IsLeading()) { if (dbcsAttribute.IsSingle() || dbcsAttribute.IsLeading()) { // L, N and L, L failing cases (correctable) fValidSequence = false; fCorrectableByErase = true; } } else if (prevDbcsAttr.IsTrailing() && dbcsAttribute.IsTrailing()) { // T, T failing case (uncorrectable) fValidSequence = false; } // If it's correctable by erase, erase the previous character if (fCorrectableByErase) { // Erase previous character into an N type. try { prevRow.GetCharRow().ClearCell(coordPrevPosition.X); } catch (...) { LOG_HR(wil::ResultFromCaughtException()); return false; } // Sequence is now N N or N L, which are both okay. Set sequence back to valid. fValidSequence = true; } return fValidSequence; } //Routine Description: // - Call before inserting a character into the buffer. // - This will ensure a consistent double byte state (KAttrs line) within the text buffer // - It will attempt to correct the buffer if we're inserting an unexpected double byte character type // and it will pad out the buffer if we're going to split a double byte sequence across two rows. //Arguments: // - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer //Return Value: // - true if we successfully prepared the buffer and moved the cursor // - false otherwise (out of memory) bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute) { // Assert the buffer state is ready for this character // This function corrects most errors. If this is false, we had an uncorrectable one. FAIL_FAST_IF(!(_AssertValidDoubleByteSequence(dbcsAttribute))); // Shouldn't be uncorrectable sequences unless something is very wrong. bool fSuccess = true; // Now compensate if we don't have enough space for the upcoming double byte sequence // We only need to compensate for leading bytes if (dbcsAttribute.IsLeading()) { short const sBufferWidth = GetSize().Width(); // If we're about to lead on the last column in the row, we need to add a padding space if (GetCursor().GetPosition().X == sBufferWidth - 1) { // set that we're wrapping for double byte reasons CharRow& charRow = GetRowByOffset(GetCursor().GetPosition().Y).GetCharRow(); charRow.SetDoubleBytePadded(true); // then move the cursor forward and onto the next row fSuccess = IncrementCursor(); } } return fSuccess; } // Routine Description: // - Writes cells to the output buffer. Writes at the cursor. // Arguments: // - givenIt - Iterator representing output cell data to write // Return Value: // - The final position of the iterator OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt) { const auto& cursor = GetCursor(); const auto target = cursor.GetPosition(); const auto finalIt = Write(givenIt, target); return finalIt; } // Routine Description: // - Writes cells to the output buffer. // Arguments: // - givenIt - Iterator representing output cell data to write // - target - the row/column to start writing the text to // Return Value: // - The final position of the iterator OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt, const COORD target) { // Make mutable copy so we can walk. auto it = givenIt; // Make mutable target so we can walk down lines. auto lineTarget = target; // Get size of the text buffer so we can stay in bounds. const auto size = GetSize(); // While there's still data in the iterator and we're still targeting in bounds... while (it && size.IsInBounds(lineTarget)) { // Attempt to write as much data as possible onto this line. it = WriteLine(it, lineTarget, true); // Move to the next line down. lineTarget.X = 0; ++lineTarget.Y; } return it; } // Routine Description: // - Writes one line of text to the output buffer. // Arguments: // - givenIt - The iterator that will dereference into cell data to insert // - target - Coordinate targeted within output buffer // - setWrap - Whether we should try to set the wrap flag if we write up to the end of the line and have more data // - limitRight - Optionally restrict the right boundary for writing (e.g. stop writing earlier than the end of line) // Return Value: // - The iterator, but advanced to where we stopped writing. Use to find input consumed length or cells written length. OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt, const COORD target, const bool setWrap, std::optional limitRight) { // If we're not in bounds, exit early. if (!GetSize().IsInBounds(target)) { return givenIt; } // Get the row and write the cells ROW& row = GetRowByOffset(target.Y); const auto newIt = row.WriteCells(givenIt, target.X, setWrap, limitRight); // Take the cell distance written and notify that it needs to be repainted. const auto written = newIt.GetCellDistance(givenIt); const Viewport paint = Viewport::FromDimensions(target, { gsl::narrow(written), 1 }); _NotifyPaint(paint); return newIt; } //Routine Description: // - Inserts one codepoint into the buffer at the current cursor position and advances the cursor as appropriate. //Arguments: // - chars - The codepoint to insert // - dbcsAttribute - Double byte information associated with the codepoint // - bAttr - Color data associated with the character //Return Value: // - true if we successfully inserted the character // - false otherwise (out of memory) bool TextBuffer::InsertCharacter(const std::wstring_view chars, const DbcsAttribute dbcsAttribute, const TextAttribute attr) { // Ensure consistent buffer state for double byte characters based on the character type we're about to insert bool fSuccess = _PrepareForDoubleByteSequence(dbcsAttribute); if (fSuccess) { // Get the current cursor position short const iRow = GetCursor().GetPosition().Y; // row stored as logical position, not array position short const iCol = GetCursor().GetPosition().X; // column logical and array positions are equal. // Get the row associated with the given logical position ROW& Row = GetRowByOffset(iRow); // Store character and double byte data CharRow& charRow = Row.GetCharRow(); short const cBufferWidth = GetSize().Width(); try { charRow.GlyphAt(iCol) = chars; charRow.DbcsAttrAt(iCol) = dbcsAttribute; } catch (...) { LOG_HR(wil::ResultFromCaughtException()); return false; } // Store color data fSuccess = Row.GetAttrRow().SetAttrToEnd(iCol, attr); if (fSuccess) { // Advance the cursor fSuccess = IncrementCursor(); } } return fSuccess; } //Routine Description: // - Inserts one ucs2 codepoint into the buffer at the current cursor position and advances the cursor as appropriate. //Arguments: // - wch - The codepoint to insert // - dbcsAttribute - Double byte information associated with the codepoint // - bAttr - Color data associated with the character //Return Value: // - true if we successfully inserted the character // - false otherwise (out of memory) bool TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr) { return InsertCharacter({ &wch, 1 }, dbcsAttribute, attr); } //Routine Description: // - Finds the current row in the buffer (as indicated by the cursor position) // and specifies that we have forced a line wrap on that row //Arguments: // - - Always sets to wrap //Return Value: // - void TextBuffer::_SetWrapOnCurrentRow() { _AdjustWrapOnCurrentRow(true); } //Routine Description: // - Finds the current row in the buffer (as indicated by the cursor position) // and specifies whether or not it should have a line wrap flag. //Arguments: // - fSet - True if this row has a wrap. False otherwise. //Return Value: // - void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) { // The vertical position of the cursor represents the current row we're manipulating. const UINT uiCurrentRowOffset = GetCursor().GetPosition().Y; // Set the wrap status as appropriate GetRowByOffset(uiCurrentRowOffset).GetCharRow().SetWrapForced(fSet); } //Routine Description: // - Increments the cursor one position in the buffer as if text is being typed into the buffer. // - NOTE: Will introduce a wrap marker if we run off the end of the current row //Arguments: // - //Return Value: // - true if we successfully moved the cursor. // - false otherwise (out of memory) bool TextBuffer::IncrementCursor() { // Cursor position is stored as logical array indices (starts at 0) for the window // Buffer Size is specified as the "length" of the array. It would say 80 for valid values of 0-79. // So subtract 1 from buffer size in each direction to find the index of the final column in the buffer const short iFinalColumnIndex = GetSize().RightInclusive(); // Move the cursor one position to the right GetCursor().IncrementXPosition(1); bool fSuccess = true; // If we've passed the final valid column... if (GetCursor().GetPosition().X > iFinalColumnIndex) { // Then mark that we've been forced to wrap _SetWrapOnCurrentRow(); // Then move the cursor to a new line fSuccess = NewlineCursor(); } return fSuccess; } //Routine Description: // - Increments the cursor one line down in the buffer and to the beginning of the line //Arguments: // - //Return Value: // - true if we successfully moved the cursor. bool TextBuffer::NewlineCursor() { bool fSuccess = false; short const iFinalRowIndex = GetSize().BottomInclusive(); // Reset the cursor position to 0 and move down one line GetCursor().SetXPosition(0); GetCursor().IncrementYPosition(1); // If we've passed the final valid row... if (GetCursor().GetPosition().Y > iFinalRowIndex) { // Stay on the final logical/offset row of the buffer. GetCursor().SetYPosition(iFinalRowIndex); // Instead increment the circular buffer to move us into the "oldest" row of the backing buffer fSuccess = IncrementCircularBuffer(); } else { fSuccess = true; } return fSuccess; } //Routine Description: // - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable. //Arguments: // - //Return Value: // - true if we successfully incremented the buffer. bool TextBuffer::IncrementCircularBuffer() { // FirstRow is at any given point in time the array index in the circular buffer that corresponds // to the logical position 0 in the window (cursor coordinates and all other coordinates). _renderTarget.TriggerCircling(); // First, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed. bool fSuccess = _storage.at(_firstRow).Reset(_currentAttributes); if (fSuccess) { // Now proceed to increment. // Incrementing it will cause the next line down to become the new "top" of the window (the new "0" in logical coordinates) _firstRow++; // If we pass up the height of the buffer, loop back to 0. if (_firstRow >= GetSize().Height()) { _firstRow = 0; } } return fSuccess; } //Routine Description: // - Retrieves the position of the last non-space character on the final line of the text buffer. //Arguments: // - //Return Value: // - Coordinate position in screen coordinates (offset coordinates, not array index coordinates). COORD TextBuffer::GetLastNonSpaceCharacter() const { COORD coordEndOfText; // Always search the whole buffer, by starting at the bottom. coordEndOfText.Y = GetSize().BottomInclusive(); const ROW* pCurrRow = &GetRowByOffset(coordEndOfText.Y); // The X position of the end of the valid text is the Right draw boundary (which is one beyond the final valid character) coordEndOfText.X = static_cast(pCurrRow->GetCharRow().MeasureRight()) - 1; // If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text. bool fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > 0); // this row is empty, and we're not at the top while (fDoBackUp) { coordEndOfText.Y--; pCurrRow = &GetRowByOffset(coordEndOfText.Y); // We need to back up to the previous row if this line is empty, AND there are more rows coordEndOfText.X = static_cast(pCurrRow->GetCharRow().MeasureRight()) - 1; fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > 0); } // don't allow negative results coordEndOfText.Y = std::max(coordEndOfText.Y, 0i16); coordEndOfText.X = std::max(coordEndOfText.X, 0i16); return coordEndOfText; } // Routine Description: // - Retrieves the position of the previous character relative to the current cursor position // Arguments: // - // Return Value: // - Coordinate position in screen coordinates of the character just before the cursor. // - NOTE: Will return 0,0 if already in the top left corner COORD TextBuffer::_GetPreviousFromCursor() const { COORD coordPosition = GetCursor().GetPosition(); // If we're not at the left edge, simply move the cursor to the left by one if (coordPosition.X > 0) { coordPosition.X--; } else { // Otherwise, only if we're not on the top row (e.g. we don't move anywhere in the top left corner. there is no previous) if (coordPosition.Y > 0) { // move the cursor to the right edge coordPosition.X = GetSize().RightInclusive(); // and up one line coordPosition.Y--; } } return coordPosition; } const SHORT TextBuffer::GetFirstRowIndex() const { return _firstRow; } const Viewport TextBuffer::GetSize() const { return Viewport::FromDimensions({ 0, 0 }, { gsl::narrow(_storage.at(0).size()), gsl::narrow(_storage.size()) }); } void TextBuffer::_SetFirstRowIndex(const SHORT FirstRowIndex) { _firstRow = FirstRowIndex; } void TextBuffer::ScrollRows(const SHORT firstRow, const SHORT size, const SHORT delta) { // If we don't have to move anything, leave early. if (delta == 0) { return; } // OK. We're about to play games by moving rows around within the deque to // scroll a massive region in a faster way than copying things. // To make this easier, first correct the circular buffer to have the first row be 0 again. if (_firstRow != 0) { // Rotate the buffer to put the first row at the front. std::rotate(_storage.begin(), _storage.begin() + _firstRow, _storage.end()); // The first row is now at the top. _firstRow = 0; } // Rotate just the subsection specified if (delta < 0) { // The layout is like this: // delta is -2, size is 3, firstRow is 5 // We want 3 rows from 5 (5, 6, and 7) to move up 2 spots. // --- (storage) ---- // | 0 begin // | 1 // | 2 // | 3 A. begin + firstRow + delta (because delta is negative) // | 4 // | 5 B. begin + firstRow // | 6 // | 7 // | 8 C. begin + firstRow + size // | 9 // | 10 // | 11 // - end // We want B to slide up to A (the negative delta) and everything from [B,C) to slide up with it. // So the final layout will be // --- (storage) ---- // | 0 begin // | 1 // | 2 // | 5 // | 6 // | 7 // | 3 // | 4 // | 8 // | 9 // | 10 // | 11 // - end std::rotate(_storage.begin() + firstRow + delta, _storage.begin() + firstRow, _storage.begin() + firstRow + size); } else { // The layout is like this: // delta is 2, size is 3, firstRow is 5 // We want 3 rows from 5 (5, 6, and 7) to move down 2 spots. // --- (storage) ---- // | 0 begin // | 1 // | 2 // | 3 // | 4 // | 5 A. begin + firstRow // | 6 // | 7 // | 8 B. begin + firstRow + size // | 9 // | 10 C. begin + firstRow + size + delta // | 11 // - end // We want B-1 to slide down to C-1 (the positive delta) and everything from [A, B) to slide down with it. // So the final layout will be // --- (storage) ---- // | 0 begin // | 1 // | 2 // | 3 // | 4 // | 8 // | 9 // | 5 // | 6 // | 7 // | 10 // | 11 // - end std::rotate(_storage.begin() + firstRow, _storage.begin() + firstRow + size, _storage.begin() + firstRow + size + delta); } // Renumber the IDs now that we've rearranged where the rows sit within the buffer. // Refreshing should also delegate to the UnicodeStorage to re-key all the stored unicode sequences (where applicable). _RefreshRowIDs(std::nullopt); } Cursor& TextBuffer::GetCursor() { return _cursor; } const Cursor& TextBuffer::GetCursor() const { return _cursor; } [[nodiscard]] TextAttribute TextBuffer::GetCurrentAttributes() const noexcept { return _currentAttributes; } void TextBuffer::SetCurrentAttributes(const TextAttribute currentAttributes) noexcept { _currentAttributes = currentAttributes; } // Routine Description: // - Resets the text contents of this buffer with the default character // and the default current color attributes void TextBuffer::Reset() { const auto attr = GetCurrentAttributes(); for (auto& row : _storage) { row.GetCharRow().Reset(); row.GetAttrRow().Reset(attr); } } // Routine Description: // - This is the legacy screen resize with minimal changes // Arguments: // - 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(const COORD newSize) noexcept { RETURN_HR_IF(E_INVALIDARG, newSize.X < 0 || newSize.Y < 0); const auto currentSize = GetSize().Dimensions(); const auto attributes = GetCurrentAttributes(); SHORT TopRow = 0; // new top row of the screen buffer if (newSize.Y <= GetCursor().GetPosition().Y) { TopRow = GetCursor().GetPosition().Y - newSize.Y + 1; } const SHORT TopRowIndex = (GetFirstRowIndex() + TopRow) % currentSize.Y; // rotate rows until the top row is at index 0 try { const ROW& newTopRow = _storage[TopRowIndex]; while (&newTopRow != &_storage.front()) { _storage.push_back(std::move(_storage.front())); _storage.pop_front(); } _SetFirstRowIndex(0); // realloc in the Y direction // remove rows if we're shrinking while (_storage.size() > static_cast(newSize.Y)) { _storage.pop_back(); } // add rows if we're growing while (_storage.size() < static_cast(newSize.Y)) { _storage.emplace_back(static_cast(_storage.size()), newSize.X, attributes, this); } // Now that we've tampered with the row placement, refresh all the row IDs. // Also take advantage of the row ID refresh loop to resize the rows in the X dimension // and cleanup the UnicodeStorage characters that might fall outside the resized buffer. _RefreshRowIDs(newSize.X); } CATCH_RETURN(); return S_OK; } const UnicodeStorage& TextBuffer::GetUnicodeStorage() const { return _unicodeStorage; } UnicodeStorage& TextBuffer::GetUnicodeStorage() { return _unicodeStorage; } // Routine Description: // - Method to help refresh all the Row IDs after manipulating the row // by shuffling pointers around. // - This will also update parent pointers that are stored in depth within the buffer // (e.g. it will update CharRow parents pointing at Rows that might have been moved around) // - Optionally takes a new row width if we're resizing to perform a resize operation and cleanup // any high unicode (UnicodeStorage) runs while we're already looping through the rows. // Arguments: // - newRowWidth - Optional new value for the row width. void TextBuffer::_RefreshRowIDs(std::optional newRowWidth) { std::map rowMap; SHORT i = 0; for (auto& it : _storage) { // Build a map so we can update Unicode Storage rowMap.emplace(it.GetId(), i); // Update the IDs it.SetId(i++); // Also update the char row parent pointers as they can get shuffled up in the rotates. it.GetCharRow().UpdateParent(&it); // Resize the rows in the X dimension if we have a new width if (newRowWidth.has_value()) { // Realloc in the X direction THROW_IF_FAILED(it.Resize(newRowWidth.value())); } } // Give the new mapping to Unicode Storage _unicodeStorage.Remap(rowMap, newRowWidth); } void TextBuffer::_NotifyPaint(const Viewport& viewport) const { _renderTarget.TriggerRedraw(viewport); } // Routine Description: // - Retrieves the first row from the underlying buffer. // Arguments: // - // Return Value: // - reference to the first row. ROW& TextBuffer::_GetFirstRow() { return GetRowByOffset(0); } // Routine Description: // - Retrieves the row that comes before the given row. // - Does not wrap around the screen buffer. // Arguments: // - The current row. // Return Value: // - reference to the previous row // Note: // - will throw exception if called with the first row of the text buffer ROW& TextBuffer::_GetPrevRowNoWrap(const ROW& Row) { int prevRowIndex = Row.GetId() - 1; if (prevRowIndex < 0) { prevRowIndex = TotalRowCount() - 1; } THROW_HR_IF(E_FAIL, Row.GetId() == _firstRow); return _storage[prevRowIndex]; } // Method Description: // - Retrieves this buffer's current render target. // Arguments: // - // Return Value: // - This buffer's current render target. Microsoft::Console::Render::IRenderTarget& TextBuffer::GetRenderTarget() { return _renderTarget; } // Routine Description: // - Retrieves the text data from the selected region and presents it in a clipboard-ready format (given little post-processing). // Arguments: // - lineSelection - true if entire line is being selected. False otherwise (box selection) // - trimTrailingWhitespace - setting flag removes trailing whitespace at the end of each row in selection // - selectionRects - the selection regions from which the data will be extracted from the buffer // - GetForegroundColor - function used to map TextAttribute to RGB COLORREF for foreground color // - GetBackgroundColor - function used to map TextAttribute to RGB COLORREF for foreground color // Return Value: // - The text, background color, and foreground color data of the selected region of the text buffer. const TextBuffer::TextAndColor TextBuffer::GetTextForClipboard(const bool lineSelection, const bool trimTrailingWhitespace, const std::vector& selectionRects, std::function GetForegroundColor, std::function GetBackgroundColor) const { TextAndColor data; // preallocate our vectors to reduce reallocs size_t const rows = selectionRects.size(); data.text.reserve(rows); data.FgAttr.reserve(rows); data.BkAttr.reserve(rows); // for each row in the selection for (UINT i = 0; i < rows; i++) { const UINT iRow = selectionRects.at(i).Top; const Viewport highlight = Viewport::FromInclusive(selectionRects.at(i)); // retrieve the data from the screen buffer auto it = GetCellDataAt(highlight.Origin(), highlight); // allocate a string buffer std::wstring selectionText; std::vector selectionFgAttr; std::vector selectionBkAttr; // preallocate to avoid reallocs selectionText.reserve(highlight.Width() + 2); // + 2 for \r\n if we munged it selectionFgAttr.reserve(highlight.Width() + 2); selectionBkAttr.reserve(highlight.Width() + 2); // copy char data into the string buffer, skipping trailing bytes while (it) { const auto& cell = *it; auto cellData = cell.TextAttr(); COLORREF const CellFgAttr = GetForegroundColor(cellData); COLORREF const CellBkAttr = GetBackgroundColor(cellData); if (!cell.DbcsAttr().IsTrailing()) { selectionText.append(cell.Chars()); for (const wchar_t wch : cell.Chars()) { selectionFgAttr.push_back(CellFgAttr); selectionBkAttr.push_back(CellBkAttr); } } it++; } // trim trailing spaces if SHIFT key not held if (trimTrailingWhitespace) { const ROW& Row = GetRowByOffset(iRow); // FOR LINE SELECTION ONLY: if the row was wrapped, don't remove the spaces at the end. if (!lineSelection || !Row.GetCharRow().WasWrapForced()) { while (!selectionText.empty() && selectionText.back() == UNICODE_SPACE) { selectionText.pop_back(); selectionFgAttr.pop_back(); selectionBkAttr.pop_back(); } } // apply CR/LF to the end of the final string, unless we're the last line. // a.k.a if we're earlier than the bottom, then apply CR/LF. if (i < selectionRects.size() - 1) { // FOR LINE SELECTION ONLY: if the row was wrapped, do not apply CR/LF. // a.k.a. if the row was NOT wrapped, then we can assume a CR/LF is proper // always apply \r\n for box selection if (!lineSelection || !GetRowByOffset(iRow).GetCharRow().WasWrapForced()) { COLORREF const Blackness = RGB(0x00, 0x00, 0x00); // cant see CR/LF so just use black FG & BK selectionText.push_back(UNICODE_CARRIAGERETURN); selectionText.push_back(UNICODE_LINEFEED); selectionFgAttr.push_back(Blackness); selectionFgAttr.push_back(Blackness); selectionBkAttr.push_back(Blackness); selectionBkAttr.push_back(Blackness); } } } data.text.emplace_back(selectionText); data.FgAttr.emplace_back(selectionFgAttr); data.BkAttr.emplace_back(selectionBkAttr); } return data; }