// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "Row.hpp" #include #include "../../types/inc/CodepointWidthDetector.hpp" // It would be nice to add checked array access in the future, but it's a little annoying to do so without impacting // performance (including Debug performance). Other languages are a little bit more ergonomic there than C++. #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).) #pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). #pragma warning(disable : 26472) // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narrow (type.1). extern "C" int __isa_available; constexpr auto clamp(auto value, auto lo, auto hi) { return value < lo ? lo : (value > hi ? hi : value); } // The STL is missing a std::iota_n analogue for std::iota, so I made my own. template constexpr OutIt iota_n(OutIt dest, Diff count, T val) { for (; count; --count, ++dest, ++val) { *dest = val; } return dest; } // ROW::ReplaceCharacters needs to calculate `val + count` after // calling iota_n() and this function achieves both things at once. template constexpr OutIt iota_n_mut(OutIt dest, Diff count, T& val) { for (; count; --count, ++dest, ++val) { *dest = val; } return dest; } // Same as std::fill, but purpose-built for very small `last - first` // where a trivial loop outperforms vectorization. template constexpr FwdIt fill_small(FwdIt first, FwdIt last, const T val) { for (; first != last; ++first) { *first = val; } return first; } // Same as std::fill_n, but purpose-built for very small `count` // where a trivial loop outperforms vectorization. template constexpr OutIt fill_n_small(OutIt dest, Diff count, const T val) { for (; count; --count, ++dest) { *dest = val; } return dest; } // Same as std::copy_n, but purpose-built for very short `count` // where a trivial loop outperforms vectorization. template constexpr OutIt copy_n_small(InIt first, Diff count, OutIt dest) { for (; count; --count, ++dest, ++first) { *dest = *first; } return dest; } CharToColumnMapper::CharToColumnMapper(const wchar_t* chars, const uint16_t* charOffsets, ptrdiff_t charsLength, til::CoordType currentColumn, til::CoordType columnCount) noexcept : _chars{ chars }, _charOffsets{ charOffsets }, _charsLength{ charsLength }, _currentColumn{ currentColumn }, _columnCount{ columnCount } { } // If given a position (`offset`) inside the ROW's text, this function will return the corresponding column. // This function in particular returns the glyph's first column. til::CoordType CharToColumnMapper::GetLeadingColumnAt(ptrdiff_t targetOffset) noexcept { targetOffset = clamp(targetOffset, 0, _charsLength); // This code needs to fulfill two conditions on top of the obvious (a forward/backward search): // A: We never want to stop on a column that is marked with CharOffsetsTrailer (= "GetLeadingColumn"). // B: With these parameters we always want to stop at currentOffset=4: // _charOffsets={4, 6} // currentOffset=4 *OR* 6 // targetOffset=5 // This is because we're being asked for a "LeadingColumn", while the caller gave us the offset of a // trailing surrogate pair or similar. Returning the column of the leading half is the correct choice. auto col = _currentColumn; auto currentOffset = _charOffsets[col]; // A plain forward-search until we find our targetOffset. // This loop may iterate too far and thus violate our example in condition B, however... while (targetOffset > (currentOffset & CharOffsetsMask)) { currentOffset = _charOffsets[++col]; } // This backward-search is not just a counter-part to the above, but simultaneously also handles conditions A and B. // It abuses the fact that columns marked with CharOffsetsTrailer are >0x8000 and targetOffset is always <0x8000. // This means we skip all "trailer" columns when iterating backwards, and only stop on a non-trailer (= condition A). // Condition B is fixed simply because we iterate backwards after the forward-search (in that exact order). while (targetOffset < currentOffset) { currentOffset = _charOffsets[--col]; } _currentColumn = col; return col; } // If given a position (`offset`) inside the ROW's text, this function will return the corresponding column. // This function in particular returns the glyph's last column (this matters for wide glyphs). til::CoordType CharToColumnMapper::GetTrailingColumnAt(ptrdiff_t offset) noexcept { auto col = GetLeadingColumnAt(offset); if (col < _columnCount) { // This loop is a little redundant with the forward search loop in GetLeadingColumnAt() // but it's realistically not worth caring about this. This code is not a bottleneck. for (; WI_IsFlagSet(_charOffsets[col + 1], CharOffsetsTrailer); ++col) { } } return col; } // If given a pointer inside the ROW's text buffer, this function will return the corresponding column. // This function in particular returns the glyph's first column. til::CoordType CharToColumnMapper::GetLeadingColumnAt(const wchar_t* str) noexcept { return GetLeadingColumnAt(str - _chars); } // If given a pointer inside the ROW's text buffer, this function will return the corresponding column. // This function in particular returns the glyph's last column (this matters for wide glyphs). til::CoordType CharToColumnMapper::GetTrailingColumnAt(const wchar_t* str) noexcept { return GetTrailingColumnAt(str - _chars); } // Routine Description: // - constructor // Arguments: // - rowWidth - the width of the row, cell elements // - fillAttribute - the default text attribute // Return Value: // - constructed object ROW::ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute) : _charsBuffer{ charsBuffer }, _chars{ charsBuffer, rowWidth }, _charOffsets{ charOffsetsBuffer, ::base::strict_cast(rowWidth) + 1u }, _attr{ rowWidth, fillAttribute }, _columnCount{ rowWidth } { _init(); } void ROW::SetWrapForced(const bool wrap) noexcept { _wrapForced = wrap; } bool ROW::WasWrapForced() const noexcept { return _wrapForced; } void ROW::SetDoubleBytePadded(const bool doubleBytePadded) noexcept { _doubleBytePadded = doubleBytePadded; } bool ROW::WasDoubleBytePadded() const noexcept { return _doubleBytePadded; } void ROW::SetLineRendition(const LineRendition lineRendition) noexcept { _lineRendition = lineRendition; } LineRendition ROW::GetLineRendition() const noexcept { return _lineRendition; } // Returns the index 1 past the last (technically) valid column in the row. // The interplay between the old console and newer VT APIs which support line renditions is // still unclear so it might be necessary to add two kinds of this function in the future. // Console APIs treat the buffer as a large NxM matrix after all. til::CoordType ROW::GetReadableColumnCount() const noexcept { if (_lineRendition == LineRendition::SingleWidth) [[likely]] { return _columnCount - _doubleBytePadded; } return (_columnCount - (_doubleBytePadded << 1)) >> 1; } // Routine Description: // - Sets all properties of the ROW to default values // Arguments: // - Attr - The default attribute (color) to fill // Return Value: // - void ROW::Reset(const TextAttribute& attr) noexcept { _charsHeap.reset(); _chars = { _charsBuffer, _columnCount }; // Constructing and then moving objects into place isn't free. // Modifying the existing object is _much_ faster. *_attr.runs().unsafe_shrink_to_size(1) = til::rle_pair{ attr, _columnCount }; _imageSlice = nullptr; _lineRendition = LineRendition::SingleWidth; _wrapForced = false; _doubleBytePadded = false; _promptData = std::nullopt; _init(); } void ROW::_init() noexcept { #pragma warning(push) #pragma warning(disable : 26462) // The value pointed to by '...' is assigned only once, mark it as a pointer to const (con.4). #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). #pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). // Fills _charsBuffer with whitespace and correspondingly _charOffsets // with successive numbers from 0 to _columnCount+1. #if defined(TIL_SSE_INTRINSICS) alignas(__m256i) static constexpr uint16_t whitespaceData[]{ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }; alignas(__m256i) static constexpr uint16_t offsetsData[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; alignas(__m256i) static constexpr uint16_t increment16Data[]{ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; alignas(__m128i) static constexpr uint16_t increment8Data[]{ 8, 8, 8, 8, 8, 8, 8, 8 }; // The AVX loop operates on 32 bytes at a minimum. Since _charsBuffer/_charOffsets uses 2 byte large // wchar_t/uint16_t respectively, this translates to 16-element writes, which equals a _columnCount of 15, // because it doesn't include the past-the-end char-offset as described in the _charOffsets member comment. if (__isa_available >= __ISA_AVAILABLE_AVX2 && _columnCount >= 15) { auto chars = _charsBuffer; auto charOffsets = _charOffsets.data(); // The backing buffer for both chars and charOffsets is guaranteed to be 16-byte aligned, // but AVX operations are 32-byte large. As such, when we write out the last chunk, we // have to align it to the ends of the 2 buffers. This results in a potential overlap of // 16 bytes between the last write in the main loop below and the final write afterwards. // // An example: // If you have a terminal between 16 and 23 columns the buffer has a size of 48 bytes. // The main loop below will iterate once, as it writes out bytes 0-31 and then exits. // The final write afterwards cannot write bytes 32-63 because that would write // out of bounds. Instead it writes bytes 16-47, overwriting 16 overlapping bytes. // This is better than branching and switching to SSE2, because both things are slow. // // Since we want to exit the main loop with at least 1 write left to do as the final write, // we need to subtract 1 alignment from the buffer length (= 16 bytes). Since _columnCount is // in wchar_t's we subtract -8. The same applies to the ~7 here vs ~15. If you squint slightly // you'll see how this is effectively the inverse of what CalculateCharsBufferStride does. const auto tailColumnOffset = gsl::narrow_cast((_columnCount - 8u) & ~7); const auto charsEndLoop = chars + tailColumnOffset; const auto charOffsetsEndLoop = charOffsets + tailColumnOffset; const auto whitespace = _mm256_load_si256(reinterpret_cast(&whitespaceData[0])); auto offsetsLoop = _mm256_load_si256(reinterpret_cast(&offsetsData[0])); const auto offsets = _mm256_add_epi16(offsetsLoop, _mm256_set1_epi16(tailColumnOffset)); if (chars < charsEndLoop) { const auto increment = _mm256_load_si256(reinterpret_cast(&increment16Data[0])); do { _mm256_storeu_si256(reinterpret_cast<__m256i*>(chars), whitespace); _mm256_storeu_si256(reinterpret_cast<__m256i*>(charOffsets), offsetsLoop); offsetsLoop = _mm256_add_epi16(offsetsLoop, increment); chars += 16; charOffsets += 16; } while (chars < charsEndLoop); } _mm256_storeu_si256(reinterpret_cast<__m256i*>(charsEndLoop), whitespace); _mm256_storeu_si256(reinterpret_cast<__m256i*>(charOffsetsEndLoop), offsets); } else { auto chars = _charsBuffer; auto charOffsets = _charOffsets.data(); const auto charsEnd = chars + _columnCount; const auto whitespace = _mm_load_si128(reinterpret_cast(&whitespaceData[0])); const auto increment = _mm_load_si128(reinterpret_cast(&increment8Data[0])); auto offsets = _mm_load_si128(reinterpret_cast(&offsetsData[0])); do { _mm_storeu_si128(reinterpret_cast<__m128i*>(chars), whitespace); _mm_storeu_si128(reinterpret_cast<__m128i*>(charOffsets), offsets); offsets = _mm_add_epi16(offsets, increment); chars += 8; charOffsets += 8; // If _columnCount is something like 120, the actual backing buffer for charOffsets is 121 items large. // --> The while loop uses <= to emit at least 1 more write. } while (chars <= charsEnd); } #elif defined(TIL_ARM_NEON_INTRINSICS) alignas(uint16x8_t) static constexpr uint16_t offsetsData[]{ 0, 1, 2, 3, 4, 5, 6, 7 }; auto chars = _charsBuffer; auto charOffsets = _charOffsets.data(); const auto charsEnd = chars + _columnCount; const auto whitespace = vdupq_n_u16(L' '); const auto increment = vdupq_n_u16(8); auto offsets = vld1q_u16(&offsetsData[0]); do { vst1q_u16(chars, whitespace); vst1q_u16(charOffsets, offsets); offsets = vaddq_u16(offsets, increment); chars += 8; charOffsets += 8; // If _columnCount is something like 120, the actual backing buffer for charOffsets is 121 items large. // --> The while loop uses <= to emit at least 1 more write. } while (chars <= charsEnd); #else #error "Vectorizing this function improves overall performance by up to 40%. Don't remove this warning, just add the vectorized code." std::fill_n(_charsBuffer, _columnCount, UNICODE_SPACE); std::iota(_charOffsets.begin(), _charOffsets.end(), uint16_t{ 0 }); #endif #pragma warning(push) } void ROW::CopyFrom(const ROW& source) { _lineRendition = source._lineRendition; _wrapForced = source._wrapForced; RowCopyTextFromState state{ .source = source, .sourceColumnLimit = source.GetReadableColumnCount(), }; CopyTextFrom(state); _attr = source.Attributes(); _attr.resize_trailing_extent(_columnCount); } // Returns the previous possible cursor position, preceding the given column. // Returns 0 if column is less than or equal to 0. til::CoordType ROW::NavigateToPrevious(til::CoordType column) const noexcept { return _adjustBackward(_clampedColumn(column - 1)); } // Returns the next possible cursor position, following the given column. // Returns the row width if column is beyond the width of the row. til::CoordType ROW::NavigateToNext(til::CoordType column) const noexcept { return _adjustForward(_clampedColumnInclusive(column + 1)); } // Returns the starting column of the glyph at the given column. // In other words, if you have 3 wide glyphs // AA BB CC // 01 23 45 <-- column // then `AdjustToGlyphStart(3)` returns 2. til::CoordType ROW::AdjustToGlyphStart(til::CoordType column) const noexcept { return _adjustBackward(_clampedColumn(column)); } // Returns the (exclusive) ending column of the glyph at the given column. // In other words, if you have 3 wide glyphs // AA BB CC // 01 23 45 <-- column // Examples: // - `AdjustToGlyphEnd(4)` returns 6. // - `AdjustToGlyphEnd(3)` returns 4. til::CoordType ROW::AdjustToGlyphEnd(til::CoordType column) const noexcept { return _adjustForward(_clampedColumnInclusive(column)); } // Routine Description: // - clears char data in column in row // Arguments: // - column - 0-indexed column index // Return Value: // - void ROW::ClearCell(const til::CoordType column) { static constexpr std::wstring_view space{ L" " }; ReplaceCharacters(column, 1, space); } // Routine Description: // - writes cell data to the row // Arguments: // - it - custom console iterator to use for seeking input data. bool() false when it becomes invalid while seeking. // - index - column in row to start writing at // - wrap - change the wrap flag if we hit the end of the row while writing and there's still more data in the iterator. // - limitRight - right inclusive column ID for the last write in this row. (optional, will just write to the end of row if nullopt) // Return Value: // - iterator to first cell that was not written to this row. OutputCellIterator ROW::WriteCells(OutputCellIterator it, const til::CoordType columnBegin, const std::optional wrap, std::optional limitRight) { THROW_HR_IF(E_INVALIDARG, columnBegin >= size()); THROW_HR_IF(E_INVALIDARG, limitRight.value_or(0) >= size()); // If we're given a right-side column limit, use it. Otherwise, the write limit is the final column index available in the char row. const auto finalColumnInRow = gsl::narrow_cast(limitRight.value_or(size() - 1)); auto currentColor = it->TextAttr(); uint16_t colorUses = 0; auto colorStarts = gsl::narrow_cast(columnBegin); auto currentIndex = colorStarts; while (it && currentIndex <= finalColumnInRow) { // Fill the color if the behavior isn't set to keeping the current color. if (it->TextAttrBehavior() != TextAttributeBehavior::Current) { // If the color of this cell is the same as the run we're currently on, // just increment the counter. if (currentColor == it->TextAttr()) { ++colorUses; } else { // Otherwise, commit this color into the run and save off the new one. // Now commit the new color runs into the attr row. _attr.replace(colorStarts, currentIndex, currentColor); currentColor = it->TextAttr(); colorUses = 1; colorStarts = currentIndex; } } // Fill the text if the behavior isn't set to saying there's only a color stored in this iterator. if (it->TextAttrBehavior() != TextAttributeBehavior::StoredOnly) { const auto fillingFirstColumn = currentIndex == 0; const auto fillingLastColumn = currentIndex == finalColumnInRow; const auto attr = it->DbcsAttr(); const auto& chars = it->Chars(); switch (attr) { case DbcsAttribute::Leading: if (fillingLastColumn) { // The wide char doesn't fit. Pad with whitespace. // Don't increment the iterator. Instead we'll return from this function and the // caller can call WriteCells() again on the next row with the same iterator position. ClearCell(currentIndex); SetDoubleBytePadded(true); } else { ReplaceCharacters(currentIndex, 2, chars); ++it; } break; case DbcsAttribute::Trailing: if (fillingFirstColumn) { // The wide char doesn't fit. Pad with whitespace. // Ignore the character. There's no correct alternative way to handle this situation. ClearCell(currentIndex); } else if (it.Position() == 0) { // A common way to back up and restore the buffer is via `ReadConsoleOutputW` and // `WriteConsoleOutputW` respectively. But the area might bisect/intersect/clip wide characters and // only backup either their leading or trailing half. In general, in the rest of conhost, we're // throwing away the trailing half of all `CHAR_INFO`s (during text rendering, as well as during // `ReadConsoleOutputW`), so to make this code behave the same and prevent surprises, we need to // make sure to only look at the trailer if it's the first `CHAR_INFO` the user is trying to write. ReplaceCharacters(currentIndex - 1, 2, chars); } ++it; break; default: ReplaceCharacters(currentIndex, 1, chars); ++it; break; } // If we're asked to (un)set the wrap status and we just filled the last column with some text... // NOTE: // - wrap = std::nullopt --> don't change the wrap value // - wrap = true --> we're filling cells as a steam, consider this a wrap // - wrap = false --> we're filling cells as a block, unwrap if (wrap.has_value() && fillingLastColumn) { // set wrap status on the row to parameter's value. SetWrapForced(*wrap); } } else { ++it; } // Move to the next cell for the next time through the loop. ++currentIndex; } // Now commit the final color into the attr row if (colorUses) { _attr.replace(colorStarts, currentIndex, currentColor); } return it; } void ROW::SetAttrToEnd(const til::CoordType columnBegin, const TextAttribute attr) { _attr.replace(_clampedColumnInclusive(columnBegin), _attr.size(), attr); } void ROW::ReplaceAttributes(const til::CoordType beginIndex, const til::CoordType endIndex, const TextAttribute& newAttr) { _attr.replace(_clampedColumnInclusive(beginIndex), _clampedColumnInclusive(endIndex), newAttr); } [[msvc::forceinline]] ROW::WriteHelper::WriteHelper(ROW& row, til::CoordType columnBegin, til::CoordType columnLimit, const std::wstring_view& chars) noexcept : row{ row }, chars{ chars } { colBeg = row._clampedColumnInclusive(columnBegin); colLimit = row._clampedColumnInclusive(columnLimit); chBegDirty = row._uncheckedCharOffset(colBeg); colBegDirty = row._adjustBackward(colBeg); leadingSpaces = colBeg - colBegDirty; chBeg = chBegDirty + leadingSpaces; colEnd = colBeg; colEndDirty = 0; charsConsumed = 0; } [[msvc::forceinline]] bool ROW::WriteHelper::IsValid() const noexcept { return colBeg < colLimit && !chars.empty(); } void ROW::ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars) try { assert(width >= 1 && width <= 2); WriteHelper h{ *this, columnBegin, _columnCount, chars }; if (!h.IsValid()) { return; } h.ReplaceCharacters(width); h.Finish(); } catch (...) { // Due to this function writing _charOffsets first, then calling _resizeChars (which may throw) and only then finally // filling in _chars, we might end up in a situation were _charOffsets contains offsets outside of the _chars array. // --> Restore this row to a known "okay"-state. Reset(TextAttribute{}); throw; } [[msvc::forceinline]] void ROW::WriteHelper::ReplaceCharacters(til::CoordType width) noexcept { const auto colEndNew = gsl::narrow_cast(colEnd + width); if (colEndNew > colLimit) { colEndDirty = colLimit; } else { til::at(row._charOffsets, colEnd++) = chBeg; for (; colEnd < colEndNew; ++colEnd) { til::at(row._charOffsets, colEnd) = gsl::narrow_cast(chBeg | CharOffsetsTrailer); } colEndDirty = colEnd; charsConsumed = chars.size(); } } void ROW::ReplaceText(RowWriteState& state) try { WriteHelper h{ *this, state.columnBegin, state.columnLimit, state.text }; if (!h.IsValid()) { state.columnEnd = h.colBeg; state.columnBeginDirty = h.colBeg; state.columnEndDirty = h.colBeg; return; } h.ReplaceText(); h.Finish(); state.text = state.text.substr(h.charsConsumed); // Here's why we set `state.columnEnd` to `colLimit` if there's remaining text: // Callers should be able to use `state.columnEnd` as the next cursor position, as well as the parameter for a // follow-up call to ReplaceAttributes(). But if we fail to insert a wide glyph into the last column of a row, // that last cell (which now contains padding whitespace) should get the same attributes as the rest of the // string so that the row looks consistent. This requires us to return `colLimit` instead of `colLimit - 1`. // Additionally, this has the benefit that callers can detect line wrapping by checking `columnEnd >= columnLimit`. state.columnEnd = state.text.empty() ? h.colEnd : h.colLimit; state.columnBeginDirty = h.colBegDirty; state.columnEndDirty = h.colEndDirty; } catch (...) { Reset(TextAttribute{}); throw; } [[msvc::forceinline]] void ROW::WriteHelper::ReplaceText() noexcept { // This function starts with a fast-pass for ASCII. ASCII is still predominant in technical areas. // // We can infer the "end" from the amount of columns we're given (colLimit - colBeg), // because ASCII is always 1 column wide per character. auto it = chars.begin(); const auto end = it + std::min(chars.size(), colLimit - colBeg); size_t ch = chBeg; while (it != end) { if (*it >= 0x80) [[unlikely]] { _replaceTextUnicode(ch, it); return; } til::at(row._charOffsets, colEnd) = gsl::narrow_cast(ch); ++colEnd; ++ch; ++it; } colEndDirty = colEnd; charsConsumed = ch - chBeg; } [[msvc::forceinline]] void ROW::WriteHelper::_replaceTextUnicode(size_t ch, std::wstring_view::const_iterator it) noexcept { auto& cwd = CodepointWidthDetector::Singleton(); // Check if the new text joins with the existing contents of the row to form a single grapheme cluster. if (it == chars.begin()) { auto colPrev = colBeg; while (colPrev > 0 && row._uncheckedIsTrailer(--colPrev)) { } const auto chPrev = row._uncheckedCharOffset(colPrev); const std::wstring_view charsPrev{ row._chars.data() + chPrev, ch - chPrev }; GraphemeState state; cwd.GraphemeNext(state, charsPrev); cwd.GraphemeNext(state, chars); if (state.len > 0) { colBegDirty = colPrev; colEnd = colPrev; const auto width = std::max(1, state.width); const auto colEndNew = gsl::narrow_cast(colEnd + width); if (colEndNew > colLimit) { colEndDirty = colLimit; charsConsumed = ch - chBeg; return; } // Fill our char-offset buffer with 1 entry containing the mapping from the // current column (colEnd) to the start of the glyph in the string (ch)... til::at(row._charOffsets, colEnd++) = gsl::narrow_cast(chPrev); // ...followed by 0-N entries containing an indication that the // columns are just a wide-glyph extension of the preceding one. while (colEnd < colEndNew) { til::at(row._charOffsets, colEnd++) = gsl::narrow_cast(chPrev | CharOffsetsTrailer); } ch += state.len; it += state.len; } } else { // The non-ASCII character we have encountered may be a combining mark, like "a^" which is then displayed as "รข". // In order to recognize both characters as a single grapheme, we need to back up by 1 ASCII character // and let MeasureNext() find the next proper grapheme boundary. --colEnd; --ch; --it; } if (const auto end = chars.end(); it != end) { GraphemeState state{ .beg = &*it }; do { cwd.GraphemeNext(state, chars); const auto width = std::max(1, state.width); const auto colEndNew = gsl::narrow_cast(colEnd + width); if (colEndNew > colLimit) { colEndDirty = colLimit; charsConsumed = ch - chBeg; return; } // Fill our char-offset buffer with 1 entry containing the mapping from the // current column (colEnd) to the start of the glyph in the string (ch)... til::at(row._charOffsets, colEnd++) = gsl::narrow_cast(ch); // ...followed by 0-N entries containing an indication that the // columns are just a wide-glyph extension of the preceding one. while (colEnd < colEndNew) { til::at(row._charOffsets, colEnd++) = gsl::narrow_cast(ch | CharOffsetsTrailer); } ch += state.len; it += state.len; } while (it != end); } colEndDirty = colEnd; charsConsumed = ch - chBeg; } void ROW::CopyTextFrom(RowCopyTextFromState& state) try { auto& source = state.source; const auto sourceColBeg = source._clampedColumnInclusive(state.sourceColumnBegin); const auto sourceColLimit = source._clampedColumnInclusive(state.sourceColumnLimit); std::span charOffsets; std::wstring_view chars; if (sourceColBeg < sourceColLimit) { charOffsets = source._charOffsets.subspan(sourceColBeg, static_cast(sourceColLimit) - sourceColBeg + 1); 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() + beg, end - beg }; } WriteHelper h{ *this, state.columnBegin, state.columnLimit, chars }; if (!h.IsValid() || // If we were to copy text from ourselves, we'd overwrite // our _charOffsets and break Finish() which reads from it. this == &state.source || // Any valid charOffsets array is at least 2 elements long (the 1st element is the start offset and the 2nd // element is the length of the first glyph) and begins/ends with a non-trailer offset. We don't really // need to test for the end offset, since `WriteHelper::WriteWithOffsets` already takes care of that. charOffsets.size() < 2 || WI_IsFlagSet(charOffsets.front(), CharOffsetsTrailer)) { state.columnEnd = h.colBeg; state.columnBeginDirty = h.colBeg; state.columnEndDirty = h.colBeg; state.sourceColumnEnd = source._columnCount; return; } h.CopyTextFrom(charOffsets); h.Finish(); // state.columnEnd is computed identical to ROW::ReplaceText. Check it out for more information. state.columnEnd = h.charsConsumed == chars.size() ? h.colEnd : h.colLimit; state.columnBeginDirty = h.colBegDirty; state.columnEndDirty = h.colEndDirty; state.sourceColumnEnd = sourceColBeg + h.colEnd - h.colBeg; } catch (...) { Reset(TextAttribute{}); throw; } [[msvc::forceinline]] void ROW::WriteHelper::CopyTextFrom(const std::span& charOffsets) noexcept { // Since our `charOffsets` input is already in columns (just like the `ROW::_charOffsets`), // we can directly look up the end char-offset, but... const auto colEndDirtyInput = std::min(gsl::narrow_cast(colLimit - colBeg), gsl::narrow(charOffsets.size() - 1)); // ...since the colLimit might intersect with a wide glyph in `charOffset`, we need to adjust our input-colEnd. auto colEndInput = colEndDirtyInput; for (; WI_IsFlagSet(til::at(charOffsets, colEndInput), CharOffsetsTrailer); --colEndInput) { } const auto baseOffset = til::at(charOffsets, 0); const auto endOffset = til::at(charOffsets, colEndInput); const auto inToOutOffset = gsl::narrow_cast(chBeg - baseOffset); #pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). const auto dst = row._charOffsets.data() + colEnd; _copyOffsets(dst, charOffsets.data(), colEndInput, inToOutOffset); colEnd += colEndInput; colEndDirty = gsl::narrow_cast(colBeg + colEndDirtyInput); charsConsumed = endOffset - baseOffset; } #pragma warning(push) #pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1). [[msvc::forceinline]] void ROW::WriteHelper::_copyOffsets(uint16_t* __restrict dst, const uint16_t* __restrict src, uint16_t size, uint16_t offset) noexcept { __assume(src != nullptr); __assume(dst != nullptr); // All tested compilers (including MSVC) will neatly unroll and vectorize // this loop, which is why it's written in this particular way. for (const auto end = src + size; src != end; ++src, ++dst) { const uint16_t ch = *src; const uint16_t off = ch & CharOffsetsMask; const uint16_t trailer = ch & CharOffsetsTrailer; const uint16_t newOff = off + offset; *dst = newOff | trailer; } } #pragma warning(pop) [[msvc::forceinline]] void ROW::WriteHelper::Finish() { colEndDirty = row._adjustForward(colEndDirty); const uint16_t trailingSpaces = colEndDirty - colEnd; const auto chEndDirtyOld = row._uncheckedCharOffset(colEndDirty); const auto chEndDirty = chBegDirty + charsConsumed + leadingSpaces + trailingSpaces; if (chEndDirty != chEndDirtyOld) { row._resizeChars(colEndDirty, chBegDirty, chEndDirty, chEndDirtyOld); } { // std::copy_n compiles to memmove. We can do better. It also gets rid of an extra branch, // because std::copy_n avoids calling memmove if the count is 0. It's never 0 for us. const auto itBeg = row._chars.begin() + chBeg; memcpy(&*itBeg, chars.data(), charsConsumed * sizeof(wchar_t)); if (leadingSpaces) { fill_n_small(row._chars.begin() + chBegDirty, leadingSpaces, L' '); iota_n(row._charOffsets.begin() + colBegDirty, leadingSpaces, chBegDirty); } if (trailingSpaces) { fill_n_small(itBeg + charsConsumed, trailingSpaces, L' '); iota_n(row._charOffsets.begin() + colEnd, trailingSpaces, gsl::narrow_cast(chBeg + charsConsumed)); } } // This updates `_doubleBytePadded` whenever we write the last column in the row. `_doubleBytePadded` tells our text // reflow algorithm whether it should ignore the last column. This is important when writing wide characters into // the terminal: If the last wide character in a row only fits partially, we should render whitespace, but // during text reflow pretend as if no whitespace exists. After all, the user didn't write any whitespace there. // // The way this is written, it'll set `_doubleBytePadded` to `true` no matter whether a wide character didn't fit, // or if the last 2 columns contain a wide character and a narrow character got written into the left half of it. // In both cases `trailingSpaces` is 1 and fills the last column and `_doubleBytePadded` will be `true`. if (colEndDirty == row._columnCount) { row.SetDoubleBytePadded(colEnd < row._columnCount); } } // This function represents the slow path of ReplaceCharacters(), // as it reallocates the backing buffer and shifts the char offsets. // The parameters are difficult to explain, but their names are identical to // local variables in ReplaceCharacters() which I've attempted to document there. void ROW::_resizeChars(uint16_t colEndDirty, uint16_t chBegDirty, size_t chEndDirty, uint16_t chEndDirtyOld) { const auto diff = chEndDirty - chEndDirtyOld; const auto currentLength = _charSize(); const auto newLength = currentLength + diff; if (newLength <= _chars.size()) { std::copy_n(_chars.begin() + chEndDirtyOld, currentLength - chEndDirtyOld, _chars.begin() + chEndDirty); } else { const auto minCapacity = std::min(UINT16_MAX, _chars.size() + (_chars.size() >> 1)); const auto newCapacity = gsl::narrow(std::max(newLength, minCapacity)); auto charsHeap = std::make_unique_for_overwrite(newCapacity); const std::span chars{ charsHeap.get(), newCapacity }; std::copy_n(_chars.begin(), chBegDirty, chars.begin()); std::copy_n(_chars.begin() + chEndDirtyOld, currentLength - chEndDirtyOld, chars.begin() + chEndDirty); _charsHeap = std::move(charsHeap); _chars = chars; } auto it = _charOffsets.begin() + colEndDirty; const auto end = _charOffsets.end(); for (; it != end; ++it) { *it = gsl::narrow_cast(*it + diff); } } RowAttributes& ROW::Attributes() noexcept { return _attr; } const RowAttributes& ROW::Attributes() const noexcept { return _attr; } TextAttribute ROW::GetAttrByColumn(const til::CoordType column) const { return _attr.at(_clampedColumn(column)); } std::vector ROW::GetHyperlinks() const { std::vector ids; for (const auto& run : _attr.runs()) { if (run.value.IsHyperlink()) { ids.emplace_back(run.value.GetHyperlinkId()); } } return ids; } ImageSlice* ROW::SetImageSlice(ImageSlice::Pointer imageSlice) noexcept { _imageSlice = std::move(imageSlice); return GetMutableImageSlice(); } const ImageSlice* ROW::GetImageSlice() const noexcept { return _imageSlice.get(); } ImageSlice* ROW::GetMutableImageSlice() noexcept { const auto ptr = _imageSlice.get(); if (!ptr) { return nullptr; } ptr->BumpRevision(); return ptr; } uint16_t ROW::size() const noexcept { return _columnCount; } // Routine Description: // - Retrieves the column that is one after the last non-space character in the row. til::CoordType ROW::GetLastNonSpaceColumn() const noexcept { const auto text = GetText(); const auto beg = text.data(); const auto end = beg + text.size(); #pragma warning(suppress : 26429) // Symbol 'it' is never tested for nullness, it can be marked as not_null (f.23). auto it = end; for (; it != beg; --it) { // it[-1] is safe as `it` is always greater than `beg` (loop invariant). if (it[-1] != L' ') { break; } } // We're supposed to return the measurement in cells and not characters // and therefore simply calculating `it - beg` would be wrong. // // An example: The row is 10 cells wide and `it` points to the second character. // `it - beg` would return 1, but it's possible it's actually 1 wide glyph and 8 whitespace. return gsl::narrow_cast(GetReadableColumnCount() - (end - it)); } til::CoordType ROW::MeasureLeft() const noexcept { const auto text = GetText(); const auto beg = text.begin(); const auto end = text.end(); auto it = beg; for (; it != end; ++it) { if (*it != L' ') { break; } } return gsl::narrow_cast(it - beg); } // Routine Description: // - Retrieves the column that is one after the last valid character in the row. til::CoordType ROW::MeasureRight() const noexcept { if (_wrapForced) { auto width = _columnCount; if (_doubleBytePadded) { width--; } return width; } return GetLastNonSpaceColumn(); } bool ROW::ContainsText() const noexcept { const auto text = GetText(); const auto beg = text.begin(); const auto end = text.end(); auto it = beg; for (; it != end; ++it) { if (*it != L' ') { return true; } } return false; } std::wstring_view ROW::GlyphAt(til::CoordType column) const noexcept { auto col = _clampedColumn(column); // Safety: col is [0, _columnCount). const auto beg = _uncheckedCharOffset(col); // Safety: col cannot be incremented past _columnCount, because the last // _charOffset at index _columnCount will never get the CharOffsetsTrailer flag. while (_uncheckedIsTrailer(++col)) { } // Safety: col is now (0, _columnCount]. const auto end = _uncheckedCharOffset(col); return { _chars.begin() + beg, _chars.begin() + end }; } DbcsAttribute ROW::DbcsAttrAt(til::CoordType column) const noexcept { const auto col = _clampedColumn(column); auto attr = DbcsAttribute::Single; // Safety: col is [0, _columnCount). if (_uncheckedIsTrailer(col)) { attr = DbcsAttribute::Trailing; } // Safety: col+1 is [1, _columnCount]. else if (_uncheckedIsTrailer(::base::strict_cast(col) + 1u)) { attr = DbcsAttribute::Leading; } return { attr }; } std::wstring_view ROW::GetText() const noexcept { const auto width = size_t{ til::at(_charOffsets, GetReadableColumnCount()) } & CharOffsetsMask; return { _chars.data(), width }; } // Arguments: // - columnBegin: inclusive // - columnEnd: exclusive std::wstring_view ROW::GetText(til::CoordType columnBegin, til::CoordType columnEnd) const noexcept { const auto columns = GetReadableColumnCount(); const auto colBeg = clamp(columnBegin, 0, columns); const auto colEnd = clamp(columnEnd, colBeg, columns); 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 }; } til::CoordType ROW::GetLeadingColumnAtCharOffset(const ptrdiff_t offset) const noexcept { return _createCharToColumnMapper(offset).GetLeadingColumnAt(offset); } til::CoordType ROW::GetTrailingColumnAtCharOffset(const ptrdiff_t offset) const noexcept { return _createCharToColumnMapper(offset).GetTrailingColumnAt(offset); } DelimiterClass ROW::DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept { const auto col = _clampedColumn(column); // Safety: col is [0, _columnCount). const auto glyph = _uncheckedChar(_uncheckedCharOffset(col)); if (glyph <= L' ') { return DelimiterClass::ControlChar; } else if (wordDelimiters.find(glyph) != std::wstring_view::npos) { return DelimiterClass::DelimiterChar; } else { return DelimiterClass::RegularChar; } } template constexpr uint16_t ROW::_clampedColumn(T v) const noexcept { return static_cast(clamp(v, 0, _columnCount - 1)); } template constexpr uint16_t ROW::_clampedColumnInclusive(T v) const noexcept { return static_cast(clamp(v, 0, _columnCount)); } uint16_t ROW::_charSize() const noexcept { // Safety: _charOffsets is an array of `_columnCount + 1` entries. return _charOffsets[_columnCount]; } // Safety: off must be [0, _charSize()]. template wchar_t ROW::_uncheckedChar(T off) const noexcept { return _chars[off]; } // Safety: col must be [0, _columnCount]. template uint16_t ROW::_uncheckedCharOffset(T col) const noexcept { assert(col < _charOffsets.size()); return _charOffsets[col] & CharOffsetsMask; } // Safety: col must be [0, _columnCount]. template bool ROW::_uncheckedIsTrailer(T col) const noexcept { assert(col < _charOffsets.size()); return WI_IsFlagSet(_charOffsets[col], CharOffsetsTrailer); } template T ROW::_adjustBackward(T column) const noexcept { // Safety: This is a little bit more dangerous. The first column is supposed // to never be a trailer and so this loop should exit if column == 0. for (; _uncheckedIsTrailer(column); --column) { } return column; } template T ROW::_adjustForward(T column) const noexcept { // Safety: This is a little bit more dangerous. The last column is supposed // to never be a trailer and so this loop should exit if column == _columnCount. for (; _uncheckedIsTrailer(column); ++column) { } return column; } // Creates a CharToColumnMapper given an offset into _chars.data(). // In other words, for a 120 column ROW with just ASCII text, the offset should be [0,120]. CharToColumnMapper ROW::_createCharToColumnMapper(ptrdiff_t offset) const noexcept { const auto charsSize = _charSize(); const auto lastChar = gsl::narrow_cast(charsSize); // We can sort of guess what column belongs to what offset because BMP glyphs are very common and // UTF-16 stores them in 1 char. In other words, usually a ROW will have N chars for N columns. const auto guessedColumn = gsl::narrow_cast(clamp(offset, 0, _columnCount)); return CharToColumnMapper{ _chars.data(), _charOffsets.data(), lastChar, guessedColumn, _columnCount }; } const std::optional& ROW::GetScrollbarData() const noexcept { return _promptData; } void ROW::SetScrollbarData(std::optional data) noexcept { _promptData = data; } void ROW::StartPrompt() noexcept { if (!_promptData.has_value()) { // You'd be tempted to write: // // _promptData = ScrollbarData{ // .category = MarkCategory::Prompt, // .color = std::nullopt, // .exitCode = std::nullopt, // }; // // But that's not very optimal! Read this thread for a breakdown of how // weird std::optional can be some times: // // https://github.com/microsoft/terminal/pull/16937#discussion_r1553660833 _promptData.emplace(MarkCategory::Prompt); } } void ROW::EndOutput(std::optional error) noexcept { if (_promptData.has_value()) { _promptData->exitCode = error; if (error.has_value()) { _promptData->category = *error == 0 ? MarkCategory::Success : MarkCategory::Error; } } }