Skip to content

Commit a6c5e7a

Browse files
committed
Add an efficient text stream write function
1 parent fe2220e commit a6c5e7a

File tree

9 files changed

+436
-149
lines changed

9 files changed

+436
-149
lines changed

src/buffer/out/Row.cpp

+291-64
Large diffs are not rendered by default.

src/buffer/out/Row.hpp

+63-3
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ Revision History:
2020

2121
#pragma once
2222

23-
#include <span>
24-
2523
#include <til/rle.h>
2624

2725
#include "LineRendition.hpp"
@@ -37,6 +35,34 @@ enum class DelimiterClass
3735
RegularChar
3836
};
3937

38+
struct RowTextIterator
39+
{
40+
RowTextIterator(std::span<const wchar_t> chars, std::span<const uint16_t> charOffsets, uint16_t offset) noexcept;
41+
42+
bool operator==(const RowTextIterator& other) const noexcept;
43+
RowTextIterator& operator++() noexcept;
44+
const RowTextIterator& operator*() const noexcept;
45+
46+
std::wstring_view Text() const noexcept;
47+
til::CoordType Cols() const noexcept;
48+
DbcsAttribute DbcsAttr() const noexcept;
49+
50+
private:
51+
uint16_t _uncheckedCharOffset(size_t col) const noexcept;
52+
bool _uncheckedIsTrailer(size_t col) const noexcept;
53+
54+
// To simplify the detection of wide glyphs, we don't just store the simple character offset as described
55+
// for _charOffsets. Instead we use the most significant bit to indicate whether any column is the
56+
// trailing half of a wide glyph. This simplifies many implementation details via _uncheckedIsTrailer.
57+
static constexpr uint16_t CharOffsetsTrailer = 0x8000;
58+
static constexpr uint16_t CharOffsetsMask = 0x7fff;
59+
60+
std::span<const wchar_t> _chars;
61+
std::span<const uint16_t> _charOffsets;
62+
uint16_t _beg;
63+
uint16_t _end;
64+
};
65+
4066
class ROW final
4167
{
4268
public:
@@ -57,16 +83,23 @@ class ROW final
5783
bool WasDoubleBytePadded() const noexcept;
5884
void SetLineRendition(const LineRendition lineRendition) noexcept;
5985
LineRendition GetLineRendition() const noexcept;
86+
RowTextIterator Begin() const noexcept;
87+
RowTextIterator End() const noexcept;
6088

6189
void Reset(const TextAttribute& attr);
6290
void Resize(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute);
6391
void TransferAttributes(const til::small_rle<TextAttribute, uint16_t, 1>& attr, til::CoordType newWidth);
6492

93+
til::CoordType NavigateToPrevious(til::CoordType column) const noexcept;
94+
til::CoordType NavigateToNext(til::CoordType column) const noexcept;
95+
6596
void ClearCell(til::CoordType column);
6697
OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional<bool> wrap = std::nullopt, std::optional<til::CoordType> limitRight = std::nullopt);
6798
bool SetAttrToEnd(til::CoordType columnBegin, TextAttribute attr);
6899
void ReplaceAttributes(til::CoordType beginIndex, til::CoordType endIndex, const TextAttribute& newAttr);
69100
void ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars);
101+
til::CoordType Write(til::CoordType columnBegin, til::CoordType columnLimit, std::wstring_view& chars);
102+
til::CoordType WriteWithOffsets(til::CoordType columnBegin, til::CoordType columnLimit, std::wstring_view& chars, std::span<const uint16_t>& charOffsetsPtr);
70103

71104
const til::small_rle<TextAttribute, uint16_t, 1>& Attributes() const noexcept;
72105
TextAttribute GetAttrByColumn(til::CoordType column) const;
@@ -89,6 +122,30 @@ class ROW final
89122
#endif
90123

