Compare commits

...

8 Commits

Author SHA1 Message Date
Leonard Hecker
2aca3103a6 Typo 2026-02-13 22:58:44 +01:00
Leonard Hecker
9841d1feaa Fix AuditMode nagging 2026-02-13 21:27:29 +01:00
Leonard Hecker
0143683891 Merge remote-tracking branch 'origin/main' into dev/lhecker/1410-large-scrollback 2026-02-13 21:26:05 +01:00
Leonard Hecker
8835876b89 Fix decommit, Fix tests 2025-10-17 10:45:05 +02:00
Leonard Hecker
7cb5a4bc78 Fix build 2025-10-16 18:50:46 +02:00
Leonard Hecker
c9d27e779f Merge remote-tracking branch 'origin/main' into dev/lhecker/1410-large-scrollback 2025-10-16 18:45:16 +02:00
Leonard Hecker
5e68a08f80 Fix spelling 2025-09-01 23:45:57 +02:00
Leonard Hecker
971dba737b Can we have infinite scrollback? 2025-09-01 23:42:06 +02:00
17 changed files with 156 additions and 156 deletions

View File

@@ -393,6 +393,7 @@ DECNKM
DECNRCM
DECOM
decommit
decommitting
DECPCCM
DECPCTERM
DECPS
@@ -1820,6 +1821,7 @@ uwa
uwp
uwu
uxtheme
VADs
Vanara
vararg
vclib

View File

