mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-25 07:31:42 +00:00
This commit fixes a regression in v1.24, where composing Korean text in-between existing characters causes text to the right to be hidden. This is problematic for the Korean IME, because it commonly inserts the composed syllable between existing characters. The fix compresses (removes/absorbs) available whitespace to the right of the composition to make space for any existing text. This means that a composition now virtually acts in insert mode. Closes #20040 Refs #19738 Refs #20039 ## Validation Steps Performed 1. Korean IME: compose `ㄷ` between `가` and `나` → display shows `가ㄷ나`, `나` visible. 2. TUI box (vim prompt with padding): compose inside padded text box → border preserved in place. 3. Composition at end of line (no chars to right): unaffected. 4. English and other IME input: not affected (no active composition row). Co-authored-by: jason <drvoss@users.noreply.github.com> Co-authored-by: Leonard Hecker <lhecker@microsoft.com>
1289 lines
48 KiB
C++
1289 lines
48 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
#include "Row.hpp"
|
|
|
|
#include <isa_availability.h>
|
|
|
|
#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<typename OutIt, typename Diff, typename T>
|
|
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<typename OutIt, typename Diff, typename T>
|
|
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<typename FwdIt, typename T>
|
|
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<typename OutIt, typename Diff, typename T>
|
|
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<typename InIt, typename Diff, typename OutIt>
|
|
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<size_t>(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:
|
|
// - <none>
|
|
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<uint16_t>((_columnCount - 8u) & ~7);
|
|
const auto charsEndLoop = chars + tailColumnOffset;
|
|
const auto charOffsetsEndLoop = charOffsets + tailColumnOffset;
|
|
|
|
const auto whitespace = _mm256_load_si256(reinterpret_cast<const __m256i*>(&whitespaceData[0]));
|
|
auto offsetsLoop = _mm256_load_si256(reinterpret_cast<const __m256i*>(&offsetsData[0]));
|
|
const auto offsets = _mm256_add_epi16(offsetsLoop, _mm256_set1_epi16(tailColumnOffset));
|
|
|
|
if (chars < charsEndLoop)
|
|
{
|
|
const auto increment = _mm256_load_si256(reinterpret_cast<const __m256i*>(&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<const __m128i*>(&whitespaceData[0]));
|
|
const auto increment = _mm_load_si128(reinterpret_cast<const __m128i*>(&increment8Data[0]));
|
|
auto offsets = _mm_load_si128(reinterpret_cast<const __m128i*>(&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(pop)
|
|
}
|
|
|
|
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:
|
|
// - <none>
|
|
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<bool> wrap, std::optional<til::CoordType> 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<uint16_t>(limitRight.value_or(size() - 1));
|
|
|
|
auto currentColor = it->TextAttr();
|
|
uint16_t colorUses = 0;
|
|
auto colorStarts = gsl::narrow_cast<uint16_t>(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<uint16_t>(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<uint16_t>(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<size_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<const uint16_t> charOffsets;
|
|
std::wstring_view chars;
|
|
|
|
if (sourceColBeg < sourceColLimit)
|
|
{
|
|
charOffsets = source._charOffsets.subspan(sourceColBeg, static_cast<size_t>(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<const uint16_t>& 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<uint16_t>(colLimit - colBeg), gsl::narrow<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<size_t>(UINT16_MAX, _chars.size() + (_chars.size() >> 1));
|
|
const auto newCapacity = gsl::narrow<uint16_t>(std::max(newLength, minCapacity));
|
|
|
|
auto charsHeap = std::make_unique_for_overwrite<wchar_t[]>(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<uint16_t>(*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<uint16_t> ROW::GetHyperlinks() const
|
|
{
|
|
std::vector<uint16_t> 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<til::CoordType>(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<til::CoordType>(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<size_t>(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<size_t>(colBeg));
|
|
const size_t chEnd = _uncheckedCharOffset(gsl::narrow_cast<size_t>(colEnd));
|
|
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
|
|
return { _chars.data() + chBeg, chEnd - chBeg };
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
uint16_t ROW::GetCharOffset(til::CoordType col) const noexcept
|
|
{
|
|
const auto columns = GetReadableColumnCount();
|
|
const auto colBeg = clamp(col, 0, columns);
|
|
return _uncheckedCharOffset(gsl::narrow_cast<size_t>(colBeg));
|
|
}
|
|
|
|
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<typename T>
|
|
constexpr uint16_t ROW::_clampedColumn(T v) const noexcept
|
|
{
|
|
return static_cast<uint16_t>(clamp(v, 0, _columnCount - 1));
|
|
}
|
|
|
|
template<typename T>
|
|
constexpr uint16_t ROW::_clampedColumnInclusive(T v) const noexcept
|
|
{
|
|
return static_cast<uint16_t>(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<typename T>
|
|
wchar_t ROW::_uncheckedChar(T off) const noexcept
|
|
{
|
|
return _chars[off];
|
|
}
|
|
|
|
// Safety: col must be [0, _columnCount].
|
|
template<typename T>
|
|
uint16_t ROW::_uncheckedCharOffset(T col) const noexcept
|
|
{
|
|
assert(col < _charOffsets.size());
|
|
return _charOffsets[col] & CharOffsetsMask;
|
|
}
|
|
|
|
// Safety: col must be [0, _columnCount].
|
|
template<typename T>
|
|
bool ROW::_uncheckedIsTrailer(T col) const noexcept
|
|
{
|
|
assert(col < _charOffsets.size());
|
|
return WI_IsFlagSet(_charOffsets[col], CharOffsetsTrailer);
|
|
}
|
|
|
|
template<typename T>
|
|
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<typename T>
|
|
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<ptrdiff_t>(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<til::CoordType>(clamp(offset, 0, _columnCount));
|
|
return CharToColumnMapper{ _chars.data(), _charOffsets.data(), lastChar, guessedColumn, _columnCount };
|
|
}
|
|
|
|
const std::optional<ScrollbarData>& ROW::GetScrollbarData() const noexcept
|
|
{
|
|
return _promptData;
|
|
}
|
|
void ROW::SetScrollbarData(std::optional<ScrollbarData> 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<unsigned int> error) noexcept
|
|
{
|
|
if (_promptData.has_value())
|
|
{
|
|
_promptData->exitCode = error;
|
|
if (error.has_value())
|
|
{
|
|
_promptData->category = *error == 0 ? MarkCategory::Success : MarkCategory::Error;
|
|
}
|
|
}
|
|
}
|