91124
private:
125+
// WriteHelper exists because other forms of abstracting this functionality away (like templates with lambdas)
126+
// where only very poorly optimized by MSVC as it failed to inline the templates.
127+
struct WriteHelper
128+
{
129+
explicit WriteHelper(ROW& row, til::CoordType columnBegin, til::CoordType columnLimit, const std::wstring_view& chars) noexcept;
130+
bool IsValid() const noexcept;
131+
void ReplaceCharacters(til::CoordType width) noexcept;
132+
void Write() noexcept;
133+
void WriteWithOffsets(const std::span<const uint16_t>& charOffsets) noexcept;
134+
void Finish();
135+
136+
ROW& row;
137+
const std::wstring_view& chars;
138+
uint16_t colBeg;
139+
uint16_t colLimit;
140+
uint16_t chExtBeg;
141+
uint16_t colExtBeg;
142+
uint16_t leadingSpaces;
143+
uint16_t chBeg;
144+
uint16_t colEnd;
145+
uint16_t colExtEnd;
146+
size_t charsConsumed;
147+
};
148+
92149
// To simplify the detection of wide glyphs, we don't just store the simple character offset as described
93150
// for _charOffsets. Instead we use the most significant bit to indicate whether any column is the
94151
// trailing half of a wide glyph. This simplifies many implementation details via _uncheckedIsTrailer.
@@ -102,13 +159,16 @@ class ROW final
102159
template<typename T>
103160
constexpr uint16_t _clampedColumnInclusive(T v) const noexcept;
104161

162+
uint16_t _adjustBackward(uint16_t column) const noexcept;
163+
uint16_t _adjustForward(uint16_t column) const noexcept;
164+
105165
wchar_t _uncheckedChar(size_t off) const noexcept;
106166
uint16_t _charSize() const noexcept;
107167
uint16_t _uncheckedCharOffset(size_t col) const noexcept;
108168
bool _uncheckedIsTrailer(size_t col) const noexcept;
109169

110170
void _init() noexcept;
111-
void _resizeChars(uint16_t colExtEnd, uint16_t chExtBeg, uint16_t chExtEnd, size_t chExtEndNew);
171+
void _resizeChars(uint16_t colExtEnd, uint16_t chExtBeg, uint16_t chExtEndOld, size_t chExtEndNew);
112172

113173
// These fields are a bit "wasteful", but it makes all this a bit more robust against
114174
// programming errors during initial development (which is when this comment was written).

src/buffer/out/textBuffer.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,18 @@ bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute
376376
return fSuccess;
377377
}
378378

379+
til::CoordType TextBuffer::Write(til::CoordType row, til::CoordType columnBegin, til::CoordType columnLimit, bool wrapAtEOL, const TextAttribute& attributes, std::wstring_view& chars)
380+
{
381+
auto& r = GetRowByOffset(row);
382+
383+
const auto columnEnd = r.Write(columnBegin, columnLimit, chars);
384+
r.ReplaceAttributes(columnBegin, columnEnd, attributes);
385+
r.SetWrapForced(wrapAtEOL && columnEnd == r.size());
386+
387+
TriggerRedraw(Viewport::FromExclusive({ columnBegin, row, columnEnd, row + 1 }));
388+
return columnEnd;
389+
}
390+
379391
// Routine Description:
380392
// - Writes cells to the output buffer. Writes at the cursor.
381393
// Arguments:

src/buffer/out/textBuffer.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ class TextBuffer final
8989
TextBufferTextIterator GetTextDataAt(const til::point at, const Microsoft::Console::Types::Viewport limit) const;
9090

9191
// Text insertion functions
92+
til::CoordType Write(til::CoordType row, til::CoordType columnBegin, til::CoordType columnLimit, bool wrapAtEOL, const TextAttribute& attributes, std::wstring_view& chars);
93+
9294
OutputCellIterator Write(const OutputCellIterator givenIt);
9395

9496
OutputCellIterator Write(const OutputCellIterator givenIt,

src/host/ut_host/TextBufferTests.cpp

+46
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ class TextBufferTests
147147

148148
TEST_METHOD(TestBurrito);
149149
TEST_METHOD(TestOverwriteChars);
150+
TEST_METHOD(TestRowWrite);
150151

151152
TEST_METHOD(TestAppendRTFText);
152153

@@ -2046,6 +2047,51 @@ void TextBufferTests::TestOverwriteChars()
20462047
#undef complex1
20472048
}
20482049