@@ -3035,7 +3035,7 @@
"type": "boolean"
},
"historySize": {
"default": 9001,
"default": 16384,
"description": "The number of lines above the ones displayed in the window you can scroll back to.",
"minimum": -1,
"type": "integer"

View File

@@ -25,7 +25,7 @@ OutputCellRect::OutputCellRect(const til::CoordType rows, const til::CoordType c
_rows(rows),
_cols(cols)
{
_storage.resize(gsl::narrow<size_t>(rows * cols));
_storage.resize(gsl::narrow<size_t>(static_cast<til::HugeCoordType>(rows) * cols));
}
// Routine Description:
@@ -61,7 +61,7 @@ OutputCellIterator OutputCellRect::GetRowIter(const til::CoordType row) const
// - Pointer to the location in the rectangle that represents the start of the requested row.
OutputCell* OutputCellRect::_FindRowOffset(const til::CoordType row)
{
return &_storage.at(gsl::narrow_cast<size_t>(row * _cols));
return &_storage.at(gsl::narrow<size_t>(static_cast<til::HugeCoordType>(row) * _cols));
}
// Routine Description:
@@ -73,7 +73,7 @@ OutputCell* OutputCellRect::_FindRowOffset(const til::CoordType row)
// - Pointer to the location in the rectangle that represents the start of the requested row.
const OutputCell* OutputCellRect::_FindRowOffset(const til::CoordType row) const
{
return &_storage.at(gsl::narrow_cast<size_t>(row * _cols));
return &_storage.at(gsl::narrow<size_t>(static_cast<til::HugeCoordType>(row) * _cols));
}
// Routine Description:

View File

@@ -60,9 +60,6 @@ TextBuffer::TextBuffer(til::size screenBufferSize,
_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);
}
@@ -70,7 +67,7 @@ TextBuffer::~TextBuffer()
{
if (_buffer)
{
_destroy();
_destroy(_buffer.get());
}
}
@@ -91,8 +88,9 @@ TextBuffer::~TextBuffer()
// 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);
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
const auto w = std::clamp(screenBufferSize.width, 1, 0xffff);
const auto h = std::clamp(screenBufferSize.height, 1, til::CoordTypeMax / 2);
constexpr auto rowSize = ROW::CalculateRowSize();
const auto charsBufferSize = ROW::CalculateCharsBufferSize(w);
@@ -103,7 +101,7 @@ void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defau
// 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 rowCount = gsl::narrow_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().
@@ -142,31 +140,71 @@ __declspec(noinline) void TextBuffer::_commit(const std::byte* row)
_construct(_commitWatermark + size);
}
// Destructs and MEM_DECOMMITs all previously constructed ROWs.
// Destructs and MEM_DECOMMITs all rows between [rowsToKeep,_commitWatermark).
// You can use this (or rather the Reset() method) to fully clear the TextBuffer.
void TextBuffer::_decommit() noexcept
void TextBuffer::_decommit(til::CoordType rowsToKeep) noexcept
{
_destroy();
VirtualFree(_buffer.get(), 0, MEM_DECOMMIT);
_commitWatermark = _buffer.get();
// Amount of bytes that have been allocated with MEM_COMMIT so far.
const auto commitBytes = gsl::narrow_cast<size_t>(_commitWatermark - _buffer.get());
auto newWatermark = _buffer.get();
size_t pageOffset = 0;
if (rowsToKeep > 0)
{
SYSTEM_INFO si;
GetSystemInfo(&si);
rowsToKeep = std::min(rowsToKeep, _height);
// +1 for the scratchpad row at offset 0.
const auto rows = gsl::narrow_cast<size_t>(rowsToKeep) + 1;
// Offset in bytes to the first row that we were asked to destroy.
// We must ensure that the offset is not past the end of the current _commitWatermark,
// since we don't want to finish with a watermark that's somehow larger than what we started with.
const auto byteOffset = std::min(commitBytes, rows * _bufferRowStride);
newWatermark = _buffer.get() + byteOffset;
// Since the last row we were asked to keep may reside in the middle
// of a page, we must round the offset up to the next page boundary.
// That offset will tell us the offset at which we will MEM_DECOMMIT memory.
const auto pageMask = gsl::narrow_cast<size_t>(si.dwPageSize) - 1;
pageOffset = (byteOffset + pageMask) & ~pageMask;
}
// _destroy() takes care to check that the given pointer is valid.
_destroy(newWatermark);
// MEM_DECOMMIT the memory that we don't need anymore.
if (pageOffset < commitBytes)
{
// The analyzer can't know that we're intentionally decommitting memory here.
#pragma warning(suppress : 6250) // Calling 'VirtualFree' without the MEM_RELEASE flag may free memory but not address descriptors (VADs). This causes address space leaks.
VirtualFree(_buffer.get() + pageOffset, commitBytes - pageOffset, MEM_DECOMMIT);
}
_commitWatermark = newWatermark;
}
// Constructs ROWs between [_commitWatermark,until).
void TextBuffer::_construct(const std::byte* until) noexcept
{
// _width has been validated to fit into uint16_t during reserve().
const auto width = gsl::narrow_cast<uint16_t>(_width);
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);
std::construct_at(row, chars, indices, width, _initialAttributes);
}
}
// Destructs ROWs between [_buffer,_commitWatermark).
void TextBuffer::_destroy() const noexcept
// Destructs ROWs between [it,_commitWatermark).
void TextBuffer::_destroy(std::byte* it) const noexcept
{
for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride)
for (; it < _commitWatermark; it += _bufferRowStride)
{
std::destroy_at(reinterpret_cast<ROW*>(it));
}
@@ -974,7 +1012,7 @@ til::point TextBuffer::BufferToScreenPosition(const til::point position) const
// and the default current color attributes
void TextBuffer::Reset() noexcept
{
_decommit();
_decommit(0);
_initialAttributes = _currentAttributes;
}
@@ -988,30 +1026,22 @@ void TextBuffer::ClearScrollback(const til::CoordType newFirstRow, const til::Co
{
return;
}
// The new viewport should keep 0 rows? Then just reset everything.
if (rowsToKeep <= 0)
if (rowsToKeep > 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 newFirstRow parameter is relative to the _firstRow. The trick to get the content to the absolute start
// is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into
// the absolute start while reading from relative coordinates. This works because GetRowByOffset()
// operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue.
const auto startAbsolute = _firstRow + newFirstRow;
_firstRow = 0;
ScrollRows(startAbsolute, rowsToKeep, -startAbsolute);
}
ClearMarksInRange(til::point{ 0, 0 }, til::point{ _width, std::max(0, newFirstRow - 1) });
// Our goal is to move the viewport to the absolute start of the underlying memory buffer so that we can
// MEM_DECOMMIT the remaining memory. _firstRow is used to make the TextBuffer behave like a circular buffer.
// The newFirstRow parameter is relative to the _firstRow. The trick to get the content to the absolute start
// is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into
// the absolute start while reading from relative coordinates. This works because GetRowByOffset()
// operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue.
const auto startAbsolute = _firstRow + newFirstRow;
_firstRow = 0;
ScrollRows(startAbsolute, rowsToKeep, -startAbsolute);
const auto end = _estimateOffsetOfLastCommittedRow();
for (auto y = rowsToKeep; y <= end; ++y)
{
GetMutableRowByOffset(y).Reset(_initialAttributes);
}
// NOTE: If rowsToKeep is 0 we'll fall through to here and just decommit everything.
_decommit(rowsToKeep);
}
// Routine Description:
@@ -1993,7 +2023,7 @@ std::vector<til::point_span> TextBuffer::GetTextSpans(til::point start, til::poi
const auto rects = GetTextRects(start, end, /*blockSelection*/ true, bufferCoordinates);
textSpans.reserve(rects.size());
for (auto rect : rects)
for (const auto& rect : rects)
{
const til::point first = { rect.left, rect.top };
const til::point second = { rect.right, rect.bottom };
@@ -2260,7 +2290,7 @@ std::string TextBuffer::GenHTML(const CopyRequest& req,
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();
const auto& runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
auto x = rowBegU16;
for (const auto& [attr, length] : runs)
@@ -2510,7 +2540,7 @@ std::string TextBuffer::GenRTF(const CopyRequest& req,
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();
const auto& runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
auto x = rowBegU16;
for (auto& [attr, length] : runs)
@@ -2699,7 +2729,7 @@ void TextBuffer::_SerializeRow(const ROW& row, const til::CoordType startX, cons
const auto startXU16 = gsl::narrow_cast<uint16_t>(startX);
const auto endXU16 = gsl::narrow_cast<uint16_t>(endX);
const auto runs = row.Attributes().slice(startXU16, endXU16).runs();
const auto& runs = row.Attributes().slice(startXU16, endXU16).runs();
const auto beg = runs.begin();
const auto end = runs.end();
@@ -3292,7 +3322,7 @@ void TextBuffer::RemoveHyperlinkFromMap(uint16_t id) noexcept
// - The custom ID if there was one, empty string otherwise
std::wstring TextBuffer::GetCustomIdFromId(uint16_t id) const
{
for (auto customIdPair : _hyperlinkCustomIdMap)
for (const auto& customIdPair : _hyperlinkCustomIdMap)
{
if (customIdPair.second == id)
{
@@ -3488,7 +3518,6 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
bool startedPrompt = false;
bool startedCommand = false;
bool startedOutput = false;
MarkKind lastMarkKind = MarkKind::Output;
const auto endThisMark = [&](auto x, auto y) {
if (startedOutput)
@@ -3515,7 +3544,7 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
// Output attribute.
const auto& row = GetRowByOffset(y);
const auto runs = row.Attributes().runs();
const auto& runs = row.Attributes().runs();
x = 0;
for (const auto& [attr, length] : runs)
{
@@ -3558,8 +3587,6 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
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;
@@ -3592,7 +3619,7 @@ std::wstring TextBuffer::_commandForRow(const til::CoordType rowOffset,
// 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();
const auto& runs = row.Attributes().runs();
auto x = 0;
for (const auto& [attr, length] : runs)
{

View File

@@ -318,9 +318,9 @@ public:
private:
void _reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes);
void _commit(const std::byte* row);
void _decommit() noexcept;
void _decommit(til::CoordType keep) noexcept;
void _construct(const std::byte* until) noexcept;
void _destroy() const noexcept;
void _destroy(std::byte* it) const noexcept;
ROW& _getRowByOffsetDirect(size_t offset);
ROW& _getRow(til::CoordType y) const;
til::CoordType _estimateOffsetOfLastCommittedRow() const noexcept;
@@ -384,14 +384,14 @@ private:
// In other words, _commitWatermark itself will either point exactly onto the next ROW
// that should be committed or be equal to _bufferEnd when all ROWs are committed.
std::byte* _commitWatermark = nullptr;
// This will MEM_COMMIT 128 rows more than we need, to avoid us from having to call VirtualAlloc too often.
// This will MEM_COMMIT 256 rows more than we need, to avoid us from having to call VirtualAlloc too often.
// This equates to roughly the following commit chunk sizes at these column counts:
// * 80 columns (the usual minimum) = 60KB chunks, 4.1MB buffer at 9001 rows
// * 120 columns (the most common) = 80KB chunks, 5.6MB buffer at 9001 rows
// * 400 columns (the usual maximum) = 220KB chunks, 15.5MB buffer at 9001 rows
// * 80 columns (the usual minimum) = 131KB chunks, 4.6MB buffer at 9001 rows
// * 120 columns (the most common) = 172KB chunks, 6.0MB buffer at 9001 rows
// * 400 columns (the usual maximum) = 459KB chunks, 16.1MB buffer at 9001 rows
// There's probably a better metric than this. (This comment was written when ROW had both,
// a _chars array containing text and a _charOffsets array contain column-to-text indices.)
static constexpr size_t _commitReadAheadRowCount = 128;
static constexpr size_t _commitReadAheadRowCount = 256;
// Before TextBuffer was made to use virtual memory it initialized the entire memory arena with the initial
// attributes right away. To ensure it continues to work the way it used to, this stores these initial attributes.
TextAttribute _initialAttributes;
@@ -405,9 +405,9 @@ private:
size_t _bufferOffsetChars = 0;
size_t _bufferOffsetCharOffsets = 0;
// The width of the buffer in columns.
uint16_t _width = 0;
til::CoordType _width = 0;
// The height of the buffer in rows, excluding the scratchpad row.
uint16_t _height = 0;
til::CoordType _height = 0;
TextAttribute _currentAttributes;
til::CoordType _firstRow = 0; // indexes top row (not necessarily 0)

View File

@@ -44,8 +44,7 @@ void Terminal::Create(til::size viewportSize, til::CoordType scrollbackLines, Re
{
_mutableViewport = Viewport::FromDimensions({ 0, 0 }, viewportSize);
_scrollbackLines = scrollbackLines;
const til::size bufferSize{ viewportSize.width,
Utils::ClampToShortMax(viewportSize.height + scrollbackLines, 1) };
const til::size bufferSize{ viewportSize.width, viewportSize.height + scrollbackLines };
const TextAttribute attr{};
const UINT cursorSize = 12;
_mainBuffer = std::make_unique<TextBuffer>(bufferSize, attr, cursorSize, true, &renderer);
@@ -67,7 +66,7 @@ void Terminal::CreateFromSettings(ICoreSettings settings,
Utils::ClampToShortMax(settings.InitialRows(), 1) };
// TODO:MSFT:20642297 - Support infinite scrollback here, if HistorySize is -1
Create(viewportSize, Utils::ClampToShortMax(settings.HistorySize(), 0), renderer);
Create(viewportSize, std::clamp(settings.HistorySize(), 0, til::CoordTypeMax / 2), renderer);
UpdateSettings(settings);
}

View File

@@ -21,6 +21,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
InitializeComponent();
Automation::AutomationProperties::SetName(AddBellSoundButton(), RS_(L"Profile_AddBellSound/Text"));
// The XAML -> C++ converter seems to use floats instead of doubles, which means it
// can't represent large numbers accurately (lol!). So, we set the property manually.
HistorySizeBox().Maximum(static_cast<double>(til::CoordTypeMax / 2));
}
void Profiles_Advanced::OnNavigatedTo(const NavigationEventArgs& e)

View File

@@ -71,7 +71,8 @@
ClearSettingValue="{x:Bind Profile.ClearHistorySize}"
HasSettingValue="{x:Bind Profile.HasHistorySize, Mode=OneWay}"
SettingOverrideSource="{x:Bind Profile.HistorySizeOverrideSource, Mode=OneWay}">
<muxc:NumberBox x:Uid="Profile_HistorySizeBox"
<muxc:NumberBox x:Name="HistorySizeBox"
x:Uid="Profile_HistorySizeBox"
LargeChange="100"
Minimum="0"
SmallChange="10"

View File

@@ -47,7 +47,7 @@
"fontFace": "Cascadia Mono",
"fontSize": 12,
"hidden": false,
"historySize": 9001,
"historySize": 16384,
"padding": "8, 8, 8, 8",
"snapOnInput": true,
"altGrAliasing": true,
@@ -66,7 +66,7 @@
"fontFace": "Cascadia Mono",
"fontSize": 12,
"hidden": false,
"historySize": 9001,
"historySize": 16384,
"padding": "8, 8, 8, 8",
"snapOnInput": true,
"altGrAliasing": true,

View File

@@ -25,7 +25,6 @@ namespace TerminalCoreUnitTests
TEST_CLASS(ScreenSizeLimitsTest);
TEST_METHOD(ScreenWidthAndHeightAreClampedToBounds);
TEST_METHOD(ScrollbackHistorySizeIsClampedToBounds);
TEST_METHOD(ResizeIsClampedToBounds);
};
@@ -54,46 +53,6 @@ void ScreenSizeLimitsTest::ScreenWidthAndHeightAreClampedToBounds()
VERIFY_ARE_EQUAL(actualDimensions.width, SHRT_MAX, L"Column count clamped to SHRT_MAX == " WCS(SHRT_MAX));
}
void ScreenSizeLimitsTest::ScrollbackHistorySizeIsClampedToBounds()
{
// What is actually clamped is the number of rows in the internal history buffer,
// which is the *sum* of the history size plus the number of rows
// actually visible on screen at the moment.
static constexpr til::CoordType visibleRowCount = 100;
// Zero history size is acceptable.
auto noHistorySettings = winrt::make<MockTermSettings>(0, visibleRowCount, 100);
Terminal noHistoryTerminal{ Terminal::TestDummyMarker{} };
DummyRenderer renderer{ &noHistoryTerminal };
noHistoryTerminal.CreateFromSettings(noHistorySettings, renderer);
VERIFY_ARE_EQUAL(noHistoryTerminal.GetTextBuffer().TotalRowCount(), visibleRowCount, L"History size of 0 is accepted");
// Negative history sizes are clamped to zero.
auto negativeHistorySizeSettings = winrt::make<MockTermSettings>(-100, visibleRowCount, 100);
Terminal negativeHistorySizeTerminal{ Terminal::TestDummyMarker{} };
negativeHistorySizeTerminal.CreateFromSettings(negativeHistorySizeSettings, renderer);
VERIFY_ARE_EQUAL(negativeHistorySizeTerminal.GetTextBuffer().TotalRowCount(), visibleRowCount, L"Negative history size is clamped to 0");
// History size + initial visible rows == SHRT_MAX is acceptable.
auto maxHistorySizeSettings = winrt::make<MockTermSettings>(SHRT_MAX - visibleRowCount, visibleRowCount, 100);
Terminal maxHistorySizeTerminal{ Terminal::TestDummyMarker{} };
maxHistorySizeTerminal.CreateFromSettings(maxHistorySizeSettings, renderer);
VERIFY_ARE_EQUAL(maxHistorySizeTerminal.GetTextBuffer().TotalRowCount(), SHRT_MAX, L"History size == SHRT_MAX - initial row count is accepted");
// History size + initial visible rows == SHRT_MAX + 1 will be clamped slightly.
auto justTooBigHistorySizeSettings = winrt::make<MockTermSettings>(SHRT_MAX - visibleRowCount + 1, visibleRowCount, 100);
Terminal justTooBigHistorySizeTerminal{ Terminal::TestDummyMarker{} };
justTooBigHistorySizeTerminal.CreateFromSettings(justTooBigHistorySizeSettings, renderer);
VERIFY_ARE_EQUAL(justTooBigHistorySizeTerminal.GetTextBuffer().TotalRowCount(), SHRT_MAX, L"History size == 1 + SHRT_MAX - initial row count is clamped to SHRT_MAX - initial row count");
// Ridiculously large history sizes are also clamped.
auto farTooBigHistorySizeSettings = winrt::make<MockTermSettings>(99999999, visibleRowCount, 100);
Terminal farTooBigHistorySizeTerminal{ Terminal::TestDummyMarker{} };
farTooBigHistorySizeTerminal.CreateFromSettings(farTooBigHistorySizeSettings, renderer);
VERIFY_ARE_EQUAL(farTooBigHistorySizeTerminal.GetTextBuffer().TotalRowCount(), SHRT_MAX, L"History size that is far too large is clamped to SHRT_MAX - initial row count");
}
void ScreenSizeLimitsTest::ResizeIsClampedToBounds()
{
// What is actually clamped is the number of rows in the internal history buffer,

View File

@@ -86,7 +86,7 @@ namespace TerminalCoreUnitTests
TEST_METHOD(OverflowTests)
{
const til::point maxCoord = { SHRT_MAX, SHRT_MAX };
const til::point maxCoord{ til::CoordTypeMax, til::CoordTypeMax };
// Test SetSelectionAnchor(til::point) and SetSelectionEnd(til::point)
// Behavior: clamp coord to viewport.
@@ -132,8 +132,8 @@ namespace TerminalCoreUnitTests
Log::Comment(L"Triple click selection with NO scrollback value");
ValidateTripleClickSelection(0, { 0, 9 }, { 10, 9 });
// Test with max scrollback
const til::CoordType expected_row = SHRT_MAX - 1;
// Test with large scrollback
const til::CoordType expected_row = (SHRT_MAX - 1) + 10; // +10 viewport size
Log::Comment(L"Single click selection with MAXIMUM scrollback value");
ValidateSingleClickSelection(SHRT_MAX, { 10, expected_row }, { 10, expected_row });
Log::Comment(L"Double click selection with MAXIMUM scrollback value");

View File

@@ -400,20 +400,16 @@ static FillConsoleResult FillConsoleImpl(SCREEN_INFORMATION& screenInfo, FillCon
// See FillConsoleOutputCharacterWImpl and its identical code.
if (enablePowershellShim)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
if (const auto writer = gci.GetVtWriterForBuffer(&OutContext))
{
const auto currentBufferDimensions{ OutContext.GetBufferSize().Dimensions() };
const auto wroteWholeBuffer = lengthToWrite == (currentBufferDimensions.area<size_t>());
const auto startedAtOrigin = startingCoordinate == til::point{ 0, 0 };
const auto wroteSpaces = attribute == (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);
const auto currentBufferDimensions{ OutContext.GetBufferSize().Dimensions() };
const auto wroteWholeBuffer = lengthToWrite == (currentBufferDimensions.area<size_t>());
const auto startedAtOrigin = startingCoordinate == til::point{ 0, 0 };
const auto wroteSpaces = attribute == (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);
if (wroteWholeBuffer && startedAtOrigin && wroteSpaces)
{
// PowerShell has previously called FillConsoleOutputCharacterW() which triggered a call to WriteClearScreen().
cellsModified = lengthToWrite;
return S_OK;
}
if (wroteWholeBuffer && startedAtOrigin && wroteSpaces)
{
// PowerShell has previously called FillConsoleOutputCharacterW() which triggered a call to WriteClearScreen().
cellsModified = lengthToWrite;
return S_OK;
}
}
@@ -458,20 +454,25 @@ static FillConsoleResult FillConsoleImpl(SCREEN_INFORMATION& screenInfo, FillCon
if (enablePowershellShim)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
if (auto writer = gci.GetVtWriterForBuffer(&OutContext))
{
const auto currentBufferDimensions{ OutContext.GetBufferSize().Dimensions() };
const auto wroteWholeBuffer = lengthToWrite == (currentBufferDimensions.area<size_t>());
const auto startedAtOrigin = startingCoordinate == til::point{ 0, 0 };
const auto wroteSpaces = character == UNICODE_SPACE;
auto writer = gci.GetVtWriterForBuffer(&OutContext);
const auto currentBufferDimensions{ OutContext.GetBufferSize().Dimensions() };
const auto wroteWholeBuffer = lengthToWrite == (currentBufferDimensions.area<size_t>());
const auto startedAtOrigin = startingCoordinate == til::point{ 0, 0 };
const auto wroteSpaces = character == UNICODE_SPACE;
if (wroteWholeBuffer && startedAtOrigin && wroteSpaces)
if (wroteWholeBuffer && startedAtOrigin && wroteSpaces)
{
// WriteClearScreen uses VT sequences and is more efficient at clearing the buffer than FillConsoleImpl.
// For this reason, we use it no matter whether we have a VT writer (= ConPTY) or not.
WriteClearScreen(OutContext);
if (writer)
{
WriteClearScreen(OutContext);
writer.Submit();
cellsModified = lengthToWrite;
return S_OK;
}
cellsModified = lengthToWrite;
return S_OK;
}
}

View File

@@ -982,23 +982,30 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
// A null character will get translated to whitespace.
fillCharacter = Microsoft::Console::VirtualTerminal::VtIo::SanitizeUCS2(fillCharacter);
if (writer)
// GH#3126 - This is a shim for cmd's `cls` function. In the
// legacy console, `cls` is supposed to clear the entire buffer.
// We always use a VT sequence, even if ConPTY isn't used, because those are faster nowadays.
if (enableCmdShim &&
source.left <= 0 && source.top <= 0 &&
source.right >= bufferSize.RightInclusive() && source.bottom >= bufferSize.BottomInclusive() &&
target.x == 0 && target.y <= -bufferSize.BottomExclusive() &&
!clip &&
fillCharacter == UNICODE_SPACE && fillAttribute == buffer.GetAttributes().GetLegacyAttributes())
{
// GH#3126 - This is a shim for cmd's `cls` function. In the
// legacy console, `cls` is supposed to clear the entire buffer.
// We always use a VT sequence, even if ConPTY isn't used, because those are faster nowadays.
if (enableCmdShim &&
source.left <= 0 && source.top <= 0 &&
source.right >= bufferSize.RightInclusive() && source.bottom >= bufferSize.BottomInclusive() &&
target.x == 0 && target.y <= -bufferSize.BottomExclusive() &&
!clip &&
fillCharacter == UNICODE_SPACE && fillAttribute == buffer.GetAttributes().GetLegacyAttributes())
// WriteClearScreen uses VT sequences and is more efficient at clearing the buffer than FillConsoleImpl.
// For this reason, we use it no matter whether we have a VT writer (= ConPTY) or not.
WriteClearScreen(context);
if (writer)
{
WriteClearScreen(context);
writer.Submit();
return S_OK;
}
return S_OK;
}
if (writer)
{
const auto clipViewport = clip ? Viewport::FromInclusive(*clip).Clamp(bufferSize) : bufferSize;
const auto sourceViewport = Viewport::FromInclusive(source);
const auto fillViewport = sourceViewport.Clamp(clipViewport);

View File

@@ -2120,10 +2120,10 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport,
// MSFT-33471786, GH#13193:
// newViewport may reside anywhere outside of the valid coordScreenBufferSize.
// For instance it might be scrolled down more than our text buffer allows to be scrolled.
const auto cx = gsl::narrow_cast<SHORT>(std::clamp(viewportRect.width(), 1, coordScreenBufferSize.width));
const auto cy = gsl::narrow_cast<SHORT>(std::clamp(viewportRect.height(), 1, coordScreenBufferSize.height));
const auto x = gsl::narrow_cast<SHORT>(std::clamp(viewportRect.left, 0, coordScreenBufferSize.width - cx));
const auto y = gsl::narrow_cast<SHORT>(std::clamp(viewportRect.top, 0, coordScreenBufferSize.height - cy));
const auto cx = std::clamp(viewportRect.width(), 1, coordScreenBufferSize.width);
const auto cy = std::clamp(viewportRect.height(), 1, coordScreenBufferSize.height);
const auto x = std::clamp(viewportRect.left, 0, coordScreenBufferSize.width - cx);
const auto y = std::clamp(viewportRect.top, 0, coordScreenBufferSize.height - cy);
_viewport = Viewport::FromExclusive({ x, y, x + cx, y + cy });

View File

@@ -180,7 +180,7 @@ int Utils::s_CompareCoords(const til::size bufferSize, const til::point coordFir
// First set the distance vertically
// If first is on row 4 and second is on row 6, first will be -2 rows behind second * an 80 character row would be -160.
// For the same row, it'll be 0 rows * 80 character width = 0 difference.
auto retVal = (coordFirst.y - coordSecond.y) * cRowWidth;
auto retVal = (til::HugeCoordType{ coordFirst.y } - coordSecond.y) * cRowWidth;
// Now adjust for horizontal differences
// If first is in position 15 and second is in position 30, first is -15 left in relation to 30.
@@ -193,7 +193,7 @@ int Utils::s_CompareCoords(const til::size bufferSize, const til::point coordFir
// Step one will set the retVal as -80 as first is one row behind the second.
// Step two will then see that first is 79 - 0 = +79 right of second and add 79
// The total is -80 + 79 = -1.
return retVal;
return gsl::narrow_cast<int>(std::clamp<til::HugeCoordType>(retVal, INT_MIN, INT_MAX));
}
// Routine Description:

View File

@@ -21,7 +21,7 @@ constexpr COLORREF OPACITY_OPAQUE = 0xff000000;
constexpr auto DEFAULT_FOREGROUND = COLOR_WHITE;
constexpr auto DEFAULT_BACKGROUND = COLOR_BLACK;
constexpr short DEFAULT_HISTORY_SIZE = 9001;
constexpr til::CoordType DEFAULT_HISTORY_SIZE = 16384;
#pragma warning(push)
#pragma warning(disable : 26426)

View File

@@ -259,11 +259,11 @@ int Viewport::CompareInBounds(const til::point first, const til::point second, b
// First set the distance vertically
// If first is on row 4 and second is on row 6, first will be -2 rows behind second * an 80 character row would be -160.
// For the same row, it'll be 0 rows * 80 character width = 0 difference.
auto retVal = (first.y - second.y) * Width();
auto retVal = (gsl::narrow_cast<til::HugeCoordType>(first.y) - second.y) * Width();
// Now adjust for horizontal differences
// If first is in position 15 and second is in position 30, first is -15 left in relation to 30.
retVal += (first.x - second.x);
retVal += gsl::narrow_cast<til::HugeCoordType>(first.x) - second.x;
// Further notes:
// If we already moved behind one row, this will help correct for when first is right of second.
@@ -272,7 +272,7 @@ int Viewport::CompareInBounds(const til::point first, const til::point second, b
// Step one will set the retVal as -80 as first is one row behind the second.
// Step two will then see that first is 79 - 0 = +79 right of second and add 79
// The total is -80 + 79 = -1.
return retVal;
return gsl::narrow_cast<int>(std::clamp<til::HugeCoordType>(retVal, INT_MIN, INT_MAX));
}
// Method Description: