mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-11 04:38:24 -06:00
3586 lines
138 KiB
C++
3586 lines
138 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "textBuffer.hpp"
|
|
|
|
#include <til/hash.h>
|
|
#include <til/unicode.h>
|
|
|
|
#include "UTextAdapter.h"
|
|
#include "../../types/inc/GlyphWidth.hpp"
|
|
#include "../renderer/base/renderer.hpp"
|
|
#include "../types/inc/utils.hpp"
|
|
|
|
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));
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Call before inserting a character into the buffer.
|
|
// - This will ensure a consistent double byte state (KAttrs line) within the text buffer
|
|
// - It will attempt to correct the buffer if we're inserting an unexpected double byte character type
|
|
// and it will pad out the buffer if we're going to split a double byte sequence across two rows.
|
|
//Arguments:
|
|
// - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer
|
|
//Return Value:
|
|
// - true if we successfully prepared the buffer and moved the cursor
|
|
// - false otherwise (out of memory)
|
|
void TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute)
|
|
{
|
|
// Now compensate if we don't have enough space for the upcoming double byte sequence
|
|
// We only need to compensate for leading bytes
|
|
if (dbcsAttribute == DbcsAttribute::Leading)
|
|
{
|
|
const auto cursorPosition = GetCursor().GetPosition();
|
|
const auto lineWidth = GetLineWidth(cursorPosition.y);
|
|
|
|
// If we're about to lead on the last column in the row, we need to add a padding space
|
|
if (cursorPosition.x == lineWidth - 1)
|
|
{
|
|
// set that we're wrapping for double byte reasons
|
|
auto& row = GetMutableRowByOffset(cursorPosition.y);
|
|
row.SetDoubleBytePadded(true);
|
|
|
|
// then move the cursor forward and onto the next row
|
|
IncrementCursor();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
// In the future, these functions are expected to also deliver information about how many columns a grapheme occupies.
|
|
// (I know that mere UTF-16 code point iteration doesn't handle graphemes, but that's what we're working towards.)
|
|
size_t TextBuffer::GraphemeNext(const std::wstring_view& chars, size_t position) noexcept
|
|
{
|
|
return til::utf16_iterate_next(chars, position);
|
|
}
|
|
|
|
// It's the counterpart to GraphemeNext. See GraphemeNext.
|
|
size_t TextBuffer::GraphemePrev(const std::wstring_view& chars, size_t position) noexcept
|
|
{
|
|
return til::utf16_iterate_prev(chars, position);
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
}
|
|
|
|
const 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.
|
|
for (;;)
|
|
{
|
|
auto ptr = &*it;
|
|
const auto wch = *ptr;
|
|
size_t len = 1;
|
|
|
|
col++;
|
|
|
|
// Even in our slow-path we can avoid calling IsGlyphFullWidth if the current character is ASCII.
|
|
// It also allows us to skip the surrogate pair decoding at the same time.
|
|
if (wch >= 0x80)
|
|
{
|
|
if (til::is_surrogate(wch))
|
|
{
|
|
const auto it2 = it + 1;
|
|
if (til::is_leading_surrogate(wch) && it2 != end && til::is_trailing_surrogate(*it2))
|
|
{
|
|
len = 2;
|
|
}
|
|
else
|
|
{
|
|
ptr = &UNICODE_REPLACEMENT;
|
|
}
|
|
}
|
|
|
|
col += IsGlyphFullWidth({ ptr, len });
|
|
}
|
|
|
|
// If we ran out of columns, we need to always return `columnLimit` and not `cols`,
|
|
// because if we tried inserting a wide glyph into just 1 remaining column it will
|
|
// fail to fit, but that remaining column still has been used up. When the caller sees
|
|
// `columns == columnLimit` they will line-wrap and continue inserting into the next row.
|
|
if (col > columnLimit)
|
|
{
|
|
columns = columnLimit;
|
|
return gsl::narrow_cast<size_t>(it - beg);
|
|
}
|
|
|
|
// But if we simply ran out of text we just need to return the actual number of columns.
|
|
it += len;
|
|
if (it == end)
|
|
{
|
|
columns = col;
|
|
return chars.size();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
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);
|
|
}
|
|
|
|
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);
|
|
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:
|
|
// - Inserts one codepoint into the buffer at the current cursor position and advances the cursor as appropriate.
|
|
//Arguments:
|
|
// - chars - The codepoint to insert
|
|
// - dbcsAttribute - Double byte information associated with the codepoint
|
|
// - bAttr - Color data associated with the character
|
|
//Return Value:
|
|
// - true if we successfully inserted the character
|
|
// - false otherwise (out of memory)
|
|
void TextBuffer::InsertCharacter(const std::wstring_view chars,
|
|
const DbcsAttribute dbcsAttribute,
|
|
const TextAttribute attr)
|
|
{
|
|
// Ensure consistent buffer state for double byte characters based on the character type we're about to insert
|
|
_PrepareForDoubleByteSequence(dbcsAttribute);
|
|
|
|
// Get the current cursor position
|
|
const auto iRow = GetCursor().GetPosition().y; // row stored as logical position, not array position
|
|
const auto iCol = GetCursor().GetPosition().x; // column logical and array positions are equal.
|
|
|
|
// Get the row associated with the given logical position
|
|
auto& Row = GetMutableRowByOffset(iRow);
|
|
|
|
// Store character and double byte data
|
|
switch (dbcsAttribute)
|
|
{
|
|
case DbcsAttribute::Leading:
|
|
Row.ReplaceCharacters(iCol, 2, chars);
|
|
break;
|
|
case DbcsAttribute::Trailing:
|
|
Row.ReplaceCharacters(iCol - 1, 2, chars);
|
|
break;
|
|
default:
|
|
Row.ReplaceCharacters(iCol, 1, chars);
|
|
break;
|
|
}
|
|
|
|
// Store color data
|
|
Row.SetAttrToEnd(iCol, attr);
|
|
IncrementCursor();
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Inserts one ucs2 codepoint into the buffer at the current cursor position and advances the cursor as appropriate.
|
|
//Arguments:
|
|
// - wch - The codepoint to insert
|
|
// - dbcsAttribute - Double byte information associated with the codepoint
|
|
// - bAttr - Color data associated with the character
|
|
//Return Value:
|
|
// - true if we successfully inserted the character
|
|
// - false otherwise (out of memory)
|
|
void TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr)
|
|
{
|
|
InsertCharacter({ &wch, 1 }, dbcsAttribute, attr);
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Finds the current row in the buffer (as indicated by the cursor position)
|
|
// and specifies that we have forced a line wrap on that row
|
|
//Arguments:
|
|
// - <none> - Always sets to wrap
|
|
//Return Value:
|
|
// - <none>
|
|
void TextBuffer::_SetWrapOnCurrentRow()
|
|
{
|
|
_AdjustWrapOnCurrentRow(true);
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Finds the current row in the buffer (as indicated by the cursor position)
|
|
// and specifies whether or not it should have a line wrap flag.
|
|
//Arguments:
|
|
// - fSet - True if this row has a wrap. False otherwise.
|
|
//Return Value:
|
|
// - <none>
|
|
void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet)
|
|
{
|
|
// The vertical position of the cursor represents the current row we're manipulating.
|
|
const auto uiCurrentRowOffset = GetCursor().GetPosition().y;
|
|
|
|
// Set the wrap status as appropriate
|
|
GetMutableRowByOffset(uiCurrentRowOffset).SetWrapForced(fSet);
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Increments the cursor one position in the buffer as if text is being typed into the buffer.
|
|
// - NOTE: Will introduce a wrap marker if we run off the end of the current row
|
|
//Arguments:
|
|
// - <none>
|
|
//Return Value:
|
|
// - true if we successfully moved the cursor.
|
|
// - false otherwise (out of memory)
|
|
void TextBuffer::IncrementCursor()
|
|
{
|
|
// Cursor position is stored as logical array indices (starts at 0) for the window
|
|
// Buffer Size is specified as the "length" of the array. It would say 80 for valid values of 0-79.
|
|
// So subtract 1 from buffer size in each direction to find the index of the final column in the buffer
|
|
const auto iFinalColumnIndex = GetLineWidth(GetCursor().GetPosition().y) - 1;
|
|
|
|
// Move the cursor one position to the right
|
|
GetCursor().IncrementXPosition(1);
|
|
|
|
// If we've passed the final valid column...
|
|
if (GetCursor().GetPosition().x > iFinalColumnIndex)
|
|
{
|
|
// Then mark that we've been forced to wrap
|
|
_SetWrapOnCurrentRow();
|
|
|
|
// Then move the cursor to a new line
|
|
NewlineCursor();
|
|
}
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Increments the cursor one line down in the buffer and to the beginning of the line
|
|
//Arguments:
|
|
// - <none>
|
|
//Return Value:
|
|
// - true if we successfully moved the cursor.
|
|
void TextBuffer::NewlineCursor()
|
|
{
|
|
const auto iFinalRowIndex = GetSize().BottomInclusive();
|
|
|
|
// Reset the cursor position to 0 and move down one line
|
|
GetCursor().SetXPosition(0);
|
|
GetCursor().IncrementYPosition(1);
|
|
|
|
// If we've passed the final valid row...
|
|
if (GetCursor().GetPosition().y > iFinalRowIndex)
|
|
{
|
|
// Stay on the final logical/offset row of the buffer.
|
|
GetCursor().SetYPosition(iFinalRowIndex);
|
|
|
|
// Instead increment the circular buffer to move us into the "oldest" row of the backing buffer
|
|
IncrementCircularBuffer();
|
|
}
|
|
}
|
|
|
|
//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)
|
|
{
|
|
// FirstRow is at any given point in time the array index in the circular buffer that corresponds
|
|
// to the logical position 0 in the window (cursor coordinates and all other coordinates).
|
|
if (_isActiveBuffer)
|
|
{
|
|
_renderer.TriggerFlush(true);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves the position of the previous character relative to the current cursor position
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - Coordinate position in screen coordinates of the character just before the cursor.
|
|
// - NOTE: Will return 0,0 if already in the top left corner
|
|
til::point TextBuffer::_GetPreviousFromCursor() const
|
|
{
|
|
auto coordPosition = GetCursor().GetPosition();
|
|
|
|
// If we're not at the left edge, simply move the cursor to the left by one
|
|
if (coordPosition.x > 0)
|
|
{
|
|
coordPosition.x--;
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, only if we're not on the top row (e.g. we don't move anywhere in the top left corner. there is no previous)
|
|
if (coordPosition.y > 0)
|
|
{
|
|
// move the cursor up one line
|
|
coordPosition.y--;
|
|
|
|
// and to the right edge
|
|
coordPosition.x = GetLineWidth(coordPosition.y) - 1;
|
|
}
|
|
}
|
|
|
|
return coordPosition;
|
|
}
|
|
|
|
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)
|
|
{
|
|
GetMutableRowByOffset(y + delta).CopyFrom(GetRowByOffset(y));
|
|
}
|
|
}
|
|
|
|
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 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
|
|
{
|
|
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;
|
|
}
|
|
|
|
void TextBuffer::ClearScrollback(const til::CoordType start, const til::CoordType height)
|
|
{
|
|
if (start <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (height <= 0)
|
|
{
|
|
_decommit();
|
|
return;
|
|
}
|
|
|
|
// 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 start 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 + start;
|
|
_firstRow = 0;
|
|
ScrollRows(startAbsolute, height, -startAbsolute);
|
|
|
|
const auto end = _estimateOffsetOfLastCommittedRow();
|
|
for (auto y = height; y <= end; ++y)
|
|
{
|
|
GetMutableRowByOffset(y).Reset(_initialAttributes);
|
|
}
|
|
|
|
ClearMarksInRange(til::point{ 0, height }, til::point{ _width, _height });
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
newBuffer.GetMutableRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow));
|
|
}
|
|
|
|
// 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.NotifyPaintFrame();
|
|
}
|
|
}
|
|
|
|
void TextBuffer::TriggerRedraw(const Viewport& viewport)
|
|
{
|
|
if (_isActiveBuffer)
|
|
{
|
|
_renderer.TriggerRedraw(viewport);
|
|
}
|
|
}
|
|
|
|
void TextBuffer::TriggerRedrawAll()
|
|
{
|
|
if (_isActiveBuffer)
|
|
{
|
|
_renderer.TriggerRedrawAll();
|
|
}
|
|
}
|
|
|
|
void TextBuffer::TriggerScroll()
|
|
{
|
|
if (_isActiveBuffer)
|
|
{
|
|
_renderer.TriggerScroll();
|
|
}
|
|
}
|
|
|
|
void TextBuffer::TriggerScroll(const til::point delta)
|
|
{
|
|
if (_isActiveBuffer)
|
|
{
|
|
_renderer.TriggerScroll(&delta);
|
|
}
|
|
}
|
|
|
|
void TextBuffer::TriggerNewTextNotification(const std::wstring_view newText)
|
|
{
|
|
if (_isActiveBuffer)
|
|
{
|
|
_renderer.TriggerNewTextNotification(newText);
|
|
}
|
|
}
|
|
|
|
// 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 til::point for the beginning of the word you are on
|
|
// Arguments:
|
|
// - target - a til::point on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// - accessibilityMode - when enabled, we continue expanding left until we are at the beginning of a readable word.
|
|
// Otherwise, expand left until a character of a new delimiter class is found
|
|
// (or a row boundary is encountered)
|
|
// - limitOptional - (optional) the last possible position in the buffer that can be explored. This can be used to improve performance.
|
|
// Return Value:
|
|
// - The til::point for the first character on the "word" (inclusive)
|
|
til::point TextBuffer::GetWordStart(const til::point target, const std::wstring_view wordDelimiters, bool accessibilityMode, std::optional<til::point> limitOptional) const
|
|
{
|
|
// Consider a buffer with this text in it:
|
|
// " word other "
|
|
// In selection (accessibilityMode = false),
|
|
// a "word" is defined as the range between two delimiters
|
|
// so the words in the example include [" ", "word", " ", "other", " "]
|
|
// In accessibility (accessibilityMode = true),
|
|
// a "word" includes the delimiters after a range of readable characters
|
|
// so the words in the example include ["word ", "other "]
|
|
// NOTE: the start anchor (this one) is inclusive, whereas the end anchor (GetWordEnd) is exclusive
|
|
|
|
#pragma warning(suppress : 26496)
|
|
auto copy{ target };
|
|
const auto bufferSize{ GetSize() };
|
|
const auto limit{ limitOptional.value_or(bufferSize.EndExclusive()) };
|
|
if (target == bufferSize.Origin())
|
|
{
|
|
// can't expand left
|
|
return target;
|
|
}
|
|
else if (target == bufferSize.EndExclusive())
|
|
{
|
|
// GH#7664: Treat EndExclusive as EndInclusive so
|
|
// that it actually points to a space in the buffer
|
|
copy = bufferSize.BottomRightInclusive();
|
|
}
|
|
else if (bufferSize.CompareInBounds(target, limit, true) >= 0)
|
|
{
|
|
// if at/past the limit --> clamp to limit
|
|
copy = limitOptional.value_or(bufferSize.BottomRightInclusive());
|
|
}
|
|
|
|
if (accessibilityMode)
|
|
{
|
|
return _GetWordStartForAccessibility(copy, wordDelimiters);
|
|
}
|
|
else
|
|
{
|
|
return _GetWordStartForSelection(copy, wordDelimiters);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for GetWordStart(). Get the til::point for the beginning of the word (accessibility definition) you are on
|
|
// Arguments:
|
|
// - target - a til::point on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// Return Value:
|
|
// - The til::point for the first character on the current/previous READABLE "word" (inclusive)
|
|
til::point TextBuffer::_GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const
|
|
{
|
|
auto result = target;
|
|
const auto bufferSize = GetSize();
|
|
|
|
// ignore left boundary. Continue until readable text found
|
|
while (_GetDelimiterClassAt(result, wordDelimiters) != DelimiterClass::RegularChar)
|
|
{
|
|
if (result == bufferSize.Origin())
|
|
{
|
|
//looped around and hit origin (no word between origin and target)
|
|
return result;
|
|
}
|
|
bufferSize.DecrementInBounds(result);
|
|
}
|
|
|
|
// make sure we expand to the left boundary or the beginning of the word
|
|
while (_GetDelimiterClassAt(result, wordDelimiters) == DelimiterClass::RegularChar)
|
|
{
|
|
if (result == bufferSize.Origin())
|
|
{
|
|
// first char in buffer is a RegularChar
|
|
// we can't move any further back
|
|
return result;
|
|
}
|
|
bufferSize.DecrementInBounds(result);
|
|
}
|
|
|
|
// move off of delimiter
|
|
bufferSize.IncrementInBounds(result);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for GetWordStart(). Get the til::point for the beginning of the word (selection definition) you are on
|
|
// Arguments:
|
|
// - target - a til::point on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// Return Value:
|
|
// - The til::point for the first character on the current word or delimiter run (stopped by the left margin)
|
|
til::point TextBuffer::_GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const
|
|
{
|
|
auto result = target;
|
|
const auto bufferSize = GetSize();
|
|
|
|
const auto initialDelimiter = _GetDelimiterClassAt(result, wordDelimiters);
|
|
const bool isControlChar = initialDelimiter == DelimiterClass::ControlChar;
|
|
|
|
// expand left until we hit the left boundary or a different delimiter class
|
|
while (result != bufferSize.Origin() && _GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter)
|
|
{
|
|
if (result.x == bufferSize.Left())
|
|
{
|
|
// Prevent wrapping to the previous line if the selection begins on whitespace
|
|
if (isControlChar)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (result.y > 0)
|
|
{
|
|
// Prevent wrapping to the previous line if it was hard-wrapped (e.g. not forced by us to wrap)
|
|
const auto& priorRow = GetRowByOffset(result.y - 1);
|
|
if (!priorRow.WasWrapForced())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
bufferSize.DecrementInBounds(result);
|
|
}
|
|
|
|
if (_GetDelimiterClassAt(result, wordDelimiters) != initialDelimiter)
|
|
{
|
|
// move off of delimiter
|
|
bufferSize.IncrementInBounds(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Get the til::point for the beginning of the NEXT word
|
|
// Arguments:
|
|
// - target - a til::point on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// - accessibilityMode - when enabled, we continue expanding right until we are at the beginning of the next READABLE word
|
|
// Otherwise, expand right until a character of a new delimiter class is found
|
|
// (or a row boundary is encountered)
|
|
// - limitOptional - (optional) the last possible position in the buffer that can be explored. This can be used to improve performance.
|
|
// Return Value:
|
|
// - The til::point for the last character on the "word" (inclusive)
|
|
til::point TextBuffer::GetWordEnd(const til::point target, const std::wstring_view wordDelimiters, bool accessibilityMode, std::optional<til::point> limitOptional) const
|
|
{
|
|
// Consider a buffer with this text in it:
|
|
// " word other "
|
|
// In selection (accessibilityMode = false),
|
|
// a "word" is defined as the range between two delimiters
|
|
// so the words in the example include [" ", "word", " ", "other", " "]
|
|
// In accessibility (accessibilityMode = true),
|
|
// a "word" includes the delimiters after a range of readable characters
|
|
// so the words in the example include ["word ", "other "]
|
|
// NOTE: the end anchor (this one) is exclusive, whereas the start anchor (GetWordStart) is inclusive
|
|
|
|
// Already at/past the limit. Can't move forward.
|
|
const auto bufferSize{ GetSize() };
|
|
const auto limit{ limitOptional.value_or(bufferSize.EndExclusive()) };
|
|
if (bufferSize.CompareInBounds(target, limit, true) >= 0)
|
|
{
|
|
return target;
|
|
}
|
|
|
|
if (accessibilityMode)
|
|
{
|
|
return _GetWordEndForAccessibility(target, wordDelimiters, limit);
|
|
}
|
|
else
|
|
{
|
|
return _GetWordEndForSelection(target, wordDelimiters);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for GetWordEnd(). Get the til::point for the beginning of the next READABLE word
|
|
// Arguments:
|
|
// - target - a til::point on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// - limit - the last "valid" position in the text buffer (to improve performance)
|
|
// Return Value:
|
|
// - The til::point for the first character of the next readable "word". If no next word, return one past the end of the buffer
|
|
til::point TextBuffer::_GetWordEndForAccessibility(const til::point target, const std::wstring_view wordDelimiters, const til::point limit) const
|
|
{
|
|
const auto bufferSize{ GetSize() };
|
|
auto result{ target };
|
|
|
|
if (bufferSize.CompareInBounds(target, limit, true) >= 0)
|
|
{
|
|
// if we're already on/past the last RegularChar,
|
|
// clamp result to that position
|
|
result = limit;
|
|
|
|
// make the result exclusive
|
|
bufferSize.IncrementInBounds(result, true);
|
|
}
|
|
else
|
|
{
|
|
while (result != limit && result != bufferSize.BottomRightInclusive() && _GetDelimiterClassAt(result, wordDelimiters) == DelimiterClass::RegularChar)
|
|
{
|
|
// Iterate through readable text
|
|
bufferSize.IncrementInBounds(result);
|
|
}
|
|
|
|
while (result != limit && result != bufferSize.BottomRightInclusive() && _GetDelimiterClassAt(result, wordDelimiters) != DelimiterClass::RegularChar)
|
|
{
|
|
// expand to the beginning of the NEXT word
|
|
bufferSize.IncrementInBounds(result);
|
|
}
|
|
|
|
// Special case: we tried to move one past the end of the buffer
|
|
// Manually increment onto the EndExclusive point.
|
|
if (result == bufferSize.BottomRightInclusive())
|
|
{
|
|
bufferSize.IncrementInBounds(result, true);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for GetWordEnd(). Get the til::point for the beginning of the NEXT word
|
|
// Arguments:
|
|
// - target - a til::point on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// Return Value:
|
|
// - The til::point for the last character of the current word or delimiter run (stopped by right margin)
|
|
til::point TextBuffer::_GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const
|
|
{
|
|
const auto bufferSize = GetSize();
|
|
|
|
auto result = target;
|
|
const auto initialDelimiter = _GetDelimiterClassAt(result, wordDelimiters);
|
|
const bool isControlChar = initialDelimiter == DelimiterClass::ControlChar;
|
|
|
|
// expand right until we hit the right boundary as a ControlChar or a different delimiter class
|
|
while (result != bufferSize.BottomRightInclusive() && _GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter)
|
|
{
|
|
if (result.x == bufferSize.RightInclusive())
|
|
{
|
|
// Prevent wrapping to the next line if the selection begins on whitespace
|
|
if (isControlChar)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Prevent wrapping to the next line if this one was hard-wrapped (e.g. not forced by us to wrap)
|
|
const auto& row = GetRowByOffset(result.y);
|
|
if (!row.WasWrapForced())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
bufferSize.IncrementInBoundsCircular(result);
|
|
}
|
|
|
|
if (_GetDelimiterClassAt(result, wordDelimiters) != initialDelimiter)
|
|
{
|
|
// move off of delimiter
|
|
bufferSize.DecrementInBounds(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
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 position of the first character of the next word. This is used for accessibility
|
|
// Arguments:
|
|
// - pos - a til::point on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// - limitOptional - (optional) the last possible position in the buffer that can be explored. This can be used to improve performance.
|
|
// 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 character on the "word" (inclusive)
|
|
bool TextBuffer::MoveToNextWord(til::point& pos, const std::wstring_view wordDelimiters, std::optional<til::point> limitOptional) const
|
|
{
|
|
// move to the beginning of the next word
|
|
// NOTE: _GetWordEnd...() returns the exclusive position of the "end of the word"
|
|
// This is also the inclusive start of the next word.
|
|
const auto bufferSize{ GetSize() };
|
|
const auto limit{ limitOptional.value_or(bufferSize.EndExclusive()) };
|
|
const auto copy{ _GetWordEndForAccessibility(pos, wordDelimiters, limit) };
|
|
|
|
if (bufferSize.CompareInBounds(copy, limit, true) >= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
pos = copy;
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update pos to be the position of the first character of the previous word. This is used for accessibility
|
|
// Arguments:
|
|
// - pos - a til::point on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// 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 character on the "word" (inclusive)
|
|
bool TextBuffer::MoveToPreviousWord(til::point& pos, std::wstring_view wordDelimiters) const
|
|
{
|
|
// move to the beginning of the current word
|
|
auto copy{ GetWordStart(pos, wordDelimiters, true) };
|
|
|
|
if (!GetSize().DecrementInBounds(copy, true))
|
|
{
|
|
// can't move behind current word
|
|
return false;
|
|
}
|
|
|
|
// move to the beginning of the previous word
|
|
pos = GetWordStart(copy, wordDelimiters, true);
|
|
return true;
|
|
}
|
|
|
|
// 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 (bufferSize.CompareInBounds(resultPos, limit, true) > 0)
|
|
{
|
|
resultPos = limit;
|
|
}
|
|
|
|
// limit is exclusive, so we need to move back to be within valid bounds
|
|
if (resultPos != limit && 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 (bufferSize.CompareInBounds(resultPos, limit, true) > 0)
|
|
{
|
|
resultPos = limit;
|
|
}
|
|
|
|
if (resultPos != limit && 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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
|
|
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
|
|
const auto [higherCoord, lowerCoord] = bufferSize.CompareInBounds(start, end) <= 0 ?
|
|
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
|
|
{
|
|
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 (inclusive)
|
|
// - 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 (GetCellDataAt(targetPoint)->DbcsAttr() == DbcsAttribute::Trailing)
|
|
{
|
|
if (targetPoint.x == bufferSize.Left())
|
|
{
|
|
bufferSize.IncrementInBounds(targetPoint);
|
|
}
|
|
else
|
|
{
|
|
bufferSize.DecrementInBounds(targetPoint);
|
|
}
|
|
textRow.left = targetPoint.x;
|
|
}
|
|
|
|
// expand right side of rect
|
|
targetPoint = { textRow.right, textRow.bottom };
|
|
if (GetCellDataAt(targetPoint)->DbcsAttr() == DbcsAttribute::Leading)
|
|
{
|
|
if (targetPoint.x == bufferSize.RightInclusive())
|
|
{
|
|
bufferSize.DecrementInBounds(targetPoint);
|
|
}
|
|
else
|
|
{
|
|
bufferSize.IncrementInBounds(targetPoint);
|
|
}
|
|
textRow.right = targetPoint.x;
|
|
}
|
|
}
|
|
|
|
size_t TextBuffer::SpanLength(const til::point coordStart, const til::point coordEnd) const
|
|
{
|
|
const auto bufferSize = GetSize();
|
|
// The coords are inclusive, so to get the (inclusive) length we add 1.
|
|
const auto length = bufferSize.CompareInBounds(coordEnd, coordStart) + 1;
|
|
return gsl::narrow<size_t>(length);
|
|
}
|
|
|
|
// 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")
|
|
// - end - where to end getting text
|
|
// 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 + 1; // +1 to get an exclusive end
|
|
}
|
|
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 + 1; // +1 to get an exclusive end
|
|
}
|
|
|
|
// 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
|
|
selectedText += row.GetText(rowBeg, rowEnd);
|
|
|
|
if (addLineBreak && iRow != req.end.y)
|
|
{
|
|
selectedText += L"\r\n";
|
|
}
|
|
}
|
|
|
|
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 (isIntenseBold && attr.IsIntense())
|
|
{
|
|
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 += "<";
|
|
break;
|
|
case '>':
|
|
htmlBuilder += ">";
|
|
break;
|
|
case '&':
|
|
htmlBuilder += "&";
|
|
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 (isIntenseBold && attr.IsIntense())
|
|
{
|
|
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::Serialize(const wchar_t* destination) const
|
|
{
|
|
const wil::unique_handle file{ CreateFileW(destination, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
|
|
THROW_LAST_ERROR_IF(!file);
|
|
|
|
static constexpr size_t writeThreshold = 32 * 1024;
|
|
std::wstring buffer;
|
|
buffer.reserve(writeThreshold + writeThreshold / 2);
|
|
buffer.push_back(L'\uFEFF');
|
|
|
|
const til::CoordType lastRowWithText = GetLastNonSpaceCharacter(nullptr).y;
|
|
CharacterAttributes previousAttr = CharacterAttributes::Unused1;
|
|
TextColor previousFg;
|
|
TextColor previousBg;
|
|
TextColor previousUl;
|
|
uint16_t previousHyperlinkId = 0;
|
|
|
|
// 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 = 0;; currentRow++)
|
|
{
|
|
const auto& row = GetRowByOffset(currentRow);
|
|
|
|
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& runs = row.Attributes().runs();
|
|
auto it = runs.begin();
|
|
const auto end = runs.end();
|
|
const auto last = end - 1;
|
|
til::CoordType oldX = 0;
|
|
|
|
for (; it != end; ++it)
|
|
{
|
|
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));
|
|
}
|
|
|
|
previousAttr = attr;
|
|
}
|
|
|
|
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;
|
|
}
|
|
previousFg = fg;
|
|
}
|
|
|
|
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;
|
|
}
|
|
previousBg = bg;
|
|
}
|
|
|
|
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;
|
|
}
|
|
previousUl = ul;
|
|
}
|
|
|
|
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\\");
|
|
previousHyperlinkId = hyperlinkId;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
buffer.append(L"\x1b]8;;\x1b\\");
|
|
previousHyperlinkId = 0;
|
|
}
|
|
}
|
|
|
|
auto newX = oldX + it->length;
|
|
// Trim whitespace with default attributes from the end of each line.
|
|
if (it == last && it->value == TextAttribute{})
|
|
{
|
|
// This can result in oldX > newX, but that's okay because GetText()
|
|
// is robust against that and returns an empty string.
|
|
newX = row.MeasureRight();
|
|
}
|
|
|
|
buffer.append(row.GetText(oldX, newX));
|
|
oldX = newX;
|
|
}
|
|
|
|
const auto moreRowsRemaining = currentRow < lastRowWithText;
|
|
|
|
if (!row.WasWrapForced() || !moreRowsRemaining)
|
|
{
|
|
buffer.append(L"\r\n");
|
|
}
|
|
|
|
if (buffer.size() >= writeThreshold || !moreRowsRemaining)
|
|
{
|
|
const auto fileSize = gsl::narrow<DWORD>(buffer.size() * sizeof(wchar_t));
|
|
DWORD bytesWritten = 0;
|
|
THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), buffer.data(), fileSize, &bytesWritten, nullptr));
|
|
THROW_WIN32_IF_MSG(ERROR_WRITE_FAULT, bytesWritten != fileSize, "failed to write");
|
|
buffer.clear();
|
|
}
|
|
|
|
if (!moreRowsRemaining)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
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++;
|
|
}
|
|
}
|
|
|
|
// 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::vector<til::point_span> TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive) const
|
|
{
|
|
return SearchText(needle, caseInsensitive, 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.
|
|
std::vector<til::point_span> TextBuffer::SearchText(const std::wstring_view& needle, bool caseInsensitive, 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 flags = UREGEX_LITERAL;
|
|
WI_SetFlagIf(flags, UREGEX_CASE_INSENSITIVE, caseInsensitive);
|
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
|
const auto re = ICU::CreateRegex(needle, flags, &status);
|
|
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
|
|
{
|
|
std::wstring commandBuilder;
|
|
MarkKind lastMarkKind = MarkKind::Prompt;
|
|
for (auto y = rowOffset; 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();
|
|
auto 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 != 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;
|
|
}
|
|
// 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());
|
|
}
|
|
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);
|
|
}
|
|
}
|