2050+
void TextBufferTests::TestRowWrite()
2051+
{
2052+
til::size bufferSize{ 10, 3 };
2053+
UINT cursorSize = 12;
2054+
TextAttribute attr{ 0x7f };
2055+
TextBuffer buffer{ bufferSize, attr, cursorSize, false, _renderer };
2056+
auto& row = buffer.GetRowByOffset(0);
2057+
std::wstring_view str;
2058+
til::CoordType pos = 0;
2059+
2060+
#define complex L"\U0001F41B"
2061+
2062+
// Not enough space -> early exit
2063+
str = complex;
2064+
pos = row.Write(2, 2, str);
2065+
VERIFY_ARE_EQUAL(2, pos);
2066+
VERIFY_ARE_EQUAL(complex, str);
2067+
VERIFY_ARE_EQUAL(L" ", row.GetText());
2068+
2069+
// Writing with the exact right amount of space
2070+
str = complex;
2071+
pos = row.Write(2, 4, str);
2072+
VERIFY_ARE_EQUAL(4, pos);
2073+
VERIFY_ARE_EQUAL(L"", str);
2074+
VERIFY_ARE_EQUAL(L" " complex L" ", row.GetText());
2075+
2076+
// Overwrite a wide character, but with not enough space left
2077+
str = complex complex;
2078+
pos = row.Write(0, 3, str);
2079+
// It's not quite clear what Write() should return in that case,
2080+
// so this simply asserts on what it happens to return right now.
2081+
VERIFY_ARE_EQUAL(4, pos);
2082+
VERIFY_ARE_EQUAL(complex, str);
2083+
VERIFY_ARE_EQUAL(complex L" ", row.GetText());
2084+
2085+
// Various text, too much to fit into the row
2086+
str = L"a" complex L"b" complex L"c" complex L"foo";
2087+
pos = row.Write(1, til::CoordTypeMax, str);
2088+
VERIFY_ARE_EQUAL(10, pos);
2089+
VERIFY_ARE_EQUAL(L"foo", str);
2090+
VERIFY_ARE_EQUAL(L" a" complex L"b" complex L"c" complex, row.GetText());
2091+
2092+
#undef complex
2093+
}
2094+
20492095
void TextBufferTests::TestAppendRTFText()
20502096
{
20512097
{

src/inc/test/CommonState.hpp

+7-36
Original file line numberDiff line numberDiff line change
@@ -256,49 +256,20 @@ class CommonState
256256
std::unique_ptr<TextBuffer> m_backupTextBufferInfo;
257257
std::unique_ptr<INPUT_READ_HANDLE_DATA> m_readHandle;
258258

259-
struct TestString
260-
{
261-
std::wstring_view string;
262-
bool wide = false;
263-
};
264-
265-
static void applyTestString(ROW* pRow, const auto& testStrings)
266-
{
267-
uint16_t x = 0;
268-
for (const auto& t : testStrings)
269-
{
270-
if (t.wide)
271-
{
272-
pRow->ReplaceCharacters(x, 2, t.string);
273-
x += 2;
274-
}
275-
else
276-
{
277-
for (const auto& ch : t.string)
278-
{
279-
pRow->ReplaceCharacters(x, 1, { &ch, 1 });
280-
x += 1;
281-
}
282-
}
283-
}
284-
}
285-
286259
void FillRow(ROW* pRow, bool wrapForced)
287260
{
288261
// fill a row
289262
// 9 characters, 6 spaces. 15 total
290263
// か = \x304b
291264
// き = \x304d
292265

293-
static constexpr std::array testStrings{
294-
TestString{ L"AB" },
295-
TestString{ L"\x304b", true },
296-
TestString{ L"C" },
297-
TestString{ L"\x304d", true },
298-
TestString{ L"DE " },
299-
};
300-
301-
applyTestString(pRow, testStrings);
266+
uint16_t column = 0;
267+
for (const auto& ch : std::wstring_view{ L"AB\u304bC\u304dDE " })
268+
{
269+
const uint16_t width = ch >= 0x80 ? 2 : 1;
270+
pRow->ReplaceCharacters(column, width, { &ch, 1 });
271+
column += width;
272+
}
302273

303274
// A = bright red on dark gray
304275
// This string starts at index 0

src/inc/til/point.h

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
namespace til // Terminal Implementation Library. Also: "Today I Learned"
77
{
88
using CoordType = int32_t;
9+
inline constexpr CoordType CoordTypeMin = INT32_MIN;
10+
inline constexpr CoordType CoordTypeMax = INT32_MAX;
911

1012
namespace details
1113
{

src/terminal/adapter/adaptDispatch.cpp

+9-23
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
8282

8383
// The width at which we wrap is determined by the line rendition attribute.
8484
auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y);
85+
auto stringIterator = string;
8586

86-
auto stringPosition = string.cbegin();
87-
while (stringPosition < string.cend())
87+
while (!stringIterator.empty())
8888
{
8989
if (cursor.IsDelayedEOLWrap() && wrapAtEOL)
9090
{
@@ -101,45 +101,31 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
101101
}
102102
}
103103

104-
const OutputCellIterator it(std::wstring_view{ stringPosition, string.cend() }, attributes);
105104
if (_modes.test(Mode::InsertReplace))
106105
{
107106
// If insert-replace mode is enabled, we first measure how many cells
108107
// the string will occupy, and scroll the target area right by that
109108
// amount to make space for the incoming text.
109+
const OutputCellIterator it(stringIterator, attributes);
110110
auto measureIt = it;
111111
while (measureIt && measureIt.GetCellDistance(it) < lineWidth)
112112
{
113-
measureIt++;
113+
++measureIt;
114114
}
115115
const auto row = cursorPosition.y;
116116
const auto cellCount = measureIt.GetCellDistance(it);
117117
_ScrollRectHorizontally(textBuffer, { cursorPosition.x, row, lineWidth, row + 1 }, cellCount);
118118
}
119-
const auto itEnd = textBuffer.WriteLine(it, cursorPosition, wrapAtEOL, lineWidth - 1);
120119

121-
if (itEnd.GetInputDistance(it) == 0)
122-
{
123-
// If we haven't written anything out because there wasn't enough space,
124-
// we move the cursor to the end of the line so that it's forced to wrap.
125-
cursorPosition.x = lineWidth;
126-
// But if wrapping is disabled, we also need to move to the next string
127-
// position, otherwise we'll be stuck in this loop forever.
128-
if (!wrapAtEOL)
129-
{
130-
stringPosition++;
131-
}
132-
}
133-
else
120+
const auto newPosX = textBuffer.Write(cursorPosition.y, cursorPosition.x, til::CoordTypeMax, wrapAtEOL, attributes, stringIterator);
121+
122+
if (const til::rect changedRect{ cursorPosition.x, cursorPosition.y, newPosX, cursorPosition.y + 1 })
134123
{
135-
const auto cellCount = itEnd.GetCellDistance(it);
136-
const auto changedRect = til::rect{ cursorPosition, til::size{ cellCount, 1 } };
137124
_api.NotifyAccessibilityChange(changedRect);
138-
139-
stringPosition += itEnd.GetInputDistance(it);
140-
cursorPosition.x += cellCount;
141125
}
142126

127+
cursorPosition.x = newPosX;
128+
143129
if (cursorPosition.x >= lineWidth)
144130
{
145131
// If we're past the end of the line, we need to clamp the cursor

tools/ConsoleTypes.natvis

+4-23
Original file line numberDiff line numberDiff line change
@@ -38,31 +38,12 @@
3838
<DisplayString>{{LT({Left}, {Top}) RB({Right}, {Bottom}) In:[{Right-Left+1} x {Bottom-Top+1}] Ex:[{Right-Left} x {Bottom-Top}]}}</DisplayString>
3939
</Type>
4040

41-
<Type Name="CharRowCell">
42-
<DisplayString Condition="_attr._glyphStored">Stored Glyph, go to UnicodeStorage.</DisplayString>
43-
<DisplayString Condition="_attr._attribute == 0">{_wch,X} Single</DisplayString>
44-
<DisplayString Condition="_attr._attribute == 1">{_wch,X} Lead</DisplayString>
45-
<DisplayString Condition="_attr._attribute == 2">{_wch,X} Trail</DisplayString>
46-
</Type>
47-
48-
<Type Name="ATTR_ROW">
49-
<Expand>
50-
<ExpandedItem>_data</ExpandedItem>
51-
</Expand>
52-
</Type>
53-
54-
<Type Name="CharRow">
55-
<DisplayString>{{ wrap={_wrapForced} padded={_doubleBytePadded} }}</DisplayString>
56-
<Expand>
57-
<ExpandedItem>_data</ExpandedItem>
58-
</Expand>
59-
</Type>
60-
6141
<Type Name="ROW">
62-
<DisplayString>{{ id={_id} width={_rowWidth} }}</DisplayString>
42+
<DisplayString>{_chars.data(),[_charOffsets[_columnCount]]}</DisplayString>
43+
<StringView>_chars.data(),[_charOffsets[_columnCount]]</StringView>
6344
<Expand>
64-
<Item Name="_charRow">_charRow</Item>
65-
<Item Name="_attrRow">_attrRow</Item>
45+
<Item Name="_chars">_chars.data(),[_charOffsets[_columnCount]]</Item>
46+
<Item Name="_charOffsets">_charOffsets.data(),[_charOffsets.size()]</Item>
6647
</Expand>
6748
</Type>
6849

0 commit comments

Comments
 (0)