Files
terminal/src/buffer/out/textBuffer.cpp
Dustin L. Howett f8b4e19e51 avoid committing rows just to figure out rendition, blink and cursor (#19984)
We have been looking at a set of conhost crashes due to low-memory
conditions. I've observed that they all happen during the first pass of
rendering, before there's even an engine set up, when we try to estimate
whether there are any blink attributes. I also found that we'll commit
the buffer to check the cursor's double-width status and whether it's on
a double-width-rendition line.

Uncommitted rows _never_ contain a blinker, are never double-width and
never contain DBCS.

We can't necessarily avoid committing an empty buffer _forever,_ but
this at least moves the first commit until after the renderer truly
starts.

This prevents us from committing an empty buffer for headless console
apps (!)
2026-03-18 17:52:59 -05:00

3561 lines
138 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "textBuffer.hpp"
#include <til/hash.h>
#include "UTextAdapter.h"
#include "../../types/inc/CodepointWidthDetector.hpp"
#include "../renderer/base/renderer.hpp"
#include "../types/inc/utils.hpp"
#include <til/regex.h>
#include "search.h"
// BODGY: Misdiagnosis in MSVC 17.11: Referencing global constants in the member
// initializer list leads to this warning. Can probably be removed in the future.
#pragma warning(disable : 26493) // Don't use C-style casts (type.4).)
using namespace Microsoft::Console;
using namespace Microsoft::Console::Types;
using PointTree = interval_tree::IntervalTree<til::point, size_t>;
constexpr bool allWhitespace(const std::wstring_view& text) noexcept
{
for (const auto ch : text)
{
if (ch != L' ')
{
return false;
}
}
return true;
}
static std::atomic<uint64_t> s_lastMutationIdInitialValue;
// Routine Description:
// - Creates a new instance of TextBuffer
// Arguments:
// - screenBufferSize - The X by Y dimensions of the new screen buffer
// - defaultAttributes - The attributes with which the buffer will be initialized
// - cursorSize - The height of the cursor within this buffer
// - isActiveBuffer - Whether this is the currently active buffer
// - renderer - The renderer to use for triggering a redraw
// Return Value:
// - constructed object
// Note: may throw exception
TextBuffer::TextBuffer(til::size screenBufferSize,
const TextAttribute defaultAttributes,
const UINT cursorSize,
const bool isActiveBuffer,
Microsoft::Console::Render::Renderer* renderer) :
_renderer{ renderer },
_currentAttributes{ defaultAttributes },
// This way every TextBuffer will start with a ""unique"" _lastMutationId
// and so it'll compare unequal with the counter of other TextBuffers.
_lastMutationId{ s_lastMutationIdInitialValue.fetch_add(0x100000000) },
_cursor{ cursorSize, *this },
_isActiveBuffer{ isActiveBuffer }
{
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
screenBufferSize.width = std::max(screenBufferSize.width, 1);
screenBufferSize.height = std::max(screenBufferSize.height, 1);
_reserve(screenBufferSize, defaultAttributes);
}
TextBuffer::~TextBuffer()
{
if (_buffer)
{
_destroy();
}
}
// I put these functions in a block at the start of the class, because they're the most
// fundamental aspect of TextBuffer: It implements the basic gap buffer text storage.
// It's also fairly tricky code.
#pragma region buffer management
#pragma warning(push)
#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).
// MEM_RESERVEs memory sufficient to store height-many ROW structs,
// as well as their ROW::_chars and ROW::_charOffsets buffers.
//
// We use explicit virtual memory allocations to not taint the general purpose allocator
// with our huge allocation, as well as to be able to reduce the private working set of
// the application by only committing what we actually need. This reduces conhost's
// memory usage from ~7MB down to just ~2MB at startup in the general case.
void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes)
{
const auto w = gsl::narrow<uint16_t>(screenBufferSize.width);
const auto h = gsl::narrow<uint16_t>(screenBufferSize.height);
constexpr auto rowSize = ROW::CalculateRowSize();
const auto charsBufferSize = ROW::CalculateCharsBufferSize(w);
const auto charOffsetsBufferSize = ROW::CalculateCharOffsetsBufferSize(w);
const auto rowStride = rowSize + charsBufferSize + charOffsetsBufferSize;
assert(rowStride % alignof(ROW) == 0);
// 65535*65535 cells would result in a allocSize of 8GiB.
// --> Use uint64_t so that we can safely do our calculations even on x86.
// We allocate 1 additional row, which will be used for GetScratchpadRow().
const auto rowCount = ::base::strict_cast<uint64_t>(h) + 1;
const auto allocSize = gsl::narrow<size_t>(rowCount * rowStride);
// NOTE: Modifications to this block of code might have to be mirrored over to ResizeTraditional().
// It constructs a temporary TextBuffer and then extracts the members below, overwriting itself.
_buffer = wil::unique_virtualalloc_ptr<std::byte>{
static_cast<std::byte*>(THROW_LAST_ERROR_IF_NULL(VirtualAlloc(nullptr, allocSize, MEM_RESERVE, PAGE_READWRITE)))
};
_bufferEnd = _buffer.get() + allocSize;
_commitWatermark = _buffer.get();
_initialAttributes = defaultAttributes;
_bufferRowStride = rowStride;
_bufferOffsetChars = rowSize;
_bufferOffsetCharOffsets = rowSize + charsBufferSize;
_width = w;
_height = h;
}
// MEM_COMMITs the memory and constructs all ROWs up to and including the given row pointer.
// It's expected that the caller verifies the parameter. It goes hand in hand with _getRowByOffsetDirect().
//
// Declaring this function as noinline allows _getRowByOffsetDirect() to be inlined,
// which improves overall TextBuffer performance by ~6%. And all it cost is this annotation.
// The compiler doesn't understand the likelihood of our branches. (PGO does, but that's imperfect.)
__declspec(noinline) void TextBuffer::_commit(const std::byte* row)
{
assert(row >= _commitWatermark);
const auto rowEnd = row + _bufferRowStride;
const auto remaining = gsl::narrow_cast<uintptr_t>(_bufferEnd - _commitWatermark);
const auto minimum = gsl::narrow_cast<uintptr_t>(rowEnd - _commitWatermark);
const auto ideal = minimum + _bufferRowStride * _commitReadAheadRowCount;
const auto size = std::min(remaining, ideal);
THROW_LAST_ERROR_IF_NULL(VirtualAlloc(_commitWatermark, size, MEM_COMMIT, PAGE_READWRITE));
_construct(_commitWatermark + size);
}
// Destructs and MEM_DECOMMITs all previously constructed ROWs.
// You can use this (or rather the Reset() method) to fully clear the TextBuffer.
void TextBuffer::_decommit() noexcept
{
_destroy();
VirtualFree(_buffer.get(), 0, MEM_DECOMMIT);
_commitWatermark = _buffer.get();
}
// Constructs ROWs between [_commitWatermark,until).
void TextBuffer::_construct(const std::byte* until) noexcept
{
for (; _commitWatermark < until; _commitWatermark += _bufferRowStride)
{
const auto row = reinterpret_cast<ROW*>(_commitWatermark);
const auto chars = reinterpret_cast<wchar_t*>(_commitWatermark + _bufferOffsetChars);
const auto indices = reinterpret_cast<uint16_t*>(_commitWatermark + _bufferOffsetCharOffsets);
std::construct_at(row, chars, indices, _width, _initialAttributes);
}
}
// Destructs ROWs between [_buffer,_commitWatermark).
void TextBuffer::_destroy() const noexcept
{
for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride)
{
std::destroy_at(reinterpret_cast<ROW*>(it));
}
}
// This function is "direct" because it trusts the caller to properly
// wrap the "offset" parameter modulo the _height of the buffer.
ROW& TextBuffer::_getRowByOffsetDirect(size_t offset)
{
const auto row = _buffer.get() + _bufferRowStride * offset;
THROW_HR_IF(E_UNEXPECTED, row < _buffer.get() || row >= _bufferEnd);
if (row >= _commitWatermark)
{
_commit(row);
}
return *reinterpret_cast<ROW*>(row);
}
// See GetRowByOffset().
ROW& TextBuffer::_getRow(til::CoordType y) const
{
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
auto offset = (_firstRow + y) % _height;
// Support negative wrap around. This way an index of -1 will
// wrap to _rowCount-1 and make implementing scrolling easier.
if (offset < 0)
{
offset += _height;
}
// We add 1 to the row offset, because row "0" is the one returned by GetScratchpadRow().
// See GetScratchpadRow() for more explanation.
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3).
return const_cast<TextBuffer*>(this)->_getRowByOffsetDirect(gsl::narrow_cast<size_t>(offset) + 1);
}
// Returns the "user-visible" index of the last committed row, which can be used
// to short-circuit some algorithms that try to scan the entire buffer.
// Returns 0 if no rows are committed in.
til::CoordType TextBuffer::_estimateOffsetOfLastCommittedRow() const noexcept
{
const auto lastRowOffset = (_commitWatermark - _buffer.get()) / _bufferRowStride;
// This subtracts 2 from the offset to account for the:
// * scratchpad row at offset 0, whereas regular rows start at offset 1.
// * fact that _commitWatermark points _past_ the last committed row,
// but we want to return an index pointing at the last row.
return std::max(0, gsl::narrow_cast<til::CoordType>(lastRowOffset - 2));
}
bool TextBuffer::_isRowCommitted(til::CoordType y) const noexcept
{
auto offset = (_firstRow + y + 1 /* account for the scratch row */) % _height;
if (offset < 0)
{
offset += _height;
}
const auto row = _buffer.get() + _bufferRowStride * offset;
return row < _commitWatermark;
}
// Retrieves a row from the buffer by its offset from the first row of the text buffer
// (what corresponds to the top row of the screen buffer).
const ROW& TextBuffer::GetRowByOffset(const til::CoordType index) const
{
return _getRow(index);
}
// Retrieves a row from the buffer by its offset from the first row of the text buffer
// (what corresponds to the top row of the screen buffer).
ROW& TextBuffer::GetMutableRowByOffset(const til::CoordType index)
{
_lastMutationId++;
return _getRow(index);
}
// Returns a row filled with whitespace and the current attributes, for you to freely use.
ROW& TextBuffer::GetScratchpadRow()
{
return GetScratchpadRow(_currentAttributes);
}
// Returns a row filled with whitespace and the given attributes, for you to freely use.
ROW& TextBuffer::GetScratchpadRow(const TextAttribute& attributes)
{
// The scratchpad row is mapped to the underlying index 0, whereas all regular rows are mapped to
// index 1 and up. We do it this way instead of the other way around (scratchpad row at index _height),
// because that would force us to MEM_COMMIT the entire buffer whenever this function is called.
auto& r = _getRowByOffsetDirect(0);
r.Reset(attributes);
return r;
}
#pragma warning(pop)
#pragma endregion
// Routine Description:
// - Copies properties from another text buffer into this one.
// - This is primarily to copy properties that would otherwise not be specified during CreateInstance
// Arguments:
// - OtherBuffer - The text buffer to copy properties from
// Return Value:
// - <none>
void TextBuffer::CopyProperties(const TextBuffer& OtherBuffer) noexcept
{
GetCursor().CopyProperties(OtherBuffer.GetCursor());
}
// Routine Description:
// - Gets the number of rows in the buffer
// Arguments:
// - <none>
// Return Value:
// - Total number of rows in the buffer
til::CoordType TextBuffer::TotalRowCount() const noexcept
{
return _height;
}
// Method Description:
// - Gets the number of glyphs in the buffer between two points.
// - IMPORTANT: Make sure that start is before end, or this will never return!
// Arguments:
// - start - The starting point of the range to get the glyph count for.
// - end - The ending point of the range to get the glyph count for.
// Return Value:
// - The number of glyphs in the buffer between the two points.
size_t TextBuffer::GetCellDistance(const til::point from, const til::point to) const
{
auto startCell = GetCellDataAt(from);
const auto endCell = GetCellDataAt(to);
auto delta = 0;
while (startCell != endCell)
{
++startCell;
++delta;
}
return delta;
}
// Routine Description:
// - Retrieves read-only text iterator at the given buffer location
// Arguments:
// - at - X,Y position in buffer for iterator start position
// Return Value:
// - Read-only iterator of text data only.
TextBufferTextIterator TextBuffer::GetTextDataAt(const til::point at) const
{
return TextBufferTextIterator(GetCellDataAt(at));
}
// Routine Description:
// - Retrieves read-only cell iterator at the given buffer location
// Arguments:
// - at - X,Y position in buffer for iterator start position
// Return Value:
// - Read-only iterator of cell data.
TextBufferCellIterator TextBuffer::GetCellDataAt(const til::point at) const
{
return TextBufferCellIterator(*this, at);
}
// Routine Description:
// - Retrieves read-only text iterator at the given buffer location
// but restricted to only the specific line (Y coordinate).
// Arguments:
// - at - X,Y position in buffer for iterator start position
// Return Value:
// - Read-only iterator of text data only.
TextBufferTextIterator TextBuffer::GetTextLineDataAt(const til::point at) const
{
return TextBufferTextIterator(GetCellLineDataAt(at));
}
// Routine Description:
// - Retrieves read-only cell iterator at the given buffer location
// but restricted to only the specific line (Y coordinate).
// Arguments:
// - at - X,Y position in buffer for iterator start position
// Return Value:
// - Read-only iterator of cell data.
TextBufferCellIterator TextBuffer::GetCellLineDataAt(const til::point at) const
{
til::inclusive_rect limit;
limit.top = at.y;
limit.bottom = at.y;
limit.left = 0;
limit.right = GetSize().RightInclusive();
return TextBufferCellIterator(*this, at, Viewport::FromInclusive(limit));
}
// Routine Description:
// - Retrieves read-only text iterator at the given buffer location
// but restricted to operate only inside the given viewport.
// Arguments:
// - at - X,Y position in buffer for iterator start position
// - limit - boundaries for the iterator to operate within
// Return Value:
// - Read-only iterator of text data only.
TextBufferTextIterator TextBuffer::GetTextDataAt(const til::point at, const Viewport limit) const
{
return TextBufferTextIterator(GetCellDataAt(at, limit));
}
// Routine Description:
// - Retrieves read-only cell iterator at the given buffer location
// but restricted to operate only inside the given viewport.
// Arguments:
// - at - X,Y position in buffer for iterator start position
// - limit - boundaries for the iterator to operate within
// Return Value:
// - Read-only iterator of cell data.
TextBufferCellIterator TextBuffer::GetCellDataAt(const til::point at, const Viewport limit) const
{
return TextBufferCellIterator(*this, at, limit);
}
// Given the character offset `position` in the `chars` string, this function returns the starting position of the next grapheme.
// For instance, given a `chars` of L"x\uD83D\uDE42y" and a `position` of 1 it'll return 3.
// GraphemePrev would do the exact inverse of this operation.
size_t TextBuffer::GraphemeNext(const std::wstring_view& chars, size_t position) noexcept
{
auto& cwd = CodepointWidthDetector::Singleton();
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
GraphemeState state{ .beg = chars.data() + position };
cwd.GraphemeNext(state, chars);
return position + state.len;
}
// It's the counterpart to GraphemeNext. See GraphemeNext.
size_t TextBuffer::GraphemePrev(const std::wstring_view& chars, size_t position) noexcept
{
auto& cwd = CodepointWidthDetector::Singleton();
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
GraphemeState state{ .beg = chars.data() + position };
cwd.GraphemePrev(state, chars);
return position - state.len;
}
// Ever wondered how much space a piece of text needs before inserting it? This function will tell you!
// It fundamentally behaves identical to the various ROW functions around `RowWriteState`.
//
// Set `columnLimit` to the amount of space that's available (e.g. `buffer_width - cursor_position.x`)
// and it'll return the amount of characters that fit into this space. The out parameter `columns`
// will contain the amount of columns this piece of text has actually used.
//
// Just like with `RowWriteState` one special case is when not all text fits into the given space:
// In that case, this function always returns exactly `columnLimit`. This distinction is important when "inserting"
// a wide glyph but there's only 1 column left. That 1 remaining column is supposed to be padded with whitespace.
size_t TextBuffer::FitTextIntoColumns(const std::wstring_view& chars, til::CoordType columnLimit, til::CoordType& columns) noexcept
{
columnLimit = std::max(0, columnLimit);
const auto beg = chars.begin();
const auto end = chars.end();
auto it = beg;
const auto asciiEnd = beg + std::min(chars.size(), gsl::narrow_cast<size_t>(columnLimit));
// ASCII fast-path: 1 char always corresponds to 1 column.
for (; it != asciiEnd && *it < 0x80; ++it)
{
}
auto dist = gsl::narrow_cast<size_t>(it - beg);
auto col = gsl::narrow_cast<til::CoordType>(dist);
if (it == asciiEnd) [[likely]]
{
columns = col;
return dist;
}
// Unicode slow-path where we need to count text and columns separately.
auto& cwd = CodepointWidthDetector::Singleton();
const auto len = chars.size();
// 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 GraphemeNext() find the next proper grapheme boundary.
if (dist != 0)
{
dist--;
col--;
}
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
GraphemeState state{ .beg = chars.data() + dist };
while (dist < len)
{
cwd.GraphemeNext(state, chars);
col += state.width;
if (col > columnLimit)
{
break;
}
dist += state.len;
}
// But if we simply ran out of text we just need to return the actual number of columns.
columns = col;
return dist;
}
// Pretend as if `position` is a regular cursor in the TextBuffer.
// This function will then pretend as if you pressed the left/right arrow
// keys `distance` amount of times (negative = left, positive = right).
til::point TextBuffer::NavigateCursor(til::point position, til::CoordType distance) const
{
const til::CoordType maxX = _width - 1;
const til::CoordType maxY = _height - 1;
auto x = std::clamp(position.x, 0, maxX);
auto y = std::clamp(position.y, 0, maxY);
auto row = &GetRowByOffset(y);
if (distance < 0)
{
do
{
if (x > 0)
{
x = row->NavigateToPrevious(x);
}
else if (y <= 0)
{
break;
}
else
{
--y;
row = &GetRowByOffset(y);
x = row->GetReadableColumnCount() - 1;
}
} while (++distance != 0);
}
else if (distance > 0)
{
auto rowWidth = row->GetReadableColumnCount();
do
{
if (x < rowWidth)
{
x = row->NavigateToNext(x);
}
else if (y >= maxY)
{
break;
}
else
{
++y;
row = &GetRowByOffset(y);
rowWidth = row->GetReadableColumnCount();
x = 0;
}
} while (--distance != 0);
}
return { x, y };
}
// This function is intended for writing regular "lines" of text as it'll set the wrap flag on the given row.
// You can continue calling the function on the same row as long as state.columnEnd < state.columnLimit.
void TextBuffer::Replace(til::CoordType row, const TextAttribute& attributes, RowWriteState& state)
{
auto& r = GetMutableRowByOffset(row);
r.ReplaceText(state);
r.ReplaceAttributes(state.columnBegin, state.columnEnd, attributes);
ImageSlice::EraseCells(r, state.columnBegin, state.columnEnd);
TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, state.columnEndDirty, row + 1 }));
}
void TextBuffer::Insert(til::CoordType row, const TextAttribute& attributes, RowWriteState& state)
{
auto& r = GetMutableRowByOffset(row);
auto& scratch = GetScratchpadRow();
scratch.CopyFrom(r);
r.ReplaceText(state);
r.ReplaceAttributes(state.columnBegin, state.columnEnd, attributes);
// Restore trailing text from our backup in scratch.
RowWriteState restoreState{
.text = scratch.GetText(state.columnBegin, state.columnLimit),
.columnBegin = state.columnEnd,
.columnLimit = state.columnLimit,
};
r.ReplaceText(restoreState);
// Restore trailing attributes as well.
if (const auto copyAmount = restoreState.columnEnd - restoreState.columnBegin; copyAmount > 0)
{
auto& rowAttr = r.Attributes();
const auto& scratchAttr = scratch.Attributes();
const auto restoreAttr = scratchAttr.slice(gsl::narrow<uint16_t>(state.columnBegin), gsl::narrow<uint16_t>(state.columnBegin + copyAmount));
rowAttr.replace(gsl::narrow<uint16_t>(restoreState.columnBegin), gsl::narrow<uint16_t>(restoreState.columnEnd), restoreAttr);
// If there is any image content, that needs to be copied too.
ImageSlice::CopyCells(r, state.columnBegin, r, restoreState.columnBegin, restoreState.columnEnd);
}
// Image content at the insert position needs to be erased.
ImageSlice::EraseCells(r, state.columnBegin, restoreState.columnBegin);
TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, row, restoreState.columnEndDirty, row + 1 }));
}
// Fills an area of the buffer with a given fill character(s) and attributes.
void TextBuffer::FillRect(const til::rect& rect, const std::wstring_view& fill, const TextAttribute& attributes)
{
if (!rect || fill.empty())
{
return;
}
auto& scratchpad = GetScratchpadRow(attributes);
// The scratchpad row gets reset to whitespace by default, so there's no need to
// initialize it again. Filling with whitespace is the most common operation by far.
if (fill != L" ")
{
RowWriteState state{
.columnLimit = rect.right,
.columnEnd = rect.left,
};
// Fill the scratchpad row with consecutive copies of "fill" up to the amount we need.
//
// We don't just create a single string with N copies of "fill" and write that at once,
// because that might join neighboring combining marks unintentionally.
//
// Building the buffer this way is very wasteful and slow, but it's still 3x
// faster than what we had before and no one complained about that either.
// It's seldom used code and probably not worth optimizing for.
while (state.columnEnd < rect.right)
{
state.columnBegin = state.columnEnd;
state.text = fill;
scratchpad.ReplaceText(state);
}
}
// Fill the given rows with copies of the scratchpad row. That's a little
// slower when filling just a single row, but will be much faster for >1 rows.
{
RowCopyTextFromState state{
.source = scratchpad,
.columnBegin = rect.left,
.columnLimit = rect.right,
.sourceColumnBegin = rect.left,
};
for (auto y = rect.top; y < rect.bottom; ++y)
{
auto& r = GetMutableRowByOffset(y);
r.CopyTextFrom(state);
r.ReplaceAttributes(rect.left, rect.right, attributes);
ImageSlice::EraseCells(r, rect.left, rect.right);
TriggerRedraw(Viewport::FromExclusive({ state.columnBeginDirty, y, state.columnEndDirty, y + 1 }));
}
}
}
// Routine Description:
// - Writes cells to the output buffer. Writes at the cursor.
// Arguments:
// - givenIt - Iterator representing output cell data to write
// Return Value:
// - The final position of the iterator
OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt)
{
const auto& cursor = GetCursor();
const auto target = cursor.GetPosition();
const auto finalIt = Write(givenIt, target);
return finalIt;
}
// Routine Description:
// - Writes cells to the output buffer.
// Arguments:
// - givenIt - Iterator representing output cell data to write
// - target - the row/column to start writing the text to
// - wrap - change the wrap flag if we hit the end of the row while writing and there's still more data
// Return Value:
// - The final position of the iterator
OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt,
const til::point target,
const std::optional<bool> wrap)
{
// Make mutable copy so we can walk.
auto it = givenIt;
// Make mutable target so we can walk down lines.
auto lineTarget = target;
// Get size of the text buffer so we can stay in bounds.
const auto size = GetSize();
// While there's still data in the iterator and we're still targeting in bounds...
while (it && size.IsInBounds(lineTarget))
{
// Attempt to write as much data as possible onto this line.
// NOTE: if wrap = true/false, we want to set the line's wrap to true/false (respectively) if we reach the end of the line
it = WriteLine(it, lineTarget, wrap);
// Move to the next line down.
lineTarget.x = 0;
++lineTarget.y;
}
return it;
}
// Routine Description:
// - Writes one line of text to the output buffer.
// Arguments:
// - givenIt - The iterator that will dereference into cell data to insert
// - target - Coordinate targeted within output buffer
// - 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 - Optionally restrict the right boundary for writing (e.g. stop writing earlier than the end of line)
// Return Value:
// - The iterator, but advanced to where we stopped writing. Use to find input consumed length or cells written length.
OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt,
const til::point target,
const std::optional<bool> wrap,
std::optional<til::CoordType> limitRight)
{
// If we're not in bounds, exit early.
if (!GetSize().IsInBounds(target))
{
return givenIt;
}
// Get the row and write the cells
auto& row = GetMutableRowByOffset(target.y);
const auto newIt = row.WriteCells(givenIt, target.x, wrap, limitRight);
// Take the cell distance written and notify that it needs to be repainted.
const auto written = newIt.GetCellDistance(givenIt);
const auto paint = Viewport::FromDimensions(target, { written, 1 });
TriggerRedraw(paint);
return newIt;
}
//Routine Description:
// - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable.
//Arguments:
// - fillAttributes - the attributes with which the recycled row will be initialized.
//Return Value:
// - true if we successfully incremented the buffer.
void TextBuffer::IncrementCircularBuffer(const TextAttribute& fillAttributes)
{
// Prune hyperlinks to delete obsolete references
_PruneHyperlinks();
// Second, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed.
GetMutableRowByOffset(0).Reset(fillAttributes);
{
// Now proceed to increment.
// Incrementing it will cause the next line down to become the new "top" of the window (the new "0" in logical coordinates)
_firstRow++;
// If we pass up the height of the buffer, loop back to 0.
if (_firstRow >= GetSize().Height())
{
_firstRow = 0;
}
}
}
//Routine Description:
// - Retrieves the position of the last non-space character in the given
// viewport
// - By default, we search the entire buffer to find the last non-space
// character.
// - If we know the last character is within the given viewport (so we don't
// need to check the entire buffer), we can provide a value in viewOptional
// that we'll use to search for the last character in.
//Arguments:
// - The viewport
//Return value:
// - Coordinate position (relative to the text buffer)
til::point TextBuffer::GetLastNonSpaceCharacter(const Viewport* viewOptional) const
{
const auto viewport = viewOptional ? *viewOptional : GetSize();
til::point coordEndOfText;
// Search the given viewport by starting at the bottom.
coordEndOfText.y = std::min(viewport.BottomInclusive(), _estimateOffsetOfLastCommittedRow());
const auto& currRow = GetRowByOffset(coordEndOfText.y);
// The X position of the end of the valid text is the Right draw boundary (which is one beyond the final valid character)
coordEndOfText.x = currRow.MeasureRight() - 1;
// If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text.
const auto viewportTop = viewport.Top();
// while (this row is empty, and we're not at the top)
while (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop)
{
coordEndOfText.y--;
const auto& backupRow = GetRowByOffset(coordEndOfText.y);
// We need to back up to the previous row if this line is empty, AND there are more rows
coordEndOfText.x = backupRow.MeasureRight() - 1;
}
// don't allow negative results
coordEndOfText.y = std::max(coordEndOfText.y, 0);
coordEndOfText.x = std::max(coordEndOfText.x, 0);
return coordEndOfText;
}
const til::CoordType TextBuffer::GetFirstRowIndex() const noexcept
{
return _firstRow;
}
const Viewport TextBuffer::GetSize() const noexcept
{
return Viewport::FromDimensions({}, { _width, _height });
}
void TextBuffer::_SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept
{
_firstRow = FirstRowIndex;
}
void TextBuffer::ScrollRows(const til::CoordType firstRow, til::CoordType size, const til::CoordType delta)
{
if (delta == 0)
{
return;
}
// Since the for() loop uses !=, we must ensure that size is positive.
// A negative size doesn't make any sense anyways.
size = std::max(0, size);
til::CoordType y = 0;
til::CoordType end = 0;
til::CoordType step = 0;
if (delta < 0)
{
// The layout is like this:
// delta is -2, size is 3, firstRow is 5
// We want 3 rows from 5 (5, 6, and 7) to move up 2 spots.
// --- (storage) ----
// | 0 begin
// | 1
// | 2
// | 3 A. firstRow + delta (because delta is negative)
// | 4
// | 5 B. firstRow
// | 6
// | 7
// | 8 C. firstRow + size
// | 9
// | 10
// | 11
// - end
// We want B to slide up to A (the negative delta) and everything from [B,C) to slide up with it.
y = firstRow;
end = firstRow + size;
step = 1;
}
else
{
// The layout is like this:
// delta is 2, size is 3, firstRow is 5
// We want 3 rows from 5 (5, 6, and 7) to move down 2 spots.
// --- (storage) ----
// | 0 begin
// | 1
// | 2
// | 3
// | 4
// | 5 A. firstRow
// | 6
// | 7
// | 8 B. firstRow + size
// | 9
// | 10 C. firstRow + size + delta
// | 11
// - end
// We want B-1 to slide down to C-1 (the positive delta) and everything from [A, B) to slide down with it.
y = firstRow + size - 1;
end = firstRow - 1;
step = -1;
}
for (; y != end; y += step)
{
CopyRow(y, y + delta, *this);
}
}
void TextBuffer::CopyRow(const til::CoordType srcRowIndex, const til::CoordType dstRowIndex, TextBuffer& dstBuffer) const
{
auto& dstRow = dstBuffer.GetMutableRowByOffset(dstRowIndex);
const auto& srcRow = GetRowByOffset(srcRowIndex);
dstRow.CopyFrom(srcRow);
ImageSlice::CopyRow(srcRow, dstRow);
}
Cursor& TextBuffer::GetCursor() noexcept
{
return _cursor;
}
const Cursor& TextBuffer::GetCursor() const noexcept
{
return _cursor;
}
uint64_t TextBuffer::GetLastMutationId() const noexcept
{
return _lastMutationId;
}
const TextAttribute& TextBuffer::GetCurrentAttributes() const noexcept
{
return _currentAttributes;
}
void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept
{
_currentAttributes = currentAttributes;
}
void TextBuffer::SetWrapForced(const til::CoordType y, bool wrap)
{
GetMutableRowByOffset(y).SetWrapForced(wrap);
}
void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes)
{
const auto cursorPosition = GetCursor().GetPosition();
const auto rowIndex = cursorPosition.y;
auto& row = GetMutableRowByOffset(rowIndex);
if (row.GetLineRendition() != lineRendition)
{
row.SetLineRendition(lineRendition);
// If the line rendition has changed, the row can no longer be wrapped.
row.SetWrapForced(false);
// And all image content on the row is removed.
row.SetImageSlice(nullptr);
// And if it's no longer single width, the right half of the row should be erased.
if (lineRendition != LineRendition::SingleWidth)
{
const auto fillOffset = GetLineWidth(rowIndex);
FillRect({ fillOffset, rowIndex, til::CoordTypeMax, rowIndex + 1 }, L" ", fillAttributes);
// We also need to make sure the cursor is clamped within the new width.
GetCursor().SetPosition(ClampPositionWithinLine(cursorPosition));
}
TriggerRedraw(Viewport::FromDimensions({ 0, rowIndex }, { GetSize().Width(), 1 }));
}
}
void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow)
{
for (auto row = startRow; row < endRow; row++)
{
GetMutableRowByOffset(row).SetLineRendition(LineRendition::SingleWidth);
}
}
LineRendition TextBuffer::GetLineRendition(const til::CoordType row) const
{
if (!_isRowCommitted(row)) [[unlikely]]
{
return LineRendition::SingleWidth;
}
return GetRowByOffset(row).GetLineRendition();
}
bool TextBuffer::IsDoubleWidthLine(const til::CoordType row) const
{
return GetLineRendition(row) != LineRendition::SingleWidth;
}
til::CoordType TextBuffer::GetLineWidth(const til::CoordType row) const
{
// Use shift right to quickly divide the width by 2 for double width lines.
const auto scale = IsDoubleWidthLine(row) ? 1 : 0;
return GetSize().Width() >> scale;
}
til::point TextBuffer::ClampPositionWithinLine(const til::point position) const
{
const auto rightmostColumn = GetLineWidth(position.y) - 1;
return { std::min(position.x, rightmostColumn), position.y };
}
til::point TextBuffer::ScreenToBufferPosition(const til::point position) const
{
// Use shift right to quickly divide the X pos by 2 for double width lines.
const auto scale = IsDoubleWidthLine(position.y) ? 1 : 0;
return { position.x >> scale, position.y };
}
til::point TextBuffer::BufferToScreenPosition(const til::point position) const
{
// Use shift left to quickly multiply the X pos by 2 for double width lines.
const auto scale = IsDoubleWidthLine(position.y) ? 1 : 0;
return { position.x << scale, position.y };
}
// Routine Description:
// - Resets the text contents of this buffer with the default character
// and the default current color attributes
void TextBuffer::Reset() noexcept
{
_decommit();
_initialAttributes = _currentAttributes;
}
// Arguments:
// - newFirstRow: The current y-position of the viewport. We'll clear up until here.
// - rowsToKeep: the number of rows to keep in the buffer.
void TextBuffer::ClearScrollback(const til::CoordType newFirstRow, const til::CoordType rowsToKeep)
{
// We're already at the top? don't clear anything. There's no scrollback.
if (newFirstRow <= 0)
{
return;
}
// The new viewport should keep 0 rows? Then just reset everything.
if (rowsToKeep <= 0)
{
_decommit();
return;
}
ClearMarksInRange(til::point{ 0, 0 }, til::point{ _width, std::max(0, newFirstRow - 1) });
// Our goal is to move the viewport to the absolute start of the underlying memory buffer so that we can
// MEM_DECOMMIT the remaining memory. _firstRow is used to make the TextBuffer behave like a circular buffer.
// The newFirstRow parameter is relative to the _firstRow. The trick to get the content to the absolute start
// is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into
// the absolute start while reading from relative coordinates. This works because GetRowByOffset()
// operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue.
const auto startAbsolute = _firstRow + newFirstRow;
_firstRow = 0;
ScrollRows(startAbsolute, rowsToKeep, -startAbsolute);
const auto end = _estimateOffsetOfLastCommittedRow();
for (auto y = rowsToKeep; y <= end; ++y)
{
GetMutableRowByOffset(y).Reset(_initialAttributes);
}
}
// Routine Description:
// - This is the legacy screen resize with minimal changes
// Arguments:
// - newSize - new size of screen.
// Return Value:
// - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed.
void TextBuffer::ResizeTraditional(til::size newSize)
{
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
newSize.width = std::max(newSize.width, 1);
newSize.height = std::max(newSize.height, 1);
TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer };
const auto cursorRow = GetCursor().GetPosition().y;
const auto copyableRows = std::min<til::CoordType>(_height, newSize.height);
til::CoordType srcRow = 0;
til::CoordType dstRow = 0;
if (cursorRow >= newSize.height)
{
srcRow = cursorRow - newSize.height + 1;
}
for (; dstRow < copyableRows; ++dstRow, ++srcRow)
{
CopyRow(srcRow, dstRow, newBuffer);
}
// NOTE: Keep this in sync with _reserve().
_buffer = std::move(newBuffer._buffer);
_bufferEnd = newBuffer._bufferEnd;
_commitWatermark = newBuffer._commitWatermark;
_initialAttributes = newBuffer._initialAttributes;
_bufferRowStride = newBuffer._bufferRowStride;
_bufferOffsetChars = newBuffer._bufferOffsetChars;
_bufferOffsetCharOffsets = newBuffer._bufferOffsetCharOffsets;
_width = newBuffer._width;
_height = newBuffer._height;
_SetFirstRowIndex(0);
}
void TextBuffer::SetAsActiveBuffer(const bool isActiveBuffer) noexcept
{
_isActiveBuffer = isActiveBuffer;
}
bool TextBuffer::IsActiveBuffer() const noexcept
{
return _isActiveBuffer;
}
Microsoft::Console::Render::Renderer* TextBuffer::GetRenderer() noexcept
{
return _renderer;
}
void TextBuffer::NotifyPaintFrame() noexcept
{
if (_isActiveBuffer && _renderer)
{
_renderer->NotifyPaintFrame();
}
}
void TextBuffer::TriggerRedraw(const Viewport& viewport)
{
if (_isActiveBuffer && _renderer)
{
_renderer->TriggerRedraw(viewport);
}
}
void TextBuffer::TriggerRedrawAll()
{
if (_isActiveBuffer && _renderer)
{
_renderer->TriggerRedrawAll();
}
}
void TextBuffer::TriggerScroll()
{
if (_isActiveBuffer && _renderer)
{
_renderer->TriggerScroll();
}
}
void TextBuffer::TriggerScroll(const til::point delta)
{
if (_isActiveBuffer && _renderer)
{
_renderer->TriggerScroll(&delta);
}
}
void TextBuffer::TriggerNewTextNotification(const std::wstring_view newText)
{
if (_isActiveBuffer && _renderer)
{
_renderer->TriggerNewTextNotification(newText);
}
}
void TextBuffer::TriggerSelection()
{
if (_isActiveBuffer && _renderer)
{
_renderer->TriggerSelection();
}
}
// Method Description:
// - get delimiter class for buffer cell position
// - used for double click selection and uia word navigation
// Arguments:
// - pos: the buffer cell under observation
// - wordDelimiters: the delimiters defined as a part of the DelimiterClass::DelimiterChar
// Return Value:
// - the delimiter class for the given char
DelimiterClass TextBuffer::_GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const
{
const auto realPos = ScreenToBufferPosition(pos);
return GetRowByOffset(realPos.y).DelimiterClassAt(realPos.x, wordDelimiters);
}
// Method Description:
// - Get the start of the word at or before the given position
// - When includeWhitespace is false, returns the start of the current delimiter class run
// (selection behavior: stops at non-wrapped row boundaries)
// - When includeWhitespace is true, also skips backward past any leading ControlChars
// to include the preceding word (accessibility/UIA behavior: crosses all row boundaries)
// Arguments:
// - pos - the buffer position to start from
// - wordDelimiters - characters considered as DelimiterClass::DelimiterChar
// - includeWhitespace - when true, skip past leading whitespace to find the word start
// - limitOptional - (optional) the last possible position in the buffer that can be explored
// Return Value:
// - The position of the first character of the word (inclusive)
til::point TextBuffer::GetWordStart(til::point pos, const std::wstring_view wordDelimiters, bool includeWhitespace, std::optional<til::point> limitOptional) const
{
const auto bufferSize{ GetSize() };
const auto limit{ limitOptional.value_or(bufferSize.BottomInclusiveRightExclusive()) };
if (pos < bufferSize.Origin())
{
// can't move further back, so return early at origin
return bufferSize.Origin();
}
else if (pos >= limit)
{
// clamp to limit,
// but still do movement
pos = limit;
}
// Consider the delimiter classes represented as these chars:
// - ControlChar: "_"
// - DelimiterChar: "D"
// - RegularChar: "C"
// Expected results ("|" is the position):
// includeWhitespace: true false
// CCC___| --> |CCC___ CCC|___
// DDD___| --> |DDD___ DDD|___
// ___CCC| --> ___|CCC ___|CCC
// DDDCCC| --> DDD|CCC DDD|CCC
// ___DDD| --> ___|DDD ___|DDD
// CCCDDD| --> CCC|DDD CCC|DDD
// So the heuristic we use is:
// 1. move to the beginning of the delimiter class run
// 2. (includeWhitespace) if we were on a ControlChar, go back one more delimiter class run
const auto initialDelimiter = bufferSize.IsInBounds(pos) ? _GetDelimiterClassAt(pos, wordDelimiters) : DelimiterClass::ControlChar;
pos = _GetDelimiterClassRunStart(pos, wordDelimiters, includeWhitespace);
if (!includeWhitespace || pos.x == bufferSize.Left())
{
// Special case:
// we're at the left boundary (and end of a delimiter class run),
// we already know we can't wrap, so return early
return pos;
}
else if (initialDelimiter == DelimiterClass::ControlChar)
{
bufferSize.DecrementInExclusiveBounds(pos);
pos = _GetDelimiterClassRunStart(pos, wordDelimiters, includeWhitespace);
}
return pos;
}
// Method Description:
// - Get the exclusive end of the word at or after the given position
// - When includeWhitespace is false, returns the exclusive end of the current delimiter class run
// (selection behavior: stops at non-wrapped row boundaries)
// - When includeWhitespace is true, also skips forward past any trailing ControlChars
// to include trailing whitespace (accessibility/UIA behavior: crosses all row boundaries)
// - The result is clamped to limitOptional when provided
// Arguments:
// - pos - the buffer position to start from
// - wordDelimiters - characters considered as DelimiterClass::DelimiterChar
// - includeWhitespace - when true, skip past trailing whitespace to find the next word boundary
// - limitOptional - (optional) the last possible position in the buffer that can be explored
// Return Value:
// - The exclusive end position of the word
til::point TextBuffer::GetWordEnd(til::point pos, const std::wstring_view wordDelimiters, bool includeWhitespace, std::optional<til::point> limitOptional) const
{
const auto bufferSize{ GetSize() };
const auto limit{ limitOptional.value_or(bufferSize.BottomInclusiveRightExclusive()) };
if (pos >= limit)
{
// can't move further forward,
// so return early at limit
return limit;
}
else if (const auto origin{ bufferSize.Origin() }; pos < origin)
{
// clamp to origin,
// but still do movement
pos = origin;
}
// Consider the delimiter classes represented as these chars:
// - ControlChar: "_"
// - DelimiterChar: "D"
// - RegularChar: "C"
// Expected results ("|" is the position):
// includeWhitespace: true false
// |CCC___ --> CCC___| CCC|___
// |DDD___ --> DDD___| DDD|___
// |___CCC --> ___|CCC ___|CCC
// |DDDCCC --> DDD|CCC DDD|CCC
// |___DDD --> ___|DDD ___|DDD
// |CCCDDD --> CCC|DDD CCC|DDD
// So the heuristic we use is:
// 1. move to the end of the delimiter class run
// 2. (includeWhitespace) if the next delimiter class run is a ControlChar, go forward one more delimiter class run
pos = _GetDelimiterClassRunEnd(pos, wordDelimiters, includeWhitespace);
if (pos >= limit)
{
return limit;
}
else if (!includeWhitespace || pos.x == bufferSize.RightExclusive())
{
// Special case:
// we're at the right boundary (and end of a delimiter class run),
// we already know we can't wrap, so return early
return pos;
}
if (const auto nextDelimClass = bufferSize.IsInBounds(pos) ? _GetDelimiterClassAt(pos, wordDelimiters) : DelimiterClass::ControlChar;
nextDelimClass == DelimiterClass::ControlChar)
{
pos = _GetDelimiterClassRunEnd(pos, wordDelimiters, includeWhitespace);
return std::min(pos, limit);
}
return pos;
}
bool TextBuffer::IsWordBoundary(const til::point pos, const std::wstring_view wordDelimiters) const
{
const auto bufferSize = GetSize();
if (!bufferSize.IsInExclusiveBounds(pos))
{
// not in bounds
return false;
}
// buffer boundaries are always word boundaries
if (pos == bufferSize.Origin() || pos == bufferSize.BottomInclusiveRightExclusive())
{
return true;
}
// at beginning of the row, but we didn't wrap
if (pos.x == bufferSize.Left())
{
const auto& row = GetRowByOffset(pos.y - 1);
if (!row.WasWrapForced())
{
return true;
}
}
// at end of the row, but we didn't wrap
if (pos.x == bufferSize.RightExclusive())
{
const auto& row = GetRowByOffset(pos.y);
if (!row.WasWrapForced())
{
return true;
}
}
// we can treat text as contiguous,
// use DecrementInBounds (not exclusive) here
auto prevPos = pos;
bufferSize.DecrementInBounds(prevPos);
const auto prevDelimiterClass = _GetDelimiterClassAt(prevPos, wordDelimiters);
// if we changed delimiter class
// and the current delimiter class is not a control char,
// we're at a word boundary
const auto currentDelimiterClass = _GetDelimiterClassAt(pos, wordDelimiters);
return prevDelimiterClass != currentDelimiterClass && currentDelimiterClass != DelimiterClass::ControlChar;
}
// Method Description:
// - Get the start position for the current delimiter class run, scanning backward
// - Stops when the delimiter class changes or a row boundary is reached
// - When accessibilityMode is true, freely crosses non-wrapped row boundaries
// - When accessibilityMode is false, only crosses wrap-forced row boundaries
// Arguments:
// - pos - the buffer position to start scanning from
// - wordDelimiters - what characters are we considering for the separation of words
// - accessibilityMode - when true, cross non-wrapped row boundaries freely
// Return Value:
// - The position of the first character in the current delimiter class run (inclusive)
til::point TextBuffer::_GetDelimiterClassRunStart(til::point pos, const std::wstring_view wordDelimiters, const bool accessibilityMode) const
{
const auto bufferSize = GetSize();
const auto initialDelimClass = bufferSize.IsInBounds(pos) ? _GetDelimiterClassAt(pos, wordDelimiters) : DelimiterClass::ControlChar;
auto nextPos = pos;
while (nextPos != bufferSize.Origin())
{
bufferSize.DecrementInExclusiveBounds(nextPos);
if (nextPos.x == bufferSize.RightExclusive())
{
// wrapped onto previous line
const auto& row = GetRowByOffset(nextPos.y);
if (!row.WasWrapForced() && !accessibilityMode)
{
return pos;
}
// In accessibility mode (or if row was wrap-forced), continue
// across the row boundary. The actual last character of the
// previous row will be checked on the next iteration.
// Don't update pos to avoid storing a transient position
}
else if (_GetDelimiterClassAt(nextPos, wordDelimiters) != initialDelimClass)
{
// if we changed delim class, we're done (don't apply move)
return pos;
}
else
{
pos = nextPos;
}
}
return pos;
}
// Method Description:
// - Get the exclusive position for the end of the current delimiter class run
// Arguments:
// - pos - the buffer position being within the current delimiter class
// - wordDelimiters - what characters are we considering for the separation of words
// - accessibilityMode - when true, cross non-wrapped row boundaries freely
til::point TextBuffer::_GetDelimiterClassRunEnd(til::point pos, const std::wstring_view wordDelimiters, const bool accessibilityMode) const
{
const auto bufferSize = GetSize();
const auto initialDelimClass = bufferSize.IsInBounds(pos) ? _GetDelimiterClassAt(pos, wordDelimiters) : DelimiterClass::ControlChar;
for (auto nextPos = pos; nextPos != bufferSize.BottomInclusiveRightExclusive(); pos = nextPos)
{
bufferSize.IncrementInExclusiveBounds(nextPos);
if (nextPos.x == bufferSize.Left())
{
// wrapped onto next line,
// check if it was forced to wrap or switched delimiter class
const auto& row = GetRowByOffset(pos.y);
if (accessibilityMode)
{
// In accessibility mode, always cross row boundaries,
// but still stop if the delimiter class changes
if (_GetDelimiterClassAt(nextPos, wordDelimiters) != initialDelimClass)
{
return nextPos;
}
}
else if (!row.WasWrapForced() || _GetDelimiterClassAt(nextPos, wordDelimiters) != initialDelimClass)
{
return pos;
}
}
else if (bufferSize.IsInBounds(nextPos) && _GetDelimiterClassAt(nextPos, wordDelimiters) != initialDelimClass)
{
// if we changed delim class,
// apply the move and return
return nextPos;
}
}
return pos;
}
void TextBuffer::_PruneHyperlinks()
{
// Check the old first row for hyperlink references
// If there are any, search the entire buffer for the same reference
// If the buffer does not contain the same reference, we can remove that hyperlink from our map
// This way, obsolete hyperlink references are cleared from our hyperlink map instead of hanging around
// Get all the hyperlink references in the row we're erasing
const auto hyperlinks = GetRowByOffset(0).GetHyperlinks();
if (!hyperlinks.empty())
{
// Move to unordered set so we can use hashed lookup of IDs instead of linear search.
// Only make it an unordered set now because set always heap allocates but vector
// doesn't when the set is empty (saving an allocation in the common case of no links.)
std::unordered_set<uint16_t> firstRowRefs{ hyperlinks.cbegin(), hyperlinks.cend() };
const auto total = TotalRowCount();
// Loop through all the rows in the buffer except the first row -
// we have found all hyperlink references in the first row and put them in refs,
// now we need to search the rest of the buffer (i.e. all the rows except the first)
// to see if those references are anywhere else
for (til::CoordType i = 1; i < total; ++i)
{
const auto nextRowRefs = GetRowByOffset(i).GetHyperlinks();
for (auto id : nextRowRefs)
{
if (firstRowRefs.find(id) != firstRowRefs.end())
{
firstRowRefs.erase(id);
}
}
if (firstRowRefs.empty())
{
// No more hyperlink references left to search for, terminate early
break;
}
}
// Now delete obsolete references from our map
for (auto hyperlinkReference : firstRowRefs)
{
RemoveHyperlinkFromMap(hyperlinkReference);
}
}
}
// Method Description:
// - Update pos to be the beginning of the current glyph/character. This is used for accessibility
// Arguments:
// - pos - a til::point on the word you are currently on
// - limitOptional - (optional) the last possible position in the buffer that can be explored. This can be used to improve performance.
// Return Value:
// - pos - The til::point for the first cell of the current glyph (inclusive)
til::point TextBuffer::GetGlyphStart(const til::point pos, std::optional<til::point> limitOptional) const
{
auto resultPos = pos;
const auto bufferSize = GetSize();
const auto limit{ limitOptional.value_or(bufferSize.EndExclusive()) };
// Clamp pos to limit
if (resultPos > limit)
{
return limit;
}
// if we're on a trailing byte, move to the leading byte
if (bufferSize.IsInBounds(resultPos) &&
GetCellDataAt(resultPos)->DbcsAttr() == DbcsAttribute::Trailing)
{
bufferSize.DecrementInBounds(resultPos, true);
}
return resultPos;
}
// Method Description:
// - Update pos to be the end of the current glyph/character.
// Arguments:
// - pos - a til::point on the word you are currently on
// - accessibilityMode - this is being used for accessibility; make the end exclusive.
// Return Value:
// - pos - The til::point for the last cell of the current glyph (exclusive)
til::point TextBuffer::GetGlyphEnd(const til::point pos, bool accessibilityMode, std::optional<til::point> limitOptional) const
{
auto resultPos = pos;
const auto bufferSize = GetSize();
const auto limit{ limitOptional.value_or(bufferSize.EndExclusive()) };
// Clamp pos to limit
if (resultPos > limit)
{
return limit;
}
if (bufferSize.IsInBounds(resultPos) &&
GetCellDataAt(resultPos)->DbcsAttr() == DbcsAttribute::Leading)
{
bufferSize.IncrementInBounds(resultPos, true);
}
// increment one more time to become exclusive
if (accessibilityMode)
{
bufferSize.IncrementInBounds(resultPos, true);
}
return resultPos;
}
// Method Description:
// - Update pos to be the beginning of the next glyph/character. This is used for accessibility
// Arguments:
// - pos - a til::point on the word you are currently on
// - allowExclusiveEnd - allow result to be the exclusive limit (one past limit)
// - limit - boundaries for the iterator to operate within
// Return Value:
// - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary)
// - pos - The til::point for the first cell of the current glyph (inclusive)
bool TextBuffer::MoveToNextGlyph(til::point& pos, bool allowExclusiveEnd, std::optional<til::point> limitOptional) const
{
const auto bufferSize = GetSize();
const auto limit{ limitOptional.value_or(bufferSize.EndExclusive()) };
const auto distanceToLimit{ bufferSize.CompareInBounds(pos, limit, true) };
if (distanceToLimit >= 0)
{
// Corner Case: we're on/past the limit
// Clamp us to the limit
pos = limit;
return false;
}
else if (!allowExclusiveEnd && distanceToLimit == -1)
{
// Corner Case: we're just before the limit
// and we are not allowed onto the exclusive end.
// Fail to move.
return false;
}
// Try to move forward, but if we hit the buffer boundary, we fail to move.
auto iter{ GetCellDataAt(pos, bufferSize) };
const bool success{ ++iter };
// Move again if we're on a wide glyph
if (success && iter->DbcsAttr() == DbcsAttribute::Trailing)
{
++iter;
}
pos = iter.Pos();
return success;
}
bool TextBuffer::MoveToNextGlyph2(til::point& pos, std::optional<til::point> limitOptional) const
{
const auto bufferSize = GetSize();
const auto limit{ limitOptional.value_or(bufferSize.BottomInclusiveRightExclusive()) };
if (pos >= limit)
{
// Corner Case: we're on/past the limit
// Clamp us to the limit
pos = limit;
return false;
}
// Try to move forward, but if we hit the buffer boundary, we fail to move.
const bool success = bufferSize.IncrementInExclusiveBounds(pos);
if (success &&
bufferSize.IsInBounds(pos) &&
GetCellDataAt(pos)->DbcsAttr() == DbcsAttribute::Trailing)
{
// Move again if we're on a wide glyph
bufferSize.IncrementInExclusiveBounds(pos);
}
return success;
}
// Method Description:
// - Update pos to be the beginning of the previous glyph/character. This is used for accessibility
// Arguments:
// - pos - a til::point on the word you are currently on
// Return Value:
// - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary)
// - pos - The til::point for the first cell of the previous glyph (inclusive)
bool TextBuffer::MoveToPreviousGlyph(til::point& pos, std::optional<til::point> limitOptional) const
{
auto resultPos = pos;
const auto bufferSize = GetSize();
const auto limit{ limitOptional.value_or(bufferSize.EndExclusive()) };
if (bufferSize.CompareInBounds(pos, limit, true) > 0)
{
// we're past the end
// clamp us to the limit
pos = limit;
return true;
}
// try to move. If we can't, we're done.
const auto success = bufferSize.DecrementInBounds(resultPos, true);
if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr() == DbcsAttribute::Leading)
{
bufferSize.DecrementInBounds(resultPos, true);
}
pos = resultPos;
return success;
}
bool TextBuffer::MoveToPreviousGlyph2(til::point& pos, std::optional<til::point> limitOptional) const
{
const auto bufferSize = GetSize();
const auto limit{ limitOptional.value_or(bufferSize.BottomInclusiveRightExclusive()) };
if (pos >= limit)
{
// Corner Case: we're on/past the limit
// Clamp us to the limit
pos = limit;
return false;
}
// Try to move backward, but if we hit the buffer boundary, we fail to move.
const bool success = bufferSize.DecrementInExclusiveBounds(pos);
if (success &&
bufferSize.IsInBounds(pos) &&
GetCellDataAt(pos)->DbcsAttr() == DbcsAttribute::Trailing)
{
// Move again if we're on a wide glyph
bufferSize.DecrementInExclusiveBounds(pos);
}
return success;
}
// Method Description:
// - Determines the line-by-line rectangles based on two COORDs
// - expands the rectangles to support wide glyphs
// - used for selection rects and UIA bounding rects
// Arguments:
// - start: a corner of the text region of interest (inclusive)
// - end: the other corner of the text region of interest (inclusive)
// - blockSelection: when enabled, only get the rectangular text region,
// as opposed to the text extending to the left/right
// buffer margins
// - bufferCoordinates: when enabled, treat the coordinates as relative to
// the buffer rather than the screen.
// Return Value:
// - One or more rects corresponding to the selection area
const std::vector<til::inclusive_rect> TextBuffer::GetTextRects(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const
{
std::vector<til::inclusive_rect> textRects;
// (0,0) is the top-left of the screen
// the physically "higher" coordinate is closer to the top-left
// the physically "lower" coordinate is closer to the bottom-right
const auto [higherCoord, lowerCoord] = start <= end ?
std::make_tuple(start, end) :
std::make_tuple(end, start);
const auto textRectSize = 1 + lowerCoord.y - higherCoord.y;
textRects.reserve(textRectSize);
for (auto row = higherCoord.y; row <= lowerCoord.y; row++)
{
til::inclusive_rect textRow;
textRow.top = row;
textRow.bottom = row;
if (blockSelection || higherCoord.y == lowerCoord.y)
{
// set the left and right margin to the left-/right-most respectively
textRow.left = std::min(higherCoord.x, lowerCoord.x);
textRow.right = std::max(higherCoord.x, lowerCoord.x);
}
else
{
const auto bufferSize = GetSize();
textRow.left = (row == higherCoord.y) ? higherCoord.x : bufferSize.Left();
textRow.right = (row == lowerCoord.y) ? lowerCoord.x : bufferSize.RightInclusive();
}
// If we were passed screen coordinates, convert the given range into
// equivalent buffer offsets, taking line rendition into account.
if (!bufferCoordinates)
{
textRow = ScreenToBufferLine(textRow, GetLineRendition(row));
}
_ExpandTextRow(textRow);
textRects.emplace_back(textRow);
}
return textRects;
}
// Method Description:
// - Computes the span(s) for the given selection
// - If not a blockSelection, returns a single span (start - end)
// - Else if a blockSelection, returns spans corresponding to each line in the block selection
// Arguments:
// - start: beginning of the text region of interest (inclusive)
// - end: the other end of the text region of interest (exclusive)
// - blockSelection: when enabled, get spans for each line covered by the block
// - bufferCoordinates: when enabled, treat the coordinates as relative to
// the buffer rather than the screen.
// Return Value:
// - one or more sets of start-end coordinates, representing spans of text in the buffer
std::vector<til::point_span> TextBuffer::GetTextSpans(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const
{
std::vector<til::point_span> textSpans;
if (blockSelection)
{
// If blockSelection, this is effectively the same operation as GetTextRects, but
// expressed in til::point coordinates.
const auto rects = GetTextRects(start, end, /*blockSelection*/ true, bufferCoordinates);
textSpans.reserve(rects.size());
for (auto rect : rects)
{
const til::point first = { rect.left, rect.top };
const til::point second = { rect.right, rect.bottom };
textSpans.emplace_back(first, second);
}
}
else
{
const auto bufferSize = GetSize();
// (0,0) is the top-left of the screen
// the physically "higher" coordinate is closer to the top-left
// the physically "lower" coordinate is closer to the bottom-right
auto [higherCoord, lowerCoord] = start <= end ?
std::make_tuple(start, end) :
std::make_tuple(end, start);
textSpans.reserve(1);
// If we were passed screen coordinates, convert the given range into
// equivalent buffer offsets, taking line rendition into account.
if (!bufferCoordinates)
{
higherCoord = ScreenToBufferLineInclusive(higherCoord, GetLineRendition(higherCoord.y));
lowerCoord = ScreenToBufferLineInclusive(lowerCoord, GetLineRendition(lowerCoord.y));
}
til::inclusive_rect asRect = { higherCoord.x, higherCoord.y, lowerCoord.x, lowerCoord.y };
_ExpandTextRow(asRect);
higherCoord.x = asRect.left;
higherCoord.y = asRect.top;
lowerCoord.x = asRect.right;
lowerCoord.y = asRect.bottom;
textSpans.emplace_back(higherCoord, lowerCoord);
}
return textSpans;
}
// Method Description:
// - Expand the selection row according to include wide glyphs fully
// - this is particularly useful for box selections (ALT + selection)
// Arguments:
// - selectionRow: the selection row to be expanded
// Return Value:
// - modifies selectionRow's Left and Right values to expand properly
void TextBuffer::_ExpandTextRow(til::inclusive_rect& textRow) const
{
const auto bufferSize = GetSize();
// expand left side of rect
til::point targetPoint{ textRow.left, textRow.top };
if (bufferSize.IsInBounds(targetPoint) && GetCellDataAt(targetPoint)->DbcsAttr() == DbcsAttribute::Trailing)
{
bufferSize.DecrementInExclusiveBounds(targetPoint);
textRow.left = targetPoint.x;
}
// expand right side of rect
targetPoint = { textRow.right, textRow.bottom };
if (bufferSize.IsInBounds(targetPoint) && GetCellDataAt(targetPoint)->DbcsAttr() == DbcsAttribute::Trailing)
{
bufferSize.IncrementInExclusiveBounds(targetPoint);
textRow.right = targetPoint.x;
}
}
// Routine Description:
// - Retrieves the plain text data between the specified coordinates.
// Arguments:
// - trimTrailingWhitespace - remove the trailing whitespace at the end of the result.
// - start - where to start getting text (should be at or prior to "end") (inclusive)
// - end - where to end getting text (exclusive)
// Return Value:
// - Just the text.
std::wstring TextBuffer::GetPlainText(const til::point start, const til::point end) const
{
const auto req = CopyRequest::FromConfig(*this, start, end, true, false, false, false);
return GetPlainText(req);
}
// Routine Description:
// - Given a copy request and a row, retrieves the row bounds [begin, end) and
// a boolean indicating whether a line break should be added to this row.
// Arguments:
// - req - the copy request
// - iRow - the row index
// - row - the row
// Return Value:
// - The row bounds and a boolean for line break
std::tuple<til::CoordType, til::CoordType, bool> TextBuffer::_RowCopyHelper(const TextBuffer::CopyRequest& req, const til::CoordType iRow, const ROW& row) const
{
til::CoordType rowBeg = 0;
til::CoordType rowEnd = 0;
if (req.blockSelection)
{
const auto lineRendition = row.GetLineRendition();
const auto minX = req.bufferCoordinates ? req.minX : ScreenToBufferLineInclusive(til::point{ req.minX, iRow }, lineRendition).x;
const auto maxX = req.bufferCoordinates ? req.maxX : ScreenToBufferLineInclusive(til::point{ req.maxX, iRow }, lineRendition).x;
rowBeg = minX;
rowEnd = maxX;
}
else
{
const auto lineRendition = row.GetLineRendition();
const auto beg = req.bufferCoordinates ? req.beg : ScreenToBufferLineInclusive(req.beg, lineRendition);
const auto end = req.bufferCoordinates ? req.end : ScreenToBufferLineInclusive(req.end, lineRendition);
rowBeg = iRow != beg.y ? 0 : beg.x;
rowEnd = iRow != end.y ? row.GetReadableColumnCount() : end.x;
}
// Our selection mechanism doesn't stick to glyph boundaries at the moment.
// We need to adjust begin and end points manually to avoid partially
// selected glyphs.
rowBeg = row.AdjustToGlyphStart(rowBeg);
rowEnd = row.AdjustToGlyphEnd(rowEnd);
// When `formatWrappedRows` is set, apply formatting on all rows (wrapped
// and non-wrapped), but when it's false, format non-wrapped rows only.
const auto shouldFormatRow = req.formatWrappedRows || !row.WasWrapForced();
// trim trailing whitespace
if (shouldFormatRow && req.trimTrailingWhitespace)
{
rowEnd = std::min(rowEnd, row.GetLastNonSpaceColumn());
}
// line breaks
const auto addLineBreak = shouldFormatRow && req.includeLineBreak;
return { rowBeg, rowEnd, addLineBreak };
}
// Routine Description:
// - Retrieves the text data from the buffer and presents it in a clipboard-ready format.
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// Return Value:
// - The text data from the selected region of the text buffer. Empty if the copy request is invalid.
std::wstring TextBuffer::GetPlainText(const CopyRequest& req) const
{
if (req.beg > req.end)
{
return {};
}
std::wstring selectedText;
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
{
const auto& row = GetRowByOffset(iRow);
const auto& [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
// save selected text (exclusive end)
selectedText += row.GetText(rowBeg, rowEnd);
if (addLineBreak && iRow != req.end.y)
{
selectedText += L"\r\n";
}
}
return selectedText;
}
// Retrieves the text data from the buffer *with* ANSI escape code control sequences and presents it in
// a clipboard-ready format.
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// Return Value:
// - The text and control sequence data from the selected region of the text buffer. Empty if the copy request
// is invalid.
std::wstring TextBuffer::GetWithControlSequences(const CopyRequest& req) const
{
if (req.beg > req.end)
{
return {};
}
std::wstring selectedText;
std::optional<TextAttribute> previousTextAttr;
bool delayedLineBreak = false;
const auto firstRow = req.beg.y;
const auto lastRow = req.end.y;
for (til::CoordType currentRow = firstRow; currentRow <= lastRow; currentRow++)
{
const auto& row = GetRowByOffset(currentRow);
const auto [startX, endX, reqAddLineBreak] = _RowCopyHelper(req, currentRow, row);
const bool isLastRow = currentRow == lastRow;
const bool addLineBreak = reqAddLineBreak && !isLastRow;
_SerializeRow(row, startX, endX, addLineBreak, isLastRow, selectedText, previousTextAttr, delayedLineBreak);
}
return selectedText;
}
// Routine Description:
// - Generates a CF_HTML compliant structure from the selected region of the buffer
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// - fontHeightPoints - the unscaled font height
// - fontFaceName - the name of the font used
// - backgroundColor - default background color for characters, also used in padding
// - isIntenseBold - true if being intense is treated as being bold
// - GetAttributeColors - function to get the colors of the text attributes as they're rendered
// Return Value:
// - string containing the generated HTML. Empty if the copy request is invalid.
std::string TextBuffer::GenHTML(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept
{
// GH#5347 - Don't provide a title for the generated HTML, as many
// web applications will paste the title first, followed by the HTML
// content, which is unexpected.
if (req.beg > req.end)
{
return {};
}
try
{
std::string htmlBuilder;
// First we have to add some standard HTML boiler plate required for
// CF_HTML as part of the HTML Clipboard format
constexpr std::string_view htmlHeader = "<!DOCTYPE><HTML><HEAD></HEAD><BODY>";
htmlBuilder += htmlHeader;
htmlBuilder += "<!--StartFragment -->";
// apply global style in div element
{
htmlBuilder += "<DIV STYLE=\"";
htmlBuilder += "display:inline-block;";
htmlBuilder += "white-space:pre;";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("background-color:{};"), Utils::ColorToHexString(backgroundColor));
// even with different font, add monospace as fallback
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("font-family:'{}',monospace;"), til::u16u8(fontFaceName));
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("font-size:{}pt;"), fontHeightPoints);
// note: MS Word doesn't support padding (in this way at least)
// todo: customizable padding
htmlBuilder += "padding:4px;";
htmlBuilder += "\">";
}
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
{
const auto& row = GetRowByOffset(iRow);
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
auto x = rowBegU16;
for (const auto& [attr, length] : runs)
{
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
const auto [fg, bg, ul] = GetAttributeColors(attr);
const auto fgHex = Utils::ColorToHexString(fg);
const auto bgHex = Utils::ColorToHexString(bg);
const auto ulHex = Utils::ColorToHexString(ul);
const auto ulStyle = attr.GetUnderlineStyle();
const auto isUnderlined = ulStyle != UnderlineStyle::NoUnderline;
const auto isCrossedOut = attr.IsCrossedOut();
const auto isOverlined = attr.IsOverlined();
htmlBuilder += "<SPAN STYLE=\"";
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("color:{};"), fgHex);
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("background-color:{};"), bgHex);
if (attr.IsBold(isIntenseBold))
{
htmlBuilder += "font-weight:bold;";
}
if (attr.IsItalic())
{
htmlBuilder += "font-style:italic;";
}
if (isCrossedOut || isOverlined)
{
fmt::format_to(std::back_inserter(htmlBuilder),
FMT_COMPILE("text-decoration:{} {} {};"),
isCrossedOut ? "line-through" : "",
isOverlined ? "overline" : "",
fgHex);
}
if (isUnderlined)
{
// Since underline, overline and strikethrough use the same css property,
// we cannot apply different colors to them at the same time. However, we
// can achieve the desired result by creating a nested <span> and applying
// underline style and color to it.
htmlBuilder += "\"><SPAN STYLE=\"";
switch (ulStyle)
{
case UnderlineStyle::NoUnderline:
break;
case UnderlineStyle::DoublyUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline double {};"), ulHex);
break;
case UnderlineStyle::CurlyUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline wavy {};"), ulHex);
break;
case UnderlineStyle::DottedUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline dotted {};"), ulHex);
break;
case UnderlineStyle::DashedUnderlined:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline dashed {};"), ulHex);
break;
case UnderlineStyle::SinglyUnderlined:
default:
fmt::format_to(std::back_inserter(htmlBuilder), FMT_COMPILE("text-decoration:underline {};"), ulHex);
break;
}
}
htmlBuilder += "\">";
// text
std::string unescapedText;
THROW_IF_FAILED(til::u16u8(row.GetText(x, nextX), unescapedText));
for (const auto c : unescapedText)
{
switch (c)
{
case '<':
htmlBuilder += "&lt;";
break;
case '>':
htmlBuilder += "&gt;";
break;
case '&':
htmlBuilder += "&amp;";
break;
default:
htmlBuilder += c;
}
}
if (isUnderlined)
{
// close the nested span we created for underline
htmlBuilder += "</SPAN>";
}
htmlBuilder += "</SPAN>";
// advance to next run of text
x = nextX;
}
// never add line break to the last row.
if (addLineBreak && iRow < req.end.y)
{
htmlBuilder += "<BR>";
}
}
htmlBuilder += "</DIV>";
htmlBuilder += "<!--EndFragment -->";
constexpr std::string_view HtmlFooter = "</BODY></HTML>";
htmlBuilder += HtmlFooter;
// once filled with values, there will be exactly 157 bytes in the clipboard header
constexpr size_t ClipboardHeaderSize = 157;
// these values are byte offsets from start of clipboard
const auto htmlStartPos = ClipboardHeaderSize;
const auto htmlEndPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlBuilder.length());
const auto fragStartPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlHeader.length());
const auto fragEndPos = htmlEndPos - HtmlFooter.length();
// header required by HTML 0.9 format
std::string clipHeaderBuilder;
clipHeaderBuilder += "Version:0.9\r\n";
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartHTML:{:0>10}\r\n"), htmlStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndHTML:{:0>10}\r\n"), htmlEndPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartFragment:{:0>10}\r\n"), fragStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndFragment:{:0>10}\r\n"), fragEndPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("StartSelection:{:0>10}\r\n"), fragStartPos);
fmt::format_to(std::back_inserter(clipHeaderBuilder), FMT_COMPILE("EndSelection:{:0>10}\r\n"), fragEndPos);
return clipHeaderBuilder + htmlBuilder;
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
return {};
}
}
// Routine Description:
// - Generates an RTF document from the selected region of the buffer
// RTF 1.5 Spec: https://www.biblioscape.com/rtf15_spec.htm
// RTF 1.9.1 Spec: https://msopenspecs.azureedge.net/files/Archive_References/[MSFT-RTF].pdf
// Arguments:
// - req - the copy request having the bounds of the selected region and other related configuration flags.
// - fontHeightPoints - the unscaled font height
// - fontFaceName - the name of the font used
// - backgroundColor - default background color for characters, also used in padding
// - isIntenseBold - true if being intense is treated as being bold
// - GetAttributeColors - function to get the colors of the text attributes as they're rendered
// Return Value:
// - string containing the generated RTF. Empty if the copy request is invalid.
std::string TextBuffer::GenRTF(const CopyRequest& req,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor,
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept
{
if (req.beg > req.end)
{
return {};
}
try
{
std::string rtfBuilder;
// start rtf
rtfBuilder += "{";
// Standard RTF header.
// This is similar to the header generated by WordPad.
// \ansi:
// Specifies that the ANSI char set is used in the current doc.
// \ansicpg1252:
// Represents the ANSI code page which is used to perform
// the Unicode to ANSI conversion when writing RTF text.
// \deff0:
// Specifies that the default font for the document is the one
// at index 0 in the font table.
// \nouicompat:
// Some features are blocked by default to maintain compatibility
// with older programs (Eg. Word 97-2003). `nouicompat` disables this
// behavior, and unblocks these features. See: Spec 1.9.1, Pg. 51.
rtfBuilder += "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat";
// font table
// Brace escape: add an extra brace (of same kind) after a brace to escape it within the format string.
fmt::format_to(std::back_inserter(rtfBuilder), FMT_COMPILE("{{\\fonttbl{{\\f0\\fmodern\\fcharset0 {};}}}}"), til::u16u8(fontFaceName));
// map to keep track of colors:
// keys are colors represented by COLORREF
// values are indices of the corresponding colors in the color table
std::unordered_map<COLORREF, size_t> colorMap;
// RTF color table
std::string colorTableBuilder;
colorTableBuilder += "{\\colortbl ;";
const auto getColorTableIndex = [&](const COLORREF color) -> size_t {
// Exclude the 0 index for the default color, and start with 1.
const auto [it, inserted] = colorMap.emplace(color, colorMap.size() + 1);
if (inserted)
{
const auto red = static_cast<int>(GetRValue(color));
const auto green = static_cast<int>(GetGValue(color));
const auto blue = static_cast<int>(GetBValue(color));
fmt::format_to(std::back_inserter(colorTableBuilder), FMT_COMPILE("\\red{}\\green{}\\blue{};"), red, green, blue);
}
return it->second;
};
// content
std::string contentBuilder;
// \viewkindN: View mode of the document to be used. N=4 specifies that the document is in Normal view. (maybe unnecessary?)
// \ucN: Number of unicode fallback characters after each codepoint. (global)
contentBuilder += "\\viewkind4\\uc1";
// paragraph styles
// \pard: paragraph description
// \slmultN: line-spacing multiple
// \fN: font to be used for the paragraph, where N is the font index in the font table
contentBuilder += "\\pard\\slmult1\\f0";
// \fsN: specifies font size in half-points. E.g. \fs20 results in a font
// size of 10 pts. That's why, font size is multiplied by 2 here.
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\fs{}"), 2 * fontHeightPoints);
// Set the background color for the page. But the standard way (\cbN) to do
// this isn't supported in Word. However, the following control words sequence
// works in Word (and other RTF editors also) for applying the text background
// color. See: Spec 1.9.1, Pg. 23.
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\chshdng0\\chcbpat{}"), getColorTableIndex(backgroundColor));
for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)
{
const auto& row = GetRowByOffset(iRow);
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
auto x = rowBegU16;
for (auto& [attr, length] : runs)
{
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
const auto [fg, bg, ul] = GetAttributeColors(attr);
const auto fgIdx = getColorTableIndex(fg);
const auto bgIdx = getColorTableIndex(bg);
const auto ulIdx = getColorTableIndex(ul);
const auto ulStyle = attr.GetUnderlineStyle();
// start an RTF group that can be closed later to restore the
// default attribute.
contentBuilder += "{";
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\cf{}"), fgIdx);
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\chshdng0\\chcbpat{}"), bgIdx);
if (attr.IsBold(isIntenseBold))
{
contentBuilder += "\\b";
}
if (attr.IsItalic())
{
contentBuilder += "\\i";
}
if (attr.IsCrossedOut())
{
contentBuilder += "\\strike";
}
switch (ulStyle)
{
case UnderlineStyle::NoUnderline:
break;
case UnderlineStyle::DoublyUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uldb\\ulc{}"), ulIdx);
break;
case UnderlineStyle::CurlyUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\ulwave\\ulc{}"), ulIdx);
break;
case UnderlineStyle::DottedUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uld\\ulc{}"), ulIdx);
break;
case UnderlineStyle::DashedUnderlined:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\uldash\\ulc{}"), ulIdx);
break;
case UnderlineStyle::SinglyUnderlined:
default:
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\ul\\ulc{}"), ulIdx);
break;
}
// RTF commands and the text data must be separated by a space.
// Otherwise, if the text begins with a space then that space will
// be interpreted as part of the last command, and will be lost.
contentBuilder += " ";
const auto unescapedText = row.GetText(x, nextX); // including character at nextX
_AppendRTFText(contentBuilder, unescapedText);
contentBuilder += "}"; // close RTF group
// advance to next run of text
x = nextX;
}
// never add line break to the last row.
if (addLineBreak && iRow < req.end.y)
{
contentBuilder += "\\line";
}
}
// add color table to the final RTF
rtfBuilder += colorTableBuilder + "}";
// add the text content to the final RTF
rtfBuilder += contentBuilder + "}";
return rtfBuilder;
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
return {};
}
}
void TextBuffer::_AppendRTFText(std::string& contentBuilder, const std::wstring_view& text)
{
for (const auto codeUnit : text)
{
if (codeUnit <= 127)
{
switch (codeUnit)
{
case L'\\':
case L'{':
case L'}':
contentBuilder += "\\";
[[fallthrough]];
default:
contentBuilder += gsl::narrow_cast<char>(codeUnit);
}
}
else
{
// Windows uses unsigned wchar_t - RTF uses signed ones.
// '?' is the fallback ascii character.
fmt::format_to(std::back_inserter(contentBuilder), FMT_COMPILE("\\u{}?"), std::bit_cast<int16_t>(codeUnit));
}
}
}
void TextBuffer::SerializeTo(HANDLE handle) const
{
static constexpr size_t writeThreshold = 32 * 1024;
std::wstring buffer;
buffer.reserve(writeThreshold + writeThreshold / 2);
buffer.push_back(L'\uFEFF');
std::optional<TextAttribute> previousTextAttr;
bool delayedLineBreak = false;
const til::CoordType firstRow = 0;
const til::CoordType lastRow = GetLastNonSpaceCharacter(nullptr).y;
// This iterates through each row. The exit condition is at the end
// of the for() loop so that we can properly handle file flushing.
for (til::CoordType currentRow = firstRow;; currentRow++)
{
const auto& row = GetRowByOffset(currentRow);
const auto isLastRow = currentRow == lastRow;
const auto startX = 0;
const auto endX = row.GetReadableColumnCount();
const bool addLineBreak = !row.WasWrapForced() || isLastRow;
_SerializeRow(row, startX, endX, addLineBreak, isLastRow, buffer, previousTextAttr, delayedLineBreak);
if (buffer.size() >= writeThreshold || isLastRow)
{
const auto fileSize = gsl::narrow<DWORD>(buffer.size() * sizeof(wchar_t));
DWORD bytesWritten = 0;
THROW_IF_WIN32_BOOL_FALSE(WriteFile(handle, buffer.data(), fileSize, &bytesWritten, nullptr));
THROW_WIN32_IF_MSG(ERROR_WRITE_FAULT, bytesWritten != fileSize, "failed to write");
buffer.clear();
}
if (isLastRow)
{
break;
}
}
}
// Serializes one row of the text buffer including ANSI escape code control sequences.
// Arguments:
// - row - A reference to the row being serialized.
// - startX - The first column (inclusive) to include in the serialized content.
// - endX - The last column (exclusive) to include in the serialized content.
// - addLineBreak - Whether to add a line break at the end of the serialized row.
// - isLastRow - Whether this is the final row to be serialized.
// - buffer - A string to write the serialized row into.
// - previousTextAttr - Used for tracking state across multiple calls to `_SerializeRow` for sequential rows.
// The value will be mutated by the call. The initial call should contain `nullopt`, and subsequent calls
// should pass the value that was written by the previous call.
// - delayedLineBreak - Similarly used for tracking state across multiple calls, and similarly will be mutated
// by the call. The initial call should pass `false` and subsequent calls should pass the value that was
// written by the previous call.
void TextBuffer::_SerializeRow(const ROW& row, const til::CoordType startX, const til::CoordType endX, const bool addLineBreak, const bool isLastRow, std::wstring& buffer, std::optional<TextAttribute>& previousTextAttr, bool& delayedLineBreak) const
{
if (const auto lr = row.GetLineRendition(); lr != LineRendition::SingleWidth)
{
static constexpr std::wstring_view mappings[] = {
L"\x1b#6", // LineRendition::DoubleWidth
L"\x1b#3", // LineRendition::DoubleHeightTop
L"\x1b#4", // LineRendition::DoubleHeightBottom
};
const auto idx = std::clamp(static_cast<int>(lr) - 1, 0, 2);
buffer.append(til::at(mappings, idx));
}
const auto startXU16 = gsl::narrow_cast<uint16_t>(startX);
const auto endXU16 = gsl::narrow_cast<uint16_t>(endX);
const auto runs = row.Attributes().slice(startXU16, endXU16).runs();
const auto beg = runs.begin();
const auto end = runs.end();
auto it = beg;
// Don't try to get `end - 1` if it's an empty iterator; in this case we're going to ignore the `last`
// value anyway so just use `end`.
const auto last = it == end ? end : end - 1;
const auto lastCharX = row.MeasureRight();
til::CoordType oldX = startX;
for (; it != end; ++it)
{
const auto effectivePreviousTextAttr = previousTextAttr.value_or(TextAttribute{ CharacterAttributes::Unused1, TextColor{}, TextColor{}, 0, TextColor{} });
const auto previousAttr = effectivePreviousTextAttr.GetCharacterAttributes();
const auto previousHyperlinkId = effectivePreviousTextAttr.GetHyperlinkId();
const auto previousFg = effectivePreviousTextAttr.GetForeground();
const auto previousBg = effectivePreviousTextAttr.GetBackground();
const auto previousUl = effectivePreviousTextAttr.GetUnderlineColor();
const auto attr = it->value.GetCharacterAttributes();
const auto hyperlinkId = it->value.GetHyperlinkId();
const auto fg = it->value.GetForeground();
const auto bg = it->value.GetBackground();
const auto ul = it->value.GetUnderlineColor();
if (previousAttr != attr)
{
auto attrDelta = attr ^ previousAttr;
// There's no escape sequence that only turns off either bold/intense or dim/faint. SGR 22 turns off both.
// This results in two issues in our generic "Mapping" code below. Assuming, both Intense and Faint were on...
// * ...and either turned off, it would emit SGR 22 which turns both attributes off = Wrong.
// * ...and both are now off, it would emit SGR 22 twice.
//
// This extra branch takes care of both issues. If both attributes turned off it'll emit a single \x1b[22m,
// if faint turned off \x1b[22;1m (intense is still on), and \x1b[22;2m if intense turned off (vice versa).
if (WI_AreAllFlagsSet(previousAttr, CharacterAttributes::Intense | CharacterAttributes::Faint) &&
WI_IsAnyFlagSet(attrDelta, CharacterAttributes::Intense | CharacterAttributes::Faint))
{
wchar_t buf[8] = L"\x1b[22m";
size_t len = 5;
if (WI_IsAnyFlagSet(attr, CharacterAttributes::Intense | CharacterAttributes::Faint))
{
buf[4] = L';';
buf[5] = WI_IsAnyFlagSet(attr, CharacterAttributes::Intense) ? L'1' : L'2';
buf[6] = L'm';
len = 7;
}
buffer.append(&buf[0], len);
WI_ClearAllFlags(attrDelta, CharacterAttributes::Intense | CharacterAttributes::Faint);
}
{
struct Mapping
{
CharacterAttributes attr;
uint8_t change[2]; // [0] = off, [1] = on
};
static constexpr Mapping mappings[] = {
{ CharacterAttributes::Intense, { 22, 1 } },
{ CharacterAttributes::Italics, { 23, 3 } },
{ CharacterAttributes::Blinking, { 25, 5 } },
{ CharacterAttributes::Invisible, { 28, 8 } },
{ CharacterAttributes::CrossedOut, { 29, 9 } },
{ CharacterAttributes::Faint, { 22, 2 } },
{ CharacterAttributes::TopGridline, { 55, 53 } },
{ CharacterAttributes::ReverseVideo, { 27, 7 } },
};
for (const auto& mapping : mappings)
{
if (WI_IsAnyFlagSet(attrDelta, mapping.attr))
{
const auto n = til::at(mapping.change, WI_IsAnyFlagSet(attr, mapping.attr));
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[{}m"), n);
}
}
}
if (WI_IsAnyFlagSet(attrDelta, CharacterAttributes::UnderlineStyle))
{
static constexpr std::wstring_view mappings[] = {
L"\x1b[24m", // UnderlineStyle::NoUnderline
L"\x1b[4m", // UnderlineStyle::SinglyUnderlined
L"\x1b[21m", // UnderlineStyle::DoublyUnderlined
L"\x1b[4:3m", // UnderlineStyle::CurlyUnderlined
L"\x1b[4:4m", // UnderlineStyle::DottedUnderlined
L"\x1b[4:5m", // UnderlineStyle::DashedUnderlined
};
auto idx = WI_EnumValue(it->value.GetUnderlineStyle());
if (idx >= std::size(mappings))
{
idx = 1; // UnderlineStyle::SinglyUnderlined
}
buffer.append(til::at(mappings, idx));
}
}
if (previousFg != fg)
{
switch (fg.GetType())
{
case ColorType::IsDefault:
buffer.append(L"\x1b[39m");
break;
case ColorType::IsIndex16:
{
uint8_t index = WI_IsFlagSet(fg.GetIndex(), 8) ? 90 : 30;
index += fg.GetIndex() & 7;
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[{}m"), index);
break;
}
case ColorType::IsIndex256:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[38;5;{}m"), fg.GetIndex());
break;
case ColorType::IsRgb:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[38;2;{};{};{}m"), fg.GetR(), fg.GetG(), fg.GetB());
break;
default:
break;
}
}
if (previousBg != bg)
{
switch (bg.GetType())
{
case ColorType::IsDefault:
buffer.append(L"\x1b[49m");
break;
case ColorType::IsIndex16:
{
uint8_t index = WI_IsFlagSet(bg.GetIndex(), 8) ? 100 : 40;
index += bg.GetIndex() & 7;
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[{}m"), index);
break;
}
case ColorType::IsIndex256:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[48;5;{}m"), bg.GetIndex());
break;
case ColorType::IsRgb:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[48;2;{};{};{}m"), bg.GetR(), bg.GetG(), bg.GetB());
break;
default:
break;
}
}
if (previousUl != ul)
{
switch (fg.GetType())
{
case ColorType::IsDefault:
buffer.append(L"\x1b[59m");
break;
case ColorType::IsIndex256:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[58:5:{}m"), ul.GetIndex());
break;
case ColorType::IsRgb:
fmt::format_to(std::back_inserter(buffer), FMT_COMPILE(L"\x1b[58:2::{}:{}:{}m"), ul.GetR(), ul.GetG(), ul.GetB());
break;
default:
break;
}
}
if (previousHyperlinkId != hyperlinkId)
{
if (hyperlinkId)
{
const auto uri = GetHyperlinkUriFromId(hyperlinkId);
if (!uri.empty())
{
buffer.append(L"\x1b]8;;");
buffer.append(uri);
buffer.append(L"\x1b\\");
}
}
else
{
buffer.append(L"\x1b]8;;\x1b\\");
}
}
previousTextAttr = it->value;
// Initially, the buffer is initialized with the default attributes, but once it begins to scroll,
// newly scrolled in rows are initialized with the current attributes. This means we need to set
// the current attributes to those of the upcoming row before the row comes up. Or inversely:
// We let the row come up, let it set its attributes and only then print the newline.
if (delayedLineBreak)
{
buffer.append(L"\r\n");
delayedLineBreak = false;
}
auto newX = oldX + it->length;
// Since our text buffer doesn't store the original input text, the information over the amount of trailing
// whitespaces was lost. If we don't do anything here then a row that just says "Hello" would be serialized
// to "Hello ...". If the user restores the buffer dump with a different window size,
// this would result in some fairly ugly reflow. This code attempts to at least trim trailing whitespaces.
//
// As mentioned above for `delayedLineBreak`, rows are initialized with their first attribute, BUT
// only if the viewport has begun to scroll. Otherwise, they're initialized with the default attributes.
// In other words, we can only skip \x1b[K = Erase in Line, if both the first/last attribute are the default attribute.
static constexpr TextAttribute defaultAttr;
const auto trimTrailingWhitespaces = it == last && lastCharX < newX;
const auto clearToEndOfLine = trimTrailingWhitespaces && (beg->value != defaultAttr || last->value != defaultAttr);
if (trimTrailingWhitespaces)
{
newX = lastCharX;
}
buffer.append(row.GetText(oldX, newX));
if (clearToEndOfLine)
{
buffer.append(L"\x1b[K");
}
oldX = newX;
}
// Handle empty rows (with no runs). See above for more details about `delayedLineBreak`.
if (delayedLineBreak)
{
buffer.append(L"\r\n");
delayedLineBreak = false;
}
delayedLineBreak = !row.WasWrapForced() && addLineBreak;
if (isLastRow)
{
if (previousTextAttr.has_value() && previousTextAttr->GetHyperlinkId())
{
buffer.append(L"\x1b]8;;\x1b\\");
}
buffer.append(L"\x1b[0m");
if (addLineBreak)
{
buffer.append(L"\r\n");
}
}
}
// Function Description:
// - Reflow the contents from the old buffer into the new buffer. The new buffer
// can have different dimensions than the old buffer. If it does, then this
// function will attempt to maintain the logical contents of the old buffer,
// by continuing wrapped lines onto the next line in the new buffer.
// Arguments:
// - oldBuffer - the text buffer to copy the contents FROM
// - newBuffer - the text buffer to copy the contents TO
// - lastCharacterViewport - Optional. If the caller knows that the last
// nonspace character is in a particular Viewport, the caller can provide this
// parameter as an optimization, as opposed to searching the entire buffer.
// - positionInfo - Optional. The caller can provide a pair of rows in this
// parameter and we'll calculate the position of the _end_ of those rows in
// the new buffer. The rows's new value is placed back into this parameter.
// Return Value:
// - S_OK if we successfully copied the contents to the new buffer; otherwise, an appropriate HRESULT.
void TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer, const Viewport* lastCharacterViewport, PositionInformation* positionInfo)
{
const auto& oldCursor = oldBuffer.GetCursor();
auto& newCursor = newBuffer.GetCursor();
til::point oldCursorPos = oldCursor.GetPosition();
til::point newCursorPos;
// BODGY: We use oldCursorPos in two critical places below:
// * To compute an oldHeight that includes at a minimum the cursor row
// * For REFLOW_JANK_CURSOR_WRAP (see comment below)
// Both of these would break the reflow algorithm, but the latter of the two in particular
// would cause the main copy loop below to deadlock. In other words, these two lines
// protect this function against yet-unknown bugs in other parts of the code base.
oldCursorPos.x = std::clamp(oldCursorPos.x, 0, oldBuffer._width - 1);
oldCursorPos.y = std::clamp(oldCursorPos.y, 0, oldBuffer._height - 1);
const auto lastRowWithText = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport).y;
auto mutableViewportTop = positionInfo ? positionInfo->mutableViewportTop : til::CoordTypeMax;
auto visibleViewportTop = positionInfo ? positionInfo->visibleViewportTop : til::CoordTypeMax;
til::CoordType oldY = 0;
til::CoordType newY = 0;
til::CoordType newX = 0;
til::CoordType newWidth = newBuffer.GetSize().Width();
til::CoordType newYLimit = til::CoordTypeMax;
const auto oldHeight = std::max(lastRowWithText, oldCursorPos.y) + 1;
const auto newHeight = newBuffer.GetSize().Height();
const auto newWidthU16 = gsl::narrow_cast<uint16_t>(newWidth);
// Copy oldBuffer into newBuffer until oldBuffer has been fully consumed.
for (; oldY < oldHeight && newY < newYLimit; ++oldY)
{
const auto& oldRow = oldBuffer.GetRowByOffset(oldY);
// A pair of double height rows should optimally wrap as a union (i.e. after wrapping there should be 4 lines).
// But for this initial implementation I chose the alternative approach: Just truncate them.
if (oldRow.GetLineRendition() != LineRendition::SingleWidth)
{
// Since rows with a non-standard line rendition should be truncated it's important
// that we pretend as if the previous row ended in a newline, even if it didn't.
// This is what this if does: It newlines.
if (newX)
{
newX = 0;
newY++;
}
auto& newRow = newBuffer.GetMutableRowByOffset(newY);
// See the comment marked with "REFLOW_RESET".
if (newY >= newHeight)
{
newRow.Reset(newBuffer._initialAttributes);
}
newRow.CopyFrom(oldRow);
newRow.SetWrapForced(false);
if (oldY == oldCursorPos.y)
{
newCursorPos = { newRow.AdjustToGlyphStart(oldCursorPos.x), newY };
}
if (oldY >= mutableViewportTop)
{
positionInfo->mutableViewportTop = newY;
mutableViewportTop = til::CoordTypeMax;
}
if (oldY >= visibleViewportTop)
{
positionInfo->visibleViewportTop = newY;
visibleViewportTop = til::CoordTypeMax;
}
newY++;
continue;
}
// Rows don't store any information for what column the last written character is in.
// We simply truncate all trailing whitespace in this implementation.
auto oldRowLimit = oldRow.MeasureRight();
if (oldY == oldCursorPos.y)
{
// REFLOW_JANK_CURSOR_WRAP:
// Pretending as if there's always at least whitespace in front of the cursor has the benefit that
// * the cursor retains its distance from any preceding text.
// * when a client application starts writing on this new, empty line,
// enlarging the buffer unwraps the text onto the preceding line.
oldRowLimit = std::max(oldRowLimit, oldCursorPos.x + 1);
}
// Immediately copy this mark over to our new row. The positions of the
// marks themselves will be preserved, since they're just text
// attributes. But the "bookmark" needs to get moved to the new row too.
// * If a row wraps as it reflows, that's fine - we want to leave the
// mark on the row it started on.
// * If the second row of a wrapped row had a mark, and it de-flows onto a
// single row, that's fine! The mark was on that logical row.
if (oldRow.GetScrollbarData().has_value())
{
newBuffer.GetMutableRowByOffset(newY).SetScrollbarData(oldRow.GetScrollbarData());
}
til::CoordType oldX = 0;
// Copy oldRow into newBuffer until oldRow has been fully consumed.
// We use a do-while loop to ensure that line wrapping occurs and
// that attributes are copied over even for seemingly empty rows.
do
{
// This if condition handles line wrapping.
// Only if we write past the last column we should wrap and as such this if
// condition is in front of the text insertion code instead of behind it.
// A SetWrapForced of false implies an explicit newline, which is the default.
if (newX >= newWidth)
{
newBuffer.GetMutableRowByOffset(newY).SetWrapForced(true);
newX = 0;
newY++;
}
// REFLOW_RESET:
// If we shrink the buffer vertically, for instance from 100 rows to 90 rows, we will write 10 rows in the
// new buffer twice. We need to reset them before copying text, or otherwise we'll see the previous contents.
// We don't need to be smart about this. Reset() is fast and shrinking doesn't occur often.
if (newY >= newHeight && newX == 0)
{
// We need to ensure not to overwrite the row the cursor is on.
if (newY >= newYLimit)
{
break;
}
newBuffer.GetMutableRowByOffset(newY).Reset(newBuffer._initialAttributes);
}
auto& newRow = newBuffer.GetMutableRowByOffset(newY);
RowCopyTextFromState state{
.source = oldRow,
.columnBegin = newX,
.columnLimit = til::CoordTypeMax,
.sourceColumnBegin = oldX,
.sourceColumnLimit = oldRowLimit,
};
newRow.CopyTextFrom(state);
// If we're at the start of the old row, copy its image content.
if (oldX == 0)
{
ImageSlice::CopyRow(oldRow, newRow);
}
const auto& oldAttr = oldRow.Attributes();
auto& newAttr = newRow.Attributes();
const auto attributes = oldAttr.slice(gsl::narrow_cast<uint16_t>(oldX), oldAttr.size());
newAttr.replace(gsl::narrow_cast<uint16_t>(newX), newAttr.size(), attributes);
newAttr.resize_trailing_extent(newWidthU16);
if (oldY == oldCursorPos.y && oldCursorPos.x >= oldX)
{
// In theory AdjustToGlyphStart ensures we don't put the cursor on a trailing wide glyph.
// In practice I don't think that this can possibly happen. Better safe than sorry.
newCursorPos = { newRow.AdjustToGlyphStart(oldCursorPos.x - oldX + newX), newY };
// If there's so much text past the old cursor position that it doesn't fit into new buffer,
// then the new cursor position will be "lost", because it's overwritten by unrelated text.
// We have two choices how can handle this:
// * If the new cursor is at an y < 0, just put the cursor at (0,0)
// * Stop writing into the new buffer before we overwrite the new cursor position
// This implements the second option. There's no fundamental reason why this is better.
newYLimit = newY + newHeight;
}
if (oldY >= mutableViewportTop)
{
positionInfo->mutableViewportTop = newY;
mutableViewportTop = til::CoordTypeMax;
}
if (oldY >= visibleViewportTop)
{
positionInfo->visibleViewportTop = newY;
visibleViewportTop = til::CoordTypeMax;
}
oldX = state.sourceColumnEnd;
newX = state.columnEnd;
} while (oldX < oldRowLimit);
// If the row had an explicit newline we also need to newline. :)
if (!oldRow.WasWrapForced())
{
newX = 0;
newY++;
}
}
// The for loop right after this if condition will copy entire rows of attributes at a time.
// This assumes of course that the "write cursor" (newX, newY) is at the start of a row.
// If we didn't check for this, we may otherwise copy attributes from a later row into a previous one.
if (newX != 0)
{
newX = 0;
newY++;
}
// Finish copying buffer attributes to remaining rows below the last
// printable character. This is to fix the `color 2f` scenario, where you
// change the buffer colors then resize and everything below the last
// printable char gets reset. See GH #12567
const auto initializedRowsEnd = oldBuffer._estimateOffsetOfLastCommittedRow() + 1;
for (; oldY < initializedRowsEnd && newY < newHeight; oldY++, newY++)
{
auto& oldRow = oldBuffer.GetRowByOffset(oldY);
auto& newRow = newBuffer.GetMutableRowByOffset(newY);
auto& newAttr = newRow.Attributes();
newAttr = oldRow.Attributes();
newAttr.resize_trailing_extent(newWidthU16);
}
// Since we didn't use IncrementCircularBuffer() we need to compute the proper
// _firstRow offset now, in a way that replicates IncrementCircularBuffer().
// We need to do the same for newCursorPos.y for basically the same reason.
if (newY > newHeight)
{
newBuffer._firstRow = newY % newHeight;
// _firstRow maps from API coordinates that always start at 0,0 in the top left corner of the
// terminal's scrollback, to the underlying buffer Y coordinate via `(y + _firstRow) % height`.
// Here, we need to un-map the `newCursorPos.y` from the underlying Y coordinate to the API coordinate
// and so we do `(y - _firstRow) % height`, but we add `+ newHeight` to avoid getting negative results.
newCursorPos.y = (newCursorPos.y - newBuffer._firstRow + newHeight) % newHeight;
}
newBuffer.CopyProperties(oldBuffer);
newBuffer.CopyHyperlinkMaps(oldBuffer);
assert(newCursorPos.x >= 0 && newCursorPos.x < newWidth);
assert(newCursorPos.y >= 0 && newCursorPos.y < newHeight);
newCursor.SetSize(oldCursor.GetSize());
newCursor.SetPosition(newCursorPos);
}
// Method Description:
// - Adds or updates a hyperlink in our hyperlink table
// Arguments:
// - The hyperlink URI, the hyperlink id (could be new or old)
void TextBuffer::AddHyperlinkToMap(std::wstring_view uri, uint16_t id)
{
_hyperlinkMap[id] = uri;
}
// Method Description:
// - Retrieves the URI associated with a particular hyperlink ID
// Arguments:
// - The hyperlink ID
// Return Value:
// - The URI
std::wstring TextBuffer::GetHyperlinkUriFromId(uint16_t id) const
{
return _hyperlinkMap.at(id);
}
// Method description:
// - Provides the hyperlink ID to be assigned as a text attribute, based on the optional custom id provided
// Arguments:
// - The user-defined id
// Return value:
// - The internal hyperlink ID
uint16_t TextBuffer::GetHyperlinkId(std::wstring_view uri, std::wstring_view id)
{
uint16_t numericId = 0;
if (id.empty())
{
// no custom id specified, return our internal count
numericId = _currentHyperlinkId;
++_currentHyperlinkId;
}
else
{
// assign _currentHyperlinkId if the custom id does not already exist
std::wstring newId{ id };
// hash the URL and add it to the custom ID - GH#7698
newId += L"%" + std::to_wstring(til::hash(uri));
const auto result = _hyperlinkCustomIdMap.emplace(newId, _currentHyperlinkId);
if (result.second)
{
// the custom id did not already exist
++_currentHyperlinkId;
}
numericId = (*(result.first)).second;
}
// _currentHyperlinkId could overflow, make sure its not 0
if (_currentHyperlinkId == 0)
{
++_currentHyperlinkId;
}
return numericId;
}
// Method Description:
// - Removes a hyperlink from the hyperlink map and the associated
// user defined id from the custom id map (if there is one)
// Arguments:
// - The ID of the hyperlink to be removed
void TextBuffer::RemoveHyperlinkFromMap(uint16_t id) noexcept
{
_hyperlinkMap.erase(id);
for (const auto& customIdPair : _hyperlinkCustomIdMap)
{
if (customIdPair.second == id)
{
_hyperlinkCustomIdMap.erase(customIdPair.first);
break;
}
}
}
// Method Description:
// - Obtains the custom ID, if there was one, associated with the
// uint16_t id of a hyperlink
// Arguments:
// - The uint16_t id of the hyperlink
// Return Value:
// - The custom ID if there was one, empty string otherwise
std::wstring TextBuffer::GetCustomIdFromId(uint16_t id) const
{
for (auto customIdPair : _hyperlinkCustomIdMap)
{
if (customIdPair.second == id)
{
return customIdPair.first;
}
}
return {};
}
// Method Description:
// - Copies the hyperlink/customID maps of the old buffer into this one,
// also copies currentHyperlinkId
// Arguments:
// - The other buffer
void TextBuffer::CopyHyperlinkMaps(const TextBuffer& other)
{
_hyperlinkMap = other._hyperlinkMap;
_hyperlinkCustomIdMap = other._hyperlinkCustomIdMap;
_currentHyperlinkId = other._currentHyperlinkId;
}
// Searches through the entire (committed) text buffer for `needle` and returns the coordinates in absolute coordinates.
// The end coordinates of the returned ranges are considered inclusive.
std::optional<std::vector<til::point_span>> TextBuffer::SearchText(const std::wstring_view& needle, SearchFlag flags) const
{
return SearchText(needle, flags, 0, til::CoordTypeMax);
}
// Searches through the given rows [rowBeg,rowEnd) for `needle` and returns the coordinates in absolute coordinates.
// While the end coordinates of the returned ranges are considered inclusive, the [rowBeg,rowEnd) range is half-open.
// Returns nullopt if the parameters were invalid (e.g. regex search was requested with an invalid regex)
std::optional<std::vector<til::point_span>> TextBuffer::SearchText(const std::wstring_view& needle, SearchFlag flags, til::CoordType rowBeg, til::CoordType rowEnd) const
{
rowEnd = std::min(rowEnd, _estimateOffsetOfLastCommittedRow() + 1);
std::vector<til::point_span> results;
// All whitespace strings would match the not-yet-written parts of the TextBuffer which would be weird.
if (allWhitespace(needle) || rowBeg >= rowEnd)
{
return results;
}
auto text = ICU::UTextFromTextBuffer(*this, rowBeg, rowEnd);
uint32_t icuFlags{ 0 };
WI_SetFlagIf(icuFlags, UREGEX_CASE_INSENSITIVE, WI_IsFlagSet(flags, SearchFlag::CaseInsensitive));
if (WI_IsFlagSet(flags, SearchFlag::RegularExpression))
{
WI_SetFlag(icuFlags, UREGEX_MULTILINE);
}
else
{
WI_SetFlag(icuFlags, UREGEX_LITERAL);
}
UErrorCode status = U_ZERO_ERROR;
const auto re = til::ICU::CreateRegex(needle, icuFlags, &status);
if (status > U_ZERO_ERROR)
{
return std::nullopt;
}
uregex_setUText(re.get(), &text, &status);
if (uregex_find(re.get(), -1, &status))
{
do
{
results.emplace_back(ICU::BufferRangeFromMatch(&text, re.get()));
} while (uregex_findNext(re.get(), &status));
}
return results;
}
// Collect up all the rows that were marked, and the data marked on that row.
// This is what should be used for hot paths, like updating the scrollbar.
std::vector<ScrollMark> TextBuffer::GetMarkRows() const
{
std::vector<ScrollMark> marks;
const auto bottom = _estimateOffsetOfLastCommittedRow();
for (auto y = 0; y <= bottom; y++)
{
const auto& row = GetRowByOffset(y);
const auto& data{ row.GetScrollbarData() };
if (data.has_value())
{
marks.emplace_back(y, *data);
}
}
return marks;
}
// Get all the regions for all the shell integration marks in the buffer.
// Marks will be returned in top-down order.
//
// This possibly iterates over every run in the buffer, so don't do this on a
// hot path. Just do this once per user input, if at all possible.
//
// Use `limit` to control how many you get, _starting from the bottom_. (e.g.
// limit=1 will just give you the "most recent mark").
std::vector<MarkExtents> TextBuffer::GetMarkExtents(size_t limit) const
{
if (limit == 0u)
{
return {};
}
std::vector<MarkExtents> marks{};
const auto bottom = _estimateOffsetOfLastCommittedRow();
auto lastPromptY = bottom;
for (auto promptY = bottom; promptY >= 0; promptY--)
{
const auto& currRow = GetRowByOffset(promptY);
auto& rowPromptData = currRow.GetScrollbarData();
if (!rowPromptData.has_value())
{
// This row didn't start a prompt, don't even look here.
continue;
}
// Future thought! In #11000 & #14792, we considered the possibility of
// scrolling to only an error mark, or something like that. Perhaps in
// the future, add a customizable filter that's a set of types of mark
// to include?
//
// For now, skip any "Default" marks, since those came from the UI. We
// just want the ones that correspond to shell integration.
if (rowPromptData->category == MarkCategory::Default)
{
continue;
}
// This row did start a prompt! Find the prompt that starts here.
// Presumably, no rows below us will have prompts, so pass in the last
// row with text as the bottom
marks.push_back(_scrollMarkExtentForRow(promptY, lastPromptY));
// operator>=(T, optional<U>) will return true if the optional is
// nullopt, unfortunately.
if (marks.size() >= limit)
{
break;
}
lastPromptY = promptY;
}
std::reverse(marks.begin(), marks.end());
return marks;
}
// Remove all marks between `start` & `end`, inclusive.
void TextBuffer::ClearMarksInRange(
const til::point start,
const til::point end)
{
auto top = std::clamp(std::min(start.y, end.y), 0, _height - 1);
auto bottom = std::clamp(std::max(start.y, end.y), 0, _estimateOffsetOfLastCommittedRow());
for (auto y = top; y <= bottom; y++)
{
auto& row = GetMutableRowByOffset(y);
auto& runs = row.Attributes().runs();
row.SetScrollbarData(std::nullopt);
for (auto& [attr, length] : runs)
{
attr.SetMarkAttributes(MarkKind::None);
}
}
}
void TextBuffer::ClearAllMarks()
{
ClearMarksInRange({ 0, 0 }, { _width - 1, _height - 1 });
}
// Collect up the extent of the prompt and possibly command and output for the
// mark that starts on this row.
MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
const til::CoordType bottomInclusive) const
{
const auto& startRow = GetRowByOffset(rowOffset);
const auto& rowPromptData = startRow.GetScrollbarData();
assert(rowPromptData.has_value());
MarkExtents mark{
.data = *rowPromptData,
};
bool startedPrompt = false;
bool startedCommand = false;
bool startedOutput = false;
MarkKind lastMarkKind = MarkKind::Output;
const auto endThisMark = [&](auto x, auto y) {
if (startedOutput)
{
mark.outputEnd = til::point{ x, y };
}
if (!startedOutput && startedCommand)
{
mark.commandEnd = til::point{ x, y };
}
if (!startedCommand)
{
mark.end = til::point{ x, y };
}
};
auto x = 0;
auto y = rowOffset;
til::point lastMarkedText{ x, y };
for (; y <= bottomInclusive; y++)
{
// Now we need to iterate over text attributes. We need to find a
// segment of Prompt attributes, we'll skip those. Then there should be
// Command attributes. Collect up all of those, till we get to the next
// Output attribute.
const auto& row = GetRowByOffset(y);
const auto runs = row.Attributes().runs();
x = 0;
for (const auto& [attr, length] : runs)
{
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
const auto markKind{ attr.GetMarkAttributes() };
if (markKind != MarkKind::None)
{
lastMarkedText = { nextX, y };
if (markKind == MarkKind::Prompt)
{
if (startedCommand || startedOutput)
{
// we got a _new_ prompt. bail out.
break;
}
if (!startedPrompt)
{
// We entered the first prompt here
startedPrompt = true;
mark.start = til::point{ x, y };
}
endThisMark(lastMarkedText.x, lastMarkedText.y);
}
else if (markKind == MarkKind::Command && startedPrompt)
{
startedCommand = true;
endThisMark(lastMarkedText.x, lastMarkedText.y);
}
else if ((markKind == MarkKind::Output) && startedPrompt)
{
startedOutput = true;
if (!mark.commandEnd.has_value())
{
// immediately just end the command at the start here, so we can treat this whole run as output
mark.commandEnd = mark.end;
startedCommand = true;
}
endThisMark(lastMarkedText.x, lastMarkedText.y);
}
// Otherwise, we've changed from any state -> any state, and it doesn't really matter.
lastMarkKind = markKind;
}
// advance to next run of text
x = nextX;
}
// we went over all the runs in this row, but we're not done yet. Keep iterating on the next row.
}
// Okay, we're at the bottom of the buffer? Yea, just return what we found.
if (!startedCommand)
{
// If we never got to a Command or Output run, then we never set .end.
// Set it here to the last run we saw.
endThisMark(lastMarkedText.x, lastMarkedText.y);
}
return mark;
}
std::wstring TextBuffer::_commandForRow(const til::CoordType rowOffset,
const til::CoordType bottomInclusive,
const bool clipAtCursor) const
{
std::wstring commandBuilder;
MarkKind lastMarkKind = MarkKind::Prompt;
const auto cursorPosition = GetCursor().GetPosition();
for (auto y = rowOffset; y <= bottomInclusive; y++)
{
const bool onCursorRow = clipAtCursor && y == cursorPosition.y;
// Now we need to iterate over text attributes. We need to find a
// segment of Prompt attributes, we'll skip those. Then there should be
// Command attributes. Collect up all of those, till we get to the next
// Output attribute.
const auto& row = GetRowByOffset(y);
const auto runs = row.Attributes().runs();
auto x = 0;
for (const auto& [attr, length] : runs)
{
auto nextX = gsl::narrow_cast<uint16_t>(x + length);
if (onCursorRow)
{
nextX = std::min(nextX, gsl::narrow_cast<uint16_t>(cursorPosition.x));
}
const auto markKind{ attr.GetMarkAttributes() };
if (markKind != lastMarkKind)
{
if (lastMarkKind == MarkKind::Command)
{
// We've changed away from being in a command. We're done.
// Return what we've gotten so far.
return commandBuilder;
}
// Otherwise, we've changed from any state -> any state, and it doesn't really matter.
lastMarkKind = markKind;
}
if (markKind == MarkKind::Command)
{
commandBuilder += row.GetText(x, nextX);
}
// advance to next run of text
x = nextX;
if (onCursorRow && x == cursorPosition.x)
{
return commandBuilder;
}
}
// we went over all the runs in this row, but we're not done yet. Keep iterating on the next row.
}
// Okay, we're at the bottom of the buffer? Yea, just return what we found.
return commandBuilder;
}
std::wstring TextBuffer::CurrentCommand() const
{
auto promptY = GetCursor().GetPosition().y;
for (; promptY >= 0; promptY--)
{
const auto& currRow = GetRowByOffset(promptY);
auto& rowPromptData = currRow.GetScrollbarData();
if (!rowPromptData.has_value())
{
// This row didn't start a prompt, don't even look here.
continue;
}
// This row did start a prompt! Find the prompt that starts here.
// Presumably, no rows below us will have prompts, so pass in the last
// row with text as the bottom
return _commandForRow(promptY, _estimateOffsetOfLastCommittedRow(), true);
}
return L"";
}
std::vector<std::wstring> TextBuffer::Commands() const
{
std::vector<std::wstring> commands{};
const auto bottom = _estimateOffsetOfLastCommittedRow();
auto lastPromptY = bottom;
for (auto promptY = bottom; promptY >= 0; promptY--)
{
const auto& currRow = GetRowByOffset(promptY);
auto& rowPromptData = currRow.GetScrollbarData();
if (!rowPromptData.has_value())
{
// This row didn't start a prompt, don't even look here.
continue;
}
// This row did start a prompt! Find the prompt that starts here.
// Presumably, no rows below us will have prompts, so pass in the last
// row with text as the bottom
auto foundCommand = _commandForRow(promptY, lastPromptY);
if (!foundCommand.empty())
{
commands.emplace_back(std::move(foundCommand));
}
lastPromptY = promptY;
}
std::reverse(commands.begin(), commands.end());
return commands;
}
void TextBuffer::StartPrompt()
{
const auto currentRowOffset = GetCursor().GetPosition().y;
auto& currentRow = GetMutableRowByOffset(currentRowOffset);
currentRow.StartPrompt();
_currentAttributes.SetMarkAttributes(MarkKind::Prompt);
}
bool TextBuffer::_createPromptMarkIfNeeded()
{
// We might get here out-of-order, without seeing a StartPrompt (FTCS A)
// first. Since StartPrompt actually sets up the prompt mark on the ROW, we
// need to do a bit of extra work here to start a new mark (if the last one
// wasn't in an appropriate state).
const auto mostRecentMarks = GetMarkExtents(1u);
if (!mostRecentMarks.empty())
{
const auto& mostRecentMark = til::at(mostRecentMarks, 0);
if (!mostRecentMark.HasOutput())
{
// The most recent command mark _didn't_ have output yet. Great!
// we'll leave it alone, and just start treating text as Command or Output.
return false;
}
// The most recent command mark had output. That suggests that either:
// * shell integration wasn't enabled (but the user would still
// like lines with enters to be marked as prompts)
// * or we're in the middle of a command that's ongoing.
// If it does have a command, then we're still in the output of
// that command.
// --> the current attrs should already be set to Output.
if (mostRecentMark.HasCommand())
{
return false;
}
// If the mark doesn't have any command - then we know we're
// playing silly games with just marking whole lines as prompts,
// then immediately going to output.
// --> Below, we'll add a new mark to this row.
}
// There were no marks at all!
// --> add a new mark to this row, set all the attrs in this row
// to be Prompt, and set the current attrs to Output.
auto& row = GetMutableRowByOffset(GetCursor().GetPosition().y);
row.StartPrompt();
return true;
}
bool TextBuffer::StartCommand()
{
const auto createdMark = _createPromptMarkIfNeeded();
_currentAttributes.SetMarkAttributes(MarkKind::Command);
return createdMark;
}
bool TextBuffer::StartOutput()
{
const auto createdMark = _createPromptMarkIfNeeded();
_currentAttributes.SetMarkAttributes(MarkKind::Output);
return createdMark;
}
// Find the row above the cursor where this most recent prompt started, and set
// the exit code on that row's scroll mark.
void TextBuffer::EndCurrentCommand(std::optional<unsigned int> error)
{
_currentAttributes.SetMarkAttributes(MarkKind::None);
for (auto y = GetCursor().GetPosition().y; y >= 0; y--)
{
auto& currRow = GetMutableRowByOffset(y);
auto& rowPromptData = currRow.GetScrollbarData();
if (rowPromptData.has_value())
{
currRow.EndOutput(error);
return;
}
}
}
void TextBuffer::SetScrollbarData(ScrollbarData mark, til::CoordType y)
{
auto& row = GetMutableRowByOffset(y);
row.SetScrollbarData(mark);
}
void TextBuffer::ManuallyMarkRowAsPrompt(til::CoordType y)
{
auto& row = GetMutableRowByOffset(y);
for (auto& [attr, len] : row.Attributes().runs())
{
attr.SetMarkAttributes(MarkKind::Prompt);
}
}
// This is an optimization used by the renderer to avoid scheduling a timer if not necessary;
// unlike the renderer, we know the committed range of our own buffer.
bool TextBuffer::ContainsBlinkAttributeInRegion(const Microsoft::Console::Types::Viewport& region) const
{
const auto top = region.Top();
auto bottom = std::min(region.BottomInclusive(), _estimateOffsetOfLastCommittedRow());
for (auto row = top; row < bottom; ++row)
{
const auto& r = GetRowByOffset(row);
for (const auto& attr : r.Attributes())
{
if (attr.IsBlinking())
{
return true;
}
}
}
return false;
}
bool TextBuffer::IsGlyphDoubleWidthAt(const til::point at) const
{
if (!_isRowCommitted(at.y)) [[unlikely]]
{
return false;
}
return _getRow(at.y).DbcsAttrAt(at.x) != DbcsAttribute::Single;
}