From bbbe20f8107faac01b9e93055bac694e6af326e5 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 19 Jun 2024 02:28:07 +0200 Subject: [PATCH 01/21] Move colorbrewer into its own proper header --- NOTICE.md | 21 +++++++++++++++++++ src/{renderer/atlas => inc/til}/colorbrewer.h | 15 +++++++++++-- src/renderer/atlas/AtlasEngine.r.cpp | 1 + src/renderer/atlas/BackendD2D.cpp | 4 ++-- src/renderer/atlas/BackendD3D.cpp | 6 +++--- src/renderer/atlas/atlas.vcxproj | 3 +-- 6 files changed, 41 insertions(+), 9 deletions(-) rename src/{renderer/atlas => inc/til}/colorbrewer.h (79%) diff --git a/NOTICE.md b/NOTICE.md index fb48315d254..091060db2cd 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -325,6 +325,27 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +## ColorBrewer +**Source**: [https://colorbrewer2.org/](https://colorbrewer2.org/) + +### License + +``` +Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes + +Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +``` + # Microsoft Open Source This product also incorporates source code from other Microsoft open source projects, all licensed under the MIT license. diff --git a/src/renderer/atlas/colorbrewer.h b/src/inc/til/colorbrewer.h similarity index 79% rename from src/renderer/atlas/colorbrewer.h rename to src/inc/til/colorbrewer.h index be5cef6b8a6..5f22f972f86 100644 --- a/src/renderer/atlas/colorbrewer.h +++ b/src/inc/til/colorbrewer.h @@ -3,7 +3,7 @@ #pragma once -namespace Microsoft::Console::Render::Atlas::colorbrewer +namespace til::colorbrewer { // The following list of colors is only used as a debug aid and not part of the final product. // They're licensed under: @@ -22,7 +22,7 @@ namespace Microsoft::Console::Render::Atlas::colorbrewer // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. // - inline constexpr u32 pastel1[]{ + inline constexpr uint32_t pastel1[]{ 0xfbb4ae, 0xb3cde3, 0xccebc5, @@ -33,4 +33,15 @@ namespace Microsoft::Console::Render::Atlas::colorbrewer 0xfddaec, 0xf2f2f2, }; + + inline constexpr uint32_t dark2[]{ + 0x1b9e77, + 0xd95f02, + 0x7570b3, + 0xe7298a, + 0x66a61e, + 0xe6ab02, + 0xa6761d, + 0x666666, + }; } diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index 3591abe7905..ca665228345 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -465,6 +465,7 @@ void AtlasEngine::_present() return; } +#pragma warning(suppress : 4127) // conditional expression is constant if (!ATLAS_DEBUG_SHOW_DIRTY && !_p.s->target->disablePresent1 && memcmp(&dirtyRect, &fullRect, sizeof(RECT)) != 0) { params.DirtyRectsCount = 1; diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index cf7bd8a2bec..b79cee5a3ae 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -7,7 +7,7 @@ #include #if ATLAS_DEBUG_SHOW_DIRTY -#include "colorbrewer.h" +#include #endif #if ATLAS_DEBUG_DUMP_RENDER_TARGET @@ -911,7 +911,7 @@ void BackendD2D::_debugShowDirty(const RenderingPayload& p) static_cast(rect.right), static_cast(rect.bottom), }; - const auto color = colorbrewer::pastel1[i] | 0x1f000000; + const auto color = til::colorbrewer::pastel1[i] | 0x1f000000; _fillRectangle(rectF, color); } } diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index b266d3ba092..562d8ae7905 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -17,7 +17,7 @@ #include "../../types/inc/ColorFix.hpp" #if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_COLORIZE_GLYPH_ATLAS -#include "colorbrewer.h" +#include #endif TIL_FAST_MATH_BEGIN @@ -2222,7 +2222,7 @@ void BackendD3D::_debugShowDirty(const RenderingPayload& p) if (rect.non_empty()) { _appendQuad() = { - .shadingType = ShadingType::Selection, + .shadingType = static_cast(ShadingType::Selection), .position = { static_cast(rect.left), static_cast(rect.top), @@ -2231,7 +2231,7 @@ void BackendD3D::_debugShowDirty(const RenderingPayload& p) static_cast(rect.right - rect.left), static_cast(rect.bottom - rect.top), }, - .color = colorbrewer::pastel1[i] | 0x1f000000, + .color = til::colorbrewer::pastel1[i] | 0x1f000000, }; } } diff --git a/src/renderer/atlas/atlas.vcxproj b/src/renderer/atlas/atlas.vcxproj index 694b850224d..37dac662d81 100644 --- a/src/renderer/atlas/atlas.vcxproj +++ b/src/renderer/atlas/atlas.vcxproj @@ -32,7 +32,6 @@ - @@ -102,4 +101,4 @@ $(SolutionDir)\oss\stb;$(OutDir)$(ProjectName);%(AdditionalIncludeDirectories) - + \ No newline at end of file From b0cc432f0b153866c42da4da0a836173882aa25d Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 19 Jun 2024 02:48:55 +0200 Subject: [PATCH 02/21] Use VT for COOKED_READ_DATA --- src/buffer/out/cursor.cpp | 13 - src/buffer/out/cursor.h | 3 - .../TerminalCore/terminalrenderdata.cpp | 2 +- src/host/VtIo.cpp | 42 + src/host/VtIo.hpp | 3 + src/host/_stream.cpp | 5 + src/host/_stream.h | 1 + src/host/readDataCooked.cpp | 1156 ++++++++--------- src/host/readDataCooked.hpp | 114 +- src/host/renderData.cpp | 2 +- 10 files changed, 649 insertions(+), 692 deletions(-) diff --git a/src/buffer/out/cursor.cpp b/src/buffer/out/cursor.cpp index 273c05f8ea1..43c0a702939 100644 --- a/src/buffer/out/cursor.cpp +++ b/src/buffer/out/cursor.cpp @@ -20,7 +20,6 @@ Cursor::Cursor(const ULONG ulSize, TextBuffer& parentBuffer) noexcept : _fBlinkingAllowed(true), _fDelay(false), _fIsConversionArea(false), - _fIsPopupShown(false), _fDelayedEolWrap(false), _fDeferCursorRedraw(false), _fHaveDeferredCursorRedraw(false), @@ -66,11 +65,6 @@ bool Cursor::IsConversionArea() const noexcept return _fIsConversionArea; } -bool Cursor::IsPopupShown() const noexcept -{ - return _fIsPopupShown; -} - bool Cursor::GetDelay() const noexcept { return _fDelay; @@ -126,13 +120,6 @@ void Cursor::SetIsConversionArea(const bool fIsConversionArea) noexcept _RedrawCursorAlways(); } -void Cursor::SetIsPopupShown(const bool fIsPopupShown) noexcept -{ - // Functionally the same as "Hide cursor" - _fIsPopupShown = fIsPopupShown; - _RedrawCursorAlways(); -} - void Cursor::SetDelay(const bool fDelay) noexcept { _fDelay = fDelay; diff --git a/src/buffer/out/cursor.h b/src/buffer/out/cursor.h index 3c377e61b2a..d66762c1b84 100644 --- a/src/buffer/out/cursor.h +++ b/src/buffer/out/cursor.h @@ -44,7 +44,6 @@ class Cursor final bool IsBlinkingAllowed() const noexcept; bool IsDouble() const noexcept; bool IsConversionArea() const noexcept; - bool IsPopupShown() const noexcept; bool GetDelay() const noexcept; ULONG GetSize() const noexcept; til::point GetPosition() const noexcept; @@ -61,7 +60,6 @@ class Cursor final void SetBlinkingAllowed(const bool fIsOn) noexcept; void SetIsDouble(const bool fIsDouble) noexcept; void SetIsConversionArea(const bool fIsConversionArea) noexcept; - void SetIsPopupShown(const bool fIsPopupShown) noexcept; void SetDelay(const bool fDelay) noexcept; void SetSize(const ULONG ulSize) noexcept; void SetStyle(const ULONG ulSize, const CursorType type) noexcept; @@ -99,7 +97,6 @@ class Cursor final bool _fBlinkingAllowed; //Whether or not the cursor is allowed to blink at all. only set through VT (^[[?12h/l) bool _fDelay; // don't blink scursor on next timer message bool _fIsConversionArea; // is attached to a conversion area so it doesn't actually need to display the cursor. - bool _fIsPopupShown; // if a popup is being shown, turn off, stop blinking. bool _fDelayedEolWrap; // don't wrap at EOL till the next char comes in. til::point _coordDelayedAt; // coordinate the EOL wrap was delayed at. diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 3d989f31f07..951761bb9f8 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -48,7 +48,7 @@ til::point Terminal::GetCursorPosition() const noexcept bool Terminal::IsCursorVisible() const noexcept { const auto& cursor = _activeBuffer().GetCursor(); - return cursor.IsVisible() && !cursor.IsPopupShown(); + return cursor.IsVisible(); } bool Terminal::IsCursorOn() const noexcept diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index 23500c2f6a8..9a1601cb636 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -497,3 +497,45 @@ bool VtIo::IsResizeQuirkEnabled() const } return S_OK; } + +static size_t formatAttributes(char (&buffer)[16], WORD attributes) noexcept +{ + const uint8_t rv = WI_IsFlagSet(attributes, COMMON_LVB_REVERSE_VIDEO) ? 7 : 27; + uint8_t fg = 39; + uint8_t bg = 49; + + // `attributes` of exactly `FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED` + // are often used to indicate the default colors in Windows Console applications. + // Thus, we translate them to 39/49 (default foreground/background). + if ((attributes & (FG_ATTRS | BG_ATTRS)) != (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED)) + { + // The Console API represents colors in BGR order, but VT represents them in RGB order. + // This LUT transposes them. This is for foreground colors. Add +10 to get the background ones. + static const uint8_t lut[] = { 30, 34, 32, 36, 31, 35, 33, 37, 90, 94, 92, 96, 91, 95, 93, 97 }; + fg = lut[attributes & 0xf]; + bg = lut[(attributes >> 4) & 0xf] + 10; + } + + return fmt::format_to(&buffer[0], FMT_COMPILE("\x1b[{};{};{}m"), rv, fg, bg) - &buffer[0]; +} + +void VtIo::FormatAttributes(std::string& target, WORD attributes) +{ + char buf[16]; + const auto len = formatAttributes(buf, attributes); + target.append(buf, len); +} + +void VtIo::FormatAttributes(std::wstring& target, WORD attributes) +{ + char buf[16]; + const auto len = formatAttributes(buf, attributes); + + wchar_t bufW[16]; + for (size_t i = 0; i < len; i++) + { + bufW[i] = buf[i]; + } + + target.append(bufW, len); +} diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp index eccdb06aaac..28a6d356869 100644 --- a/src/host/VtIo.hpp +++ b/src/host/VtIo.hpp @@ -20,6 +20,9 @@ namespace Microsoft::Console::VirtualTerminal class VtIo { public: + static void FormatAttributes(std::string& target, WORD attributes); + static void FormatAttributes(std::wstring& target, WORD attributes); + VtIo(); [[nodiscard]] HRESULT Initialize(const ConsoleArguments* const pArgs); diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index e030cec03c7..ca7b26350d0 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -255,6 +255,11 @@ void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& t } } +void WriteCharsVT(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str) +{ + screenInfo.GetStateMachine().ProcessString(str); +} + // Routine Description: // - Takes the given text and inserts it into the given screen buffer. // Note: diff --git a/src/host/_stream.h b/src/host/_stream.h index 79eb84585dc..3897abf22fb 100644 --- a/src/host/_stream.h +++ b/src/host/_stream.h @@ -20,6 +20,7 @@ Revision History: #include "writeData.hpp" void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str, til::CoordType* psScrollY); +void WriteCharsVT(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str); // NOTE: console lock must be held when calling this routine // String has been translated to unicode at this point. diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 6028acf074c..2ea1a1341ed 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -11,100 +11,14 @@ #include "_stream.h" #include "../interactivity/inc/ServiceLocator.hpp" -using Microsoft::Console::Interactivity::ServiceLocator; - -// As per https://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10Obvious -constexpr int integerLog10(uint32_t v) -{ - return (v >= 1000000000) ? 9 : - (v >= 100000000) ? 8 : - (v >= 10000000) ? 7 : - (v >= 1000000) ? 6 : - (v >= 100000) ? 5 : - (v >= 10000) ? 4 : - (v >= 1000) ? 3 : - (v >= 100) ? 2 : - (v >= 10) ? 1 : - 0; -} - -const std::wstring& COOKED_READ_DATA::BufferState::Get() const noexcept -{ - return _buffer; -} - -void COOKED_READ_DATA::BufferState::Replace(size_t offset, size_t remove, const wchar_t* input, size_t count) -{ - const auto size = _buffer.size(); - offset = std::min(offset, size); - remove = std::min(remove, size - offset); - - _buffer.replace(offset, remove, input, count); - _cursor = offset + count; - _dirtyBeg = std::min(_dirtyBeg, offset); -} - -void COOKED_READ_DATA::BufferState::Replace(const std::wstring_view& str) -{ - _buffer.assign(str); - _cursor = _buffer.size(); - _dirtyBeg = 0; -} - -size_t COOKED_READ_DATA::BufferState::GetCursorPosition() const noexcept -{ - return _cursor; -} - -void COOKED_READ_DATA::BufferState::SetCursorPosition(size_t pos) noexcept -{ - const auto size = _buffer.size(); - _cursor = std::min(pos, size); - // This ensures that _dirtyBeg isn't npos, which ensures that IsClean() returns false. - _dirtyBeg = std::min(_dirtyBeg, size); -} - -bool COOKED_READ_DATA::BufferState::IsClean() const noexcept -{ - return _dirtyBeg == npos; -} - -void COOKED_READ_DATA::BufferState::MarkEverythingDirty() noexcept -{ - _dirtyBeg = 0; -} - -void COOKED_READ_DATA::BufferState::MarkAsClean() noexcept -{ - _dirtyBeg = npos; -} +#define COOKED_READ_DEBUG 0 -std::wstring_view COOKED_READ_DATA::BufferState::GetUnmodifiedTextBeforeCursor() const noexcept -{ - return _slice(0, std::min(_dirtyBeg, _cursor)); -} - -std::wstring_view COOKED_READ_DATA::BufferState::GetUnmodifiedTextAfterCursor() const noexcept -{ - return _slice(_cursor, _dirtyBeg); -} - -std::wstring_view COOKED_READ_DATA::BufferState::GetModifiedTextBeforeCursor() const noexcept -{ - return _slice(_dirtyBeg, _cursor); -} - -std::wstring_view COOKED_READ_DATA::BufferState::GetModifiedTextAfterCursor() const noexcept -{ - return _slice(std::max(_dirtyBeg, _cursor), npos); -} +#if COOKED_READ_DEBUG +#include +#endif -std::wstring_view COOKED_READ_DATA::BufferState::_slice(size_t from, size_t to) const noexcept -{ - to = std::min(to, _buffer.size()); - from = std::min(from, to); - return std::wstring_view{ _buffer.data() + from, to - from }; -} +using Microsoft::Console::Interactivity::ServiceLocator; +using Microsoft::Console::VirtualTerminal::VtIo; // Routine Description: // - Constructs cooked read data class to hold context across key presses while a user is modifying their 'input line'. @@ -147,9 +61,13 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, THROW_IF_FAILED(_screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, _tempHandle)); #endif + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + auto absoluteCursorPos = cursor.GetPosition(); + if (!initialData.empty()) { - _buffer.Replace(initialData); + _replace(initialData); // The console API around `nInitialChars` in `CONSOLE_READCONSOLE_CONTROL` is pretty weird. // The way it works is that cmd.exe does a ReadConsole() with a `dwCtrlWakeupMask` that includes \t, @@ -178,20 +96,25 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, // NOTE: You can't just "measure" the length of the string in columns either, because previously written // wide glyphs might have resulted in padding whitespace in the text buffer (see ROW::WasDoubleBytePadded). - // The alternative to the loop below is counting the number of padding glyphs while iterating backwards. Either approach is fine. - til::CoordType distance = 0; - for (size_t i = 0; i < initialData.size(); i = TextBuffer::GraphemeNext(initialData, i)) - { - --distance; - } + til::CoordType columns = 0; + textBuffer.FitTextIntoColumns(initialData, til::CoordTypeMax, columns); - const auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - const auto end = cursor.GetPosition(); - const auto beg = textBuffer.NavigateCursor(end, distance); - _distanceCursor = (end.y - beg.y) * textBuffer.GetSize().Width() + end.x - beg.x; - _distanceEnd = _distanceCursor; + const int64_t w = textBuffer.GetSize().Width(); + const int64_t x = absoluteCursorPos.x; + const int64_t y = absoluteCursorPos.y; + + auto cols = y * w + x - columns; + cols = std::max(0, cols); + + absoluteCursorPos.x = gsl::narrow_cast(cols % w); + absoluteCursorPos.y = gsl::narrow_cast(cols / w); } + + _screenInfo.GetVirtualViewport().ConvertToOrigin(&absoluteCursorPos); + absoluteCursorPos.x = std::max(0, absoluteCursorPos.x); + absoluteCursorPos.y = std::max(0, absoluteCursorPos.y); + + _originInViewport = absoluteCursorPos; } // Routine Description: @@ -288,7 +211,7 @@ bool COOKED_READ_DATA::Read(const bool isUnicode, size_t& numBytes, ULONG& contr // NOTE: Don't call _flushBuffer in a wil::scope_exit/defer. // It may throw and throwing during an ongoing exception is a bad idea. - _flushBuffer(); + _redisplay(); if (_state == State::Accumulating) { @@ -302,25 +225,20 @@ bool COOKED_READ_DATA::Read(const bool isUnicode, size_t& numBytes, ULONG& contr // Printing wide glyphs at the end of a row results in a forced line wrap and a padding whitespace to be inserted. // When the text buffer resizes these padding spaces may vanish and the _distanceCursor and _distanceEnd measurements become inaccurate. // To fix this, this function is called before a resize and will clear the input line. Afterwards, RedrawAfterResize() will restore it. -void COOKED_READ_DATA::EraseBeforeResize() +void COOKED_READ_DATA::EraseBeforeResize() const { - _popupsDone(); - - if (_distanceEnd) - { - _offsetCursorPosition(-_distanceCursor); - _erase(_distanceEnd); - _offsetCursorPosition(-_distanceEnd); - _distanceCursor = 0; - _distanceEnd = 0; - } + std::wstring output; + _appendCUP(output, _originInViewport); + output.append(L"\x1b[J"); + WriteCharsVT(_screenInfo, output); } // The counter-part to EraseBeforeResize(). void COOKED_READ_DATA::RedrawAfterResize() { - _buffer.MarkEverythingDirty(); - _flushBuffer(); + _bufferDirtyBeg = 0; + _dirty = true; + _redisplay(); } void COOKED_READ_DATA::SetInsertMode(bool insertMode) noexcept @@ -330,7 +248,7 @@ void COOKED_READ_DATA::SetInsertMode(bool insertMode) noexcept bool COOKED_READ_DATA::IsEmpty() const noexcept { - return _buffer.Get().empty() && _popups.empty(); + return _buffer.empty() && _popups.empty(); } bool COOKED_READ_DATA::PresentingPopup() const noexcept @@ -340,10 +258,20 @@ bool COOKED_READ_DATA::PresentingPopup() const noexcept til::point_span COOKED_READ_DATA::GetBoundaries() const noexcept { - const auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - const auto beg = _offsetPosition(cursor.GetPosition(), -_distanceCursor); - const auto end = _offsetPosition(beg, _distanceEnd); + const auto viewport = _screenInfo.GetViewport(); + const auto virtualViewport = _screenInfo.GetVirtualViewport(); + + static constexpr til::point min; + const til::point max{ viewport.RightInclusive(), viewport.BottomInclusive() }; + + auto beg = _originInViewport; + virtualViewport.ConvertFromOrigin(&beg); + + auto end = _pagerPromptEnd; + end.y -= _pagerContentTop; + end = std::clamp(end, min, max); + end.y += beg.y; + return { beg, end }; } @@ -449,11 +377,11 @@ void COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) // // It is important that we don't actually print that character out though, as it's only for the calling application to see. // That's why we flush the contents before the insertion and then ensure that the _flushBuffer() call in Read() exits early. - const auto cursor = _buffer.GetCursorPosition(); - _buffer.Replace(cursor, npos, nullptr, 0); - _flushBuffer(); - _buffer.Replace(cursor, 0, &wch, 1); - _buffer.MarkAsClean(); + _replace(_bufferCursor, npos, nullptr, 0); + _dirty = true; + _redisplay(); + _replace(_bufferCursor, 0, &wch, 1); + _dirty = false; _controlKeyState = modifiers; _transitionState(State::DoneWithWakeupMask); @@ -465,7 +393,7 @@ void COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) case UNICODE_CARRIAGERETURN: { // NOTE: Don't append newlines to the buffer just yet! See _handlePostCharInputLoop for more information. - _buffer.SetCursorPosition(npos); + _setCursorPosition(npos); _transitionState(State::DoneWithCarriageReturn); return; } @@ -473,9 +401,9 @@ void COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) case UNICODE_BACKSPACE: if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT)) { - const auto cursor = _buffer.GetCursorPosition(); - const auto pos = wch == EXTKEY_ERASE_PREV_WORD ? _wordPrev(_buffer.Get(), cursor) : TextBuffer::GraphemePrev(_buffer.Get(), cursor); - _buffer.Replace(pos, cursor - pos, nullptr, 0); + const auto cursor = _bufferCursor; + const auto pos = wch == EXTKEY_ERASE_PREV_WORD ? _wordPrev(_buffer, cursor) : TextBuffer::GraphemePrev(_buffer, cursor); + _replace(pos, cursor - pos, nullptr, 0); return; } // If processed mode is disabled, control characters like backspace are treated like any other character. @@ -489,11 +417,11 @@ void COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) { // TODO GH#15875: If the input grapheme is >1 char, then this will replace >1 grapheme // --> We should accumulate input text as much as possible and then call _processInput with wstring_view. - const auto cursor = _buffer.GetCursorPosition(); - remove = TextBuffer::GraphemeNext(_buffer.Get(), cursor) - cursor; + const auto cursor = _bufferCursor; + remove = TextBuffer::GraphemeNext(_buffer, cursor) - cursor; } - _buffer.Replace(_buffer.GetCursorPosition(), remove, &wch, 1); + _replace(_bufferCursor, remove, &wch, 1); } // Handles non-character input for _readCharInputLoop() when no popups exist. @@ -505,62 +433,62 @@ void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) switch (vkey) { case VK_ESCAPE: - if (!_buffer.Get().empty()) + if (!_buffer.empty()) { - _buffer.Replace(0, npos, nullptr, 0); + _replace(0, npos, nullptr, 0); } break; case VK_HOME: - if (_buffer.GetCursorPosition() > 0) + if (_bufferCursor > 0) { if (ctrlPressed) { - _buffer.Replace(0, _buffer.GetCursorPosition(), nullptr, 0); + _replace(0, _bufferCursor, nullptr, 0); } - _buffer.SetCursorPosition(0); + _setCursorPosition(0); } break; case VK_END: - if (_buffer.GetCursorPosition() < _buffer.Get().size()) + if (_bufferCursor < _buffer.size()) { if (ctrlPressed) { - _buffer.Replace(_buffer.GetCursorPosition(), npos, nullptr, 0); + _replace(_bufferCursor, npos, nullptr, 0); } - _buffer.SetCursorPosition(npos); + _setCursorPosition(npos); } break; case VK_LEFT: - if (_buffer.GetCursorPosition() != 0) + if (_bufferCursor != 0) { if (ctrlPressed) { - _buffer.SetCursorPosition(_wordPrev(_buffer.Get(), _buffer.GetCursorPosition())); + _setCursorPosition(_wordPrev(_buffer, _bufferCursor)); } else { - _buffer.SetCursorPosition(TextBuffer::GraphemePrev(_buffer.Get(), _buffer.GetCursorPosition())); + _setCursorPosition(TextBuffer::GraphemePrev(_buffer, _bufferCursor)); } } break; case VK_F1: case VK_RIGHT: - if (_buffer.GetCursorPosition() != _buffer.Get().size()) + if (_bufferCursor != _buffer.size()) { if (ctrlPressed && vkey == VK_RIGHT) { - _buffer.SetCursorPosition(_wordNext(_buffer.Get(), _buffer.GetCursorPosition())); + _setCursorPosition(_wordNext(_buffer, _bufferCursor)); } else { - _buffer.SetCursorPosition(TextBuffer::GraphemeNext(_buffer.Get(), _buffer.GetCursorPosition())); + _setCursorPosition(TextBuffer::GraphemeNext(_buffer, _bufferCursor)); } } else if (_history) { // Traditionally pressing right at the end of an input line would paste characters from the previous command. const auto cmd = _history->GetLastCommand(); - const auto bufferSize = _buffer.Get().size(); + const auto bufferSize = _buffer.size(); const auto cmdSize = cmd.size(); size_t bufferBeg = 0; size_t cmdBeg = 0; @@ -573,11 +501,11 @@ void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) if (bufferBeg >= bufferSize) { - _buffer.Replace(npos, 0, cmd.data() + cmdBeg, cmdEnd - cmdBeg); + _replace(npos, 0, cmd.data() + cmdBeg, cmdEnd - cmdBeg); break; } - bufferBeg = TextBuffer::GraphemeNext(_buffer.Get(), bufferBeg); + bufferBeg = TextBuffer::GraphemeNext(_buffer, bufferBeg); cmdBeg = cmdEnd; } } @@ -587,36 +515,36 @@ void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) _screenInfo.SetCursorDBMode(_insertMode != ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode()); break; case VK_DELETE: - if (_buffer.GetCursorPosition() < _buffer.Get().size()) + if (_bufferCursor < _buffer.size()) { - const auto beg = _buffer.GetCursorPosition(); - const auto end = TextBuffer::GraphemeNext(_buffer.Get(), beg); - _buffer.Replace(beg, end - beg, nullptr, 0); + const auto beg = _bufferCursor; + const auto end = TextBuffer::GraphemeNext(_buffer, beg); + _replace(beg, end - beg, nullptr, 0); } break; case VK_UP: case VK_F5: if (_history && !_history->AtFirstCommand()) { - _buffer.Replace(_history->Retrieve(CommandHistory::SearchDirection::Previous)); + _replace(_history->Retrieve(CommandHistory::SearchDirection::Previous)); } break; case VK_DOWN: if (_history && !_history->AtLastCommand()) { - _buffer.Replace(_history->Retrieve(CommandHistory::SearchDirection::Next)); + _replace(_history->Retrieve(CommandHistory::SearchDirection::Next)); } break; case VK_PRIOR: if (_history && !_history->AtFirstCommand()) { - _buffer.Replace(_history->RetrieveNth(0)); + _replace(_history->RetrieveNth(0)); } break; case VK_NEXT: if (_history && !_history->AtLastCommand()) { - _buffer.Replace(_history->RetrieveNth(INT_MAX)); + _replace(_history->RetrieveNth(INT_MAX)); } break; case VK_F2: @@ -629,10 +557,10 @@ void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) if (_history) { const auto last = _history->GetLastCommand(); - if (last.size() > _buffer.GetCursorPosition()) + if (last.size() > _bufferCursor) { - const auto count = last.size() - _buffer.GetCursorPosition(); - _buffer.Replace(_buffer.GetCursorPosition(), npos, last.data() + _buffer.GetCursorPosition(), count); + const auto count = last.size() - _bufferCursor; + _replace(_bufferCursor, npos, last.data() + _bufferCursor, count); } } break; @@ -666,12 +594,12 @@ void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) if (_history) { CommandHistory::Index index = 0; - const auto cursorPos = _buffer.GetCursorPosition(); - const auto prefix = std::wstring_view{ _buffer.Get() }.substr(0, cursorPos); + const auto cursorPos = _bufferCursor; + const auto prefix = std::wstring_view{ _buffer }.substr(0, cursorPos); if (_history->FindMatchingCommand(prefix, _history->LastDisplayed, index, CommandHistory::MatchOptions::None)) { - _buffer.Replace(_history->RetrieveNth(index)); - _buffer.SetCursorPosition(cursorPos); + _replace(_history->RetrieveNth(index)); + _setCursorPosition(cursorPos); } } break; @@ -699,7 +627,7 @@ void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) void COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) { auto writer = _userBuffer; - auto buffer = _buffer.Extract(); + auto buffer = std::move(_buffer); std::wstring_view input{ buffer }; size_t lineCount = 1; @@ -797,453 +725,457 @@ void COOKED_READ_DATA::_transitionState(State state) noexcept _state = state; } -// Draws the contents of _buffer onto the screen. -// -// By using _buffer._dirtyBeg to avoid redrawing the buffer unless needed, we turn the amortized -// time complexity of _readCharInputLoop() from O(n^2) (n(n+1)/2 redraws) into O(n). -// Pasting text would quickly turn into "accidentally quadratic" meme material otherwise. -// -// NOTE: Don't call _flushBuffer() after appending newlines to the buffer! See _handlePostCharInputLoop for more information. -void COOKED_READ_DATA::_flushBuffer() +void COOKED_READ_DATA::_replace(size_t offset, size_t remove, const wchar_t* input, size_t count) { - if (_buffer.IsClean() || WI_IsFlagClear(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) - { - return; - } + const auto size = _buffer.size(); + offset = std::min(offset, size); + remove = std::min(remove, size - offset); - // `_buffer` is split up by two different indices: - // * `_buffer._cursor`: Text before the `_buffer._cursor` index must be accumulated - // into `distanceBeforeCursor` and the other half into `distanceAfterCursor`. - // This helps us figure out where the cursor is positioned on the screen. - // * `_buffer._dirtyBeg`: Text before `_buffer._dirtyBeg` must be written with SuppressMSAA - // and the other half without. Any text before `_buffer._dirtyBeg` is considered unchanged, - // and this split prevents us from announcing text that hasn't actually changed - // to accessibility tools via MSAA (or UIA, but UIA is robust against this anyways). - // - // This results in 2*2 = 4 writes of which always at least one of the middle two is empty, - // depending on whether _buffer._cursor > _buffer._dirtyBeg or _buffer._cursor < _buffer._dirtyBeg. - // slice() returns an empty string-view when `from` index is greater than the `to` index. + _buffer.replace(offset, remove, input, count); + _bufferCursor = offset + count; + _dirty = true; - ptrdiff_t distanceBeforeCursor = 0; - ptrdiff_t distanceAfterCursor = 0; + if (offset <= _bufferDirtyBeg) { - // _distanceCursor might be larger than the entire viewport (= a really long input line). - // _offsetCursorPosition() with such an offset will end up clamping the cursor position to (0,0). - // To make this implementation behave a little bit more consistent in this case without - // writing a more thorough and complex readline implementation, we pass _measureChars() - // the relative "distance" to the current actual cursor position. That way _measureChars() - // can still figure out what the logical cursor position is, when it handles tabs, etc. - auto dirtyBegDistance = -_distanceCursor; - - distanceBeforeCursor = _measureChars(_buffer.GetUnmodifiedTextBeforeCursor(), dirtyBegDistance); - dirtyBegDistance += distanceBeforeCursor; - distanceAfterCursor = _measureChars(_buffer.GetUnmodifiedTextAfterCursor(), dirtyBegDistance); - dirtyBegDistance += distanceAfterCursor; - - _offsetCursorPosition(dirtyBegDistance); + const auto& textBuffer = _screenInfo.GetTextBuffer(); + _bufferDirtyBeg = textBuffer.GraphemePrev(_buffer, offset); } - - // Now we can finally write the parts of _buffer that have actually changed (or moved). - distanceBeforeCursor += _writeChars(_buffer.GetModifiedTextBeforeCursor()); - distanceAfterCursor += _writeChars(_buffer.GetModifiedTextAfterCursor()); - - const auto distanceEnd = distanceBeforeCursor + distanceAfterCursor; - const auto eraseDistance = std::max(0, _distanceEnd - distanceEnd); - - // If the contents of _buffer became shorter we'll have to erase the previously printed contents. - _erase(eraseDistance); - // Using the *Always() variant ensures that we reset the blinking timer, etc., even if the cursor didn't move. - _offsetCursorPositionAlways(-eraseDistance - distanceAfterCursor); - - _buffer.MarkAsClean(); - _distanceCursor = distanceBeforeCursor; - _distanceEnd = distanceEnd; } -// This is just a small helper to fill the next N cells starting at the current cursor position with whitespace. -void COOKED_READ_DATA::_erase(ptrdiff_t distance) const +void COOKED_READ_DATA::_replace(const std::wstring_view& str) { - if (distance <= 0) - { - return; - } - - std::array whitespace; - auto remaining = gsl::narrow_cast(distance); - auto nextWriteSize = std::min(remaining, whitespace.size()); - - // If we only need to erase 1 character worth of whitespace, - // we don't need to initialize 256 bytes worth of a whitespace array. - // nextWriteSize can only ever shrink past this point if anything. - std::fill_n(whitespace.begin(), nextWriteSize, L' '); + _buffer.assign(str); + _bufferCursor = _buffer.size(); + _bufferDirtyBeg = 0; + _dirty = true; +} - do - { - std::ignore = _writeChars({ whitespace.data(), nextWriteSize }); - remaining -= nextWriteSize; - nextWriteSize = std::min(remaining, whitespace.size()); - } while (remaining != 0); +void COOKED_READ_DATA::_setCursorPosition(size_t position) noexcept +{ + _bufferCursor = std::min(position, _buffer.size()); + _dirty = true; } -// A helper to calculate the number of cells `text` would take up if it were written. -// `cursorOffset` allows the caller to specify a "logical" cursor position relative to the actual cursor position. -// This allows the function to track in which column it currently is, which is needed to implement tabs for instance. -ptrdiff_t COOKED_READ_DATA::_measureChars(const std::wstring_view& text, ptrdiff_t cursorOffset) const +std::wstring_view COOKED_READ_DATA::_slice(size_t from, size_t to) const noexcept { - if (text.empty()) - { - return 0; - } - return _writeCharsImpl(text, true, cursorOffset); + to = std::min(to, _buffer.size()); + from = std::min(from, to); + return std::wstring_view{ _buffer.data() + from, to - from }; } -// A helper to write text and calculate the number of cells we've written. -// _unwindCursorPosition then allows us to go that many cells back. Tracking cells instead of explicit -// buffer positions allows us to pay no further mind to whether the buffer scrolled up or not. -ptrdiff_t COOKED_READ_DATA::_writeChars(const std::wstring_view& text) const +// Draws the contents of _buffer onto the screen. +// +// By using the _dirty flag we avoid redrawing the buffer unless needed. +// This turns the amortized time complexity of _readCharInputLoop() from O(n^2) (n(n+1)/2 redraws) into O(n). +// Without this, pasting text would otherwise quickly turn into "accidentally quadratic" meme material. +// +// NOTE: Don't call _flushBuffer() after appending newlines to the buffer! See _handlePostCharInputLoop for more information. +void COOKED_READ_DATA::_redisplay() { - if (text.empty()) + if (!_dirty || WI_IsFlagClear(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) { - return 0; + return; } - return _writeCharsImpl(text, false, 0); -} -ptrdiff_t COOKED_READ_DATA::_writeCharsImpl(const std::wstring_view& text, const bool measureOnly, const ptrdiff_t cursorOffset) const -{ - const auto width = _screenInfo.GetTextBuffer().GetSize().Width(); - auto it = text.begin(); - const auto end = text.end(); - ptrdiff_t distance = 0; + const auto size = _screenInfo.GetVirtualViewport().Dimensions(); + auto originInViewportFinal = _originInViewport; + til::point cursorPositionFinal; + til::point pagerPromptEnd; + std::vector lines; + // FYI: This loop does not loop. It exists because goto is considered evil + // and if MSVC says that then that must be true. for (;;) { - const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return wch < L' '; }); - if (nextControlChar != it) + cursorPositionFinal = { _originInViewport.x, 0 }; + + // Construct the first line manually so that it starts at the correct horizontal position. + LayoutResult res{ .column = cursorPositionFinal.x }; + lines.emplace_back(std::wstring{}, 0, cursorPositionFinal.x, cursorPositionFinal.x); + + // Split the buffer into 3 segments, so that we can find the row/column coordinates of + // the cursor within the buffer, as well as the start of the dirty parts of the buffer. + const size_t offsets[]{ + 0, + std::min(_bufferDirtyBeg, _bufferCursor), + std::max(_bufferDirtyBeg, _bufferCursor), + npos, + }; + + for (int i = 0; i < 3; i++) { - if (measureOnly) + const auto& segment = til::safe_slice_abs(_buffer, offsets[i], offsets[i + 1]); + if (segment.empty()) { - distance += _measureCharsUnprocessed({ it, nextControlChar }, distance + cursorOffset); + continue; } - else + + const auto dirty = offsets[i] >= _bufferDirtyBeg; + + // Layout the _buffer contents into lines. + for (size_t beg = 0; beg < segment.size();) { - distance += _writeCharsUnprocessed({ it, nextControlChar }); + if (res.column >= size.width) + { + lines.emplace_back(); + } + + auto& line = lines.back(); + res = _layoutLine(line.text, segment, beg, line.columns, size.width); + line.columns = res.column; + + if (!dirty) + { + line.dirtyBegOffset = line.text.size(); + line.dirtyBegColumn = res.column; + } + + beg = res.offset; + } + + // If this segment ended at the cursor offset, we got our cursor position in rows/columns. + if (offsets[i + 1] == _bufferCursor) + { + cursorPositionFinal = { res.column, gsl::narrow_cast(lines.size() - 1) }; } - it = nextControlChar; - } - if (nextControlChar == end) - { - break; } - wchar_t buf[2]; - size_t len = 0; + pagerPromptEnd = { res.column, gsl::narrow_cast(lines.size() - 1) }; - const auto wch = *it; - if (wch == UNICODE_TAB) - { - buf[0] = L'\t'; - len = 1; - } - else + // If the content got a little shorter than it was before, we need to erase the tail end. + // If entire lines got removed, then we'll fix this later when comparing against _pagerContentEnd.y. + if (pagerPromptEnd.y <= _pagerPromptEnd.y) { - // In the interactive mode we replace C0 control characters (0x00-0x1f) with ASCII representations like ^C (= 0x03). - buf[0] = L'^'; - buf[1] = gsl::narrow_cast(wch + L'@'); - len = 2; + auto& line = lines.back(); + const auto endX = _pagerPromptEnd.y == pagerPromptEnd.y ? _pagerPromptEnd.x : size.width; + const auto remaining = endX - line.columns; + if (remaining > 0) + { + // CSI K may be expensive, so use spaces if we can. + if (remaining <= 8) + { + line.text.append(remaining, L' '); + } + else + { + line.text.append(L"\x1b[K"); + } + } } - if (measureOnly) + // Render the popups, if there are any. + if (!_popups.empty()) { - if (wch == UNICODE_TAB) + auto& popup = _popups.back(); + + // But it should not be right on the line where the prompt starts. + // That would look goofy otherwise, since there's where the text is supposed to go. + if (lines.empty()) { - const auto col = _getColumnAtRelativeCursorPosition(distance + cursorOffset); - const auto remaining = width - col; - distance += std::min(remaining, 8 - (col & 7)); + lines.emplace_back(); } - else + + // Ensure that the popup is not considered part of the prompt line. That is, if someone double-clicks + // to select the last word in the prompt, it should not select the first word in the popup. + lines.back().text.append(L"\r\n"); + + switch (popup.kind) { - distance += 2; + case PopupKind::CopyToChar: + _popupDrawPrompt(lines, size, ID_CONSOLE_MSGCMDLINEF2, {}); + break; + case PopupKind::CopyFromChar: + _popupDrawPrompt(lines, size, ID_CONSOLE_MSGCMDLINEF4, {}); + break; + case PopupKind::CommandNumber: + _popupDrawPrompt(lines, size, ID_CONSOLE_MSGCMDLINEF9, { popup.commandNumber.buffer.data(), CommandNumberMaxInputLength }); + break; + case PopupKind::CommandList: + _popupDrawCommandList(lines, size, popup); + break; + default: + assert(false); } + + // Put the cursor at the end of the contents. This ensures we scroll all the way down. + cursorPositionFinal.x = lines.back().columns; + cursorPositionFinal.y = gsl::narrow_cast(lines.size()) - 1; } - else + // If the cursor is at a delay-wrapped position, wrap it explicitly. + // This ensures that the cursor is always "after" the insertion position. + // We don't need to do this when popups are present, because they're not supposed to end in a newline. + else if (cursorPositionFinal.x >= size.width) { - distance += _writeCharsUnprocessed({ &buf[0], len }); + cursorPositionFinal.x = 0; + cursorPositionFinal.y++; + + // If the cursor is at the end of the buffer however, we have to insert + // a line because otherwise it won't have any space to be visible. + if (_bufferCursor == _buffer.size()) + { + lines.emplace_back(L" \b"); + } } - ++it; + // Usually we'll be on a "prompt> ..." line and behave like a regular single-line-editor. + // But once the entire viewport is full of text, we need to behave more like a pager (= scrolling, etc.). + // This code retries the layout process if needed, because then the cursor starts at origin {0, 0}. + if (gsl::narrow_cast(lines.size()) > size.height && originInViewportFinal.x != 0) + { + lines.clear(); + _originInViewport.x = 0; + _bufferDirtyBeg = 0; + originInViewportFinal = {}; + continue; + } + + break; } - return distance; -} + const auto lineCount = gsl::narrow_cast(lines.size()); + const auto pagerHeight = std::min(lineCount, size.height); -ptrdiff_t COOKED_READ_DATA::_measureCharsUnprocessed(const std::wstring_view& text, ptrdiff_t cursorOffset) const -{ - if (text.empty()) + // If the contents of the prompt are longer than the remaining number of lines in the viewport, + // we need to reduce originInViewportFinal.y towards 0 to account for that. In other words, + // as the viewport fills itself with text the _originInViewport will slowly move towards 0. + originInViewportFinal.y = std::min(originInViewportFinal.y, size.height - pagerHeight); + + auto pagerContentTop = _pagerContentTop; + // If the cursor is above the viewport, we go up... + pagerContentTop = std::min(pagerContentTop, cursorPositionFinal.y); + // and if the cursor is below it, we go down. + pagerContentTop = std::max(pagerContentTop, cursorPositionFinal.y - size.height + 1); + // The value may be out of bounds, because the above min/max doesn't ensure this on its own. + pagerContentTop = std::clamp(pagerContentTop, 0, lineCount - pagerHeight); + + // Transform the recorded position from the lines vector coordinate space into VT screen space. + // Due to the above scrolling of pagerTop, cursorPosition should now always be within the viewport. + // dirtyBegPosition however could be outside of it. + cursorPositionFinal.y += originInViewportFinal.y - pagerContentTop; + + std::wstring output; + + // Disable the cursor when opening a popup, reenable it when closing them. + if (const auto popupOpened = !_popups.empty(); _popupOpened != popupOpened) { - return 0; + wchar_t buf[] = L"\x1b[?25l"; + buf[5] = popupOpened ? 'l' : 'h'; + output.append(&buf[0], 6); + _popupOpened = popupOpened; } - auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto width = textBuffer.GetSize().Width(); - auto columnLimit = width - _getColumnAtRelativeCursorPosition(cursorOffset); + // Scroll the contents of the pager if needed, so we only need to write what actually changed. + if (const auto delta = pagerContentTop - _pagerContentTop; delta != 0) + { + const auto deltaAbs = abs(delta); + til::CoordType beg = 0; + til::CoordType end = pagerHeight; - size_t offset = 0; - ptrdiff_t distance = 0; + // If the top changed by more than the viewport height, scrolling doesn't make sense. + if (deltaAbs < size.height) + { + beg = delta >= 0 ? pagerHeight - deltaAbs : 0; + end = delta >= 0 ? pagerHeight : deltaAbs; + const auto cmd = delta >= 0 ? L'S' : L'T'; + fmt::format_to(std::back_inserter(output), FMT_COMPILE(L"\x1b[{}{}"), deltaAbs, cmd); + } + + // Mark each row that has been uncovered by the scroll as dirty. + for (auto i = beg; i < end; i++) + { + auto& line = lines.at(i + pagerContentTop); + line.dirtyBegOffset = 0; + line.dirtyBegColumn = 0; + } + } - while (offset < text.size()) + bool anyDirty = false; + for (til::CoordType i = 0; i < pagerHeight; i++) { - til::CoordType columns = 0; - offset += textBuffer.FitTextIntoColumns(text.substr(offset), columnLimit, columns); - distance += columns; - columnLimit = width; + const auto& line = lines.at(i + pagerContentTop); + anyDirty = line.dirtyBegOffset < line.text.size(); + if (anyDirty) + { + break; + } } - return distance; -} + til::point writeCursorPosition{ -1, -1 }; -// A helper to write text and calculate the number of cells we've written. -// _unwindCursorPosition then allows us to go that many cells back. Tracking cells instead of explicit -// buffer positions allows us to pay no further mind to whether the buffer scrolled up or not. -ptrdiff_t COOKED_READ_DATA::_writeCharsUnprocessed(const std::wstring_view& text) const -{ - if (text.empty()) + if (anyDirty) { - return 0; - } +#if COOKED_READ_DEBUG + static size_t debugColorIndex = 0; + const auto color = til::colorbrewer::dark2[++debugColorIndex % std::size(til::colorbrewer::dark2)]; + fmt::format_to(std::back_inserter(output), FMT_COMPILE(L"\x1b[48;2;{};{};{}m"), GetRValue(color), GetGValue(color), GetBValue(color)); +#endif - const auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - const auto width = static_cast(textBuffer.GetSize().Width()); - const auto initialCursorPos = cursor.GetPosition(); - til::CoordType scrollY = 0; + for (til::CoordType i = 0; i < pagerHeight; i++) + { + const auto row = std::min(_originInViewport.y + i, size.height - 1); - WriteCharsLegacy(_screenInfo, text, &scrollY); + // If the last write left the cursor at the end of a line, the next write will start at the beginning of the next line. + // This avoids needless calls to _appendCUP. The reason it's here and not at the end of the loop is similar to how + // delay-wrapping in VT works: The line wrap only occurs after writing 1 more character than fits on the line. + if (writeCursorPosition.x >= size.width) + { + writeCursorPosition.x = 0; + writeCursorPosition.y = row; + } - const auto finalCursorPos = cursor.GetPosition(); - const auto distance = (finalCursorPos.y - initialCursorPos.y + scrollY) * width + finalCursorPos.x - initialCursorPos.x; - assert(distance >= 0); - return distance; -} + const auto& line = lines.at(i + pagerContentTop); -// Moves the given point by the given distance inside the text buffer, as if moving a cursor with the left/right arrow keys. -til::point COOKED_READ_DATA::_offsetPosition(til::point pos, ptrdiff_t distance) const -{ - if (distance == 0) - { - return pos; - } + // Skip lines that aren't marked as dirty. + if (line.dirtyBegOffset >= line.text.size()) + { + continue; + } - const auto size = _screenInfo.GetTextBuffer().GetSize().Dimensions(); - const auto w = static_cast(size.width); - const auto h = static_cast(size.height); - const auto area = w * h; + // Position the cursor wherever the dirty part of the line starts. + if (const til::point pos{ line.dirtyBegColumn, row }; writeCursorPosition != pos) + { + writeCursorPosition = pos; + _appendCUP(output, pos); + } - auto off = w * pos.y + pos.x; - off += distance; - off = off < 0 ? 0 : (off > area ? area : off); + output.append(line.text, line.dirtyBegOffset); + writeCursorPosition.x = line.columns; + } - return { - gsl::narrow_cast(off % w), - gsl::narrow_cast(off / w), - }; -} +#if COOKED_READ_DEBUG + output.append(L"\x1b[m"); +#endif + } -// See _offsetCursorPositionAlways(). This wrapper is just here to avoid doing -// expensive cursor movements when there's nothing to move. A no-op wrapper. -void COOKED_READ_DATA::_offsetCursorPosition(ptrdiff_t distance) const -{ - if (distance != 0) + // Clear any lines that we previously filled and are now empty. { - _offsetCursorPositionAlways(distance); + const auto pagerHeightPrevious = std::min(_pagerContentHeight, size.height); + + if (pagerHeight < pagerHeightPrevious) + { + const auto row = std::min(_originInViewport.y + pagerHeight, size.height - 1); + _appendCUP(output, { 0, row }); + output.append(L"\x1b[K"); + + for (til::CoordType i = pagerHeight + 1; i < pagerHeightPrevious; i++) + { + output.append(L"\x1b[E\x1b[K"); + } + } } -} -// This moves the cursor `distance`-many cells around in the buffer. -// It's intended to be used in combination with _writeChars. -// Usually you should use _offsetCursorPosition() to no-op distance==0. -void COOKED_READ_DATA::_offsetCursorPositionAlways(ptrdiff_t distance) const -{ - const auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - const auto pos = _offsetPosition(cursor.GetPosition(), distance); + _appendCUP(output, cursorPositionFinal); + WriteCharsVT(_screenInfo, output); - std::ignore = _screenInfo.SetCursorPosition(pos, true); - _screenInfo.MakeCursorVisible(pos); + _originInViewport = originInViewportFinal; + _pagerPromptEnd = pagerPromptEnd; + _pagerContentTop = pagerContentTop; + _pagerContentHeight = lineCount; + _bufferDirtyBeg = _buffer.size(); + _dirty = false; } -til::CoordType COOKED_READ_DATA::_getColumnAtRelativeCursorPosition(ptrdiff_t distance) const +COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& output, const std::wstring_view& input, const size_t inputOffset, const til::CoordType columnBegin, const til::CoordType columnLimit) const { const auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto size = _screenInfo.GetTextBuffer().GetSize().Dimensions(); - const auto& cursor = textBuffer.GetCursor(); - const auto pos = cursor.GetPosition(); + const auto beg = input.data(); + const auto end = beg + input.size(); + auto it = beg + std::min(inputOffset, input.size()); + auto column = std::min(columnBegin, columnLimit); + + output.reserve(output.size() + columnLimit - column); - auto x = gsl::narrow_cast((pos.x + distance) % size.width); - if (x < 0) + while (it != end) { - x += size.width; - } + const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return wch < L' '; }); + if (it != nextControlChar) + { + std::wstring_view text{ it, nextControlChar }; + til::CoordType cols = 0; + const auto len = textBuffer.FitTextIntoColumns(text, columnLimit - column, cols); - return x; -} + output.append(text, 0, len); + column += cols; + it += len; -// If the viewport is large enough to fit a popup, this function prepares everything for -// showing the given type. It handles computing the size of the popup, its position, -// backs the affected area up and draws the border and initial contents. -void COOKED_READ_DATA::_popupPush(const PopupKind kind) -try -{ - auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto viewport = _screenInfo.GetViewport(); - const auto viewportOrigin = viewport.Origin(); - const auto viewportSize = viewport.Dimensions(); + if (len < text.size() || it == end) + { + break; + } + } - til::size proposedSize; - switch (kind) - { - case PopupKind::CopyToChar: - proposedSize = { 26, 1 }; - break; - case PopupKind::CopyFromChar: - proposedSize = { 28, 1 }; - break; - case PopupKind::CommandNumber: - proposedSize = { 22 + CommandNumberMaxInputLength, 1 }; - break; - case PopupKind::CommandList: - { - const auto& commands = _history->GetCommands(); - const auto commandCount = _history->GetNumberOfCommands(); + const auto wch = *it++; + wchar_t buf[8]; + til::CoordType len = 0; - size_t maxStringLength = 0; - for (const auto& c : commands) + if (wch == UNICODE_TAB) { - maxStringLength = std::max(maxStringLength, c.size()); + const auto remaining = columnLimit - column; + len = std::min(8 - (column & 7), remaining); + std::fill_n(&buf[0], len, L' '); + } + else + { + buf[0] = L'^'; + buf[1] = wch + L'@'; + len = 2; } - // Account for the "123: " prefix each line gets. - maxStringLength += integerLog10(commandCount); - maxStringLength += 3; - - // conhost used to draw the command list with a size of 40x10, but at some point it switched over to dynamically - // sizing it depending on the history count and width of the entries. Back then it was implemented with the - // assumption that the code unit count equals the column count, which I kept because I don't want the TextBuffer - // class to expose how wide characters are, any more than necessary. It makes implementing Unicode support - // much harder, because things like combining marks and work zones may cause TextBuffer to end up deciding - // a piece of text has a different size than what you thought it had when measuring it on its own. - proposedSize.width = gsl::narrow_cast(std::clamp(maxStringLength, 40, til::CoordTypeMax)); - proposedSize.height = std::clamp(commandCount, 10, 20); - break; - } - default: - assert(false); - return; + if (column + len <= columnLimit) + { + column += len; + output.append(buf, len); + } } - // Subtract 2 because we need to draw the border around our content. We must return early if we're - // unable to do so, otherwise the remaining code fails because the size would be zero/negative. - const til::size contentSize{ - std::min(proposedSize.width, viewportSize.width - 2), - std::min(proposedSize.height, viewportSize.height - 2), - }; - if (!contentSize) + const size_t offset = it - beg; + + if (offset < input.size() && column < columnLimit) { - return; + column = columnLimit; + output.append(columnLimit - column, L' '); } - const auto widthSizeT = gsl::narrow_cast(contentSize.width + 2); - const auto heightSizeT = gsl::narrow_cast(contentSize.height + 2); - const til::point contentOrigin{ - (viewportSize.width - contentSize.width) / 2 + viewportOrigin.x, - (viewportSize.height - contentSize.height) / 2 + viewportOrigin.y, - }; - const til::rect contentRect{ - contentOrigin, - contentSize, + return { + .offset = offset, + .column = column, }; - const auto backupRect = Viewport::FromExclusive({ - contentRect.left - 1, - contentRect.top - 1, - contentRect.right + 1, - contentRect.bottom + 1, - }); - - auto& popup = _popups.emplace_back(kind, contentRect, backupRect, std::vector{ widthSizeT * heightSizeT }); - - // Create a backup of the TextBuffer parts we're scribbling over. - // We need to flush the buffer to ensure we capture the latest contents. - // NOTE: This may theoretically modify popup.backupRect (practically unlikely). - _flushBuffer(); - THROW_IF_FAILED(ServiceLocator::LocateGlobals().api->ReadConsoleOutputWImpl(_screenInfo, popup.backup, backupRect, popup.backupRect)); - - // Draw the border around our content and fill it with whitespace to prepare it for future usage. - { - const auto attributes = _screenInfo.GetPopupAttributes(); +} - RowWriteState state{ - .columnBegin = contentRect.left - 1, - .columnLimit = contentRect.right + 1, - }; +void COOKED_READ_DATA::_appendCUP(std::wstring& output, til::point pos) +{ + fmt::format_to(std::back_inserter(output), FMT_COMPILE(L"\x1b[{};{}H"), pos.y + 1, pos.x + 1); +} - // top line ┌───┐ - std::wstring buffer; - buffer.assign(widthSizeT, L'─'); - buffer.front() = L'┌'; - buffer.back() = L'┐'; - state.text = buffer; - textBuffer.Replace(contentRect.top - 1, attributes, state); - - // bottom line └───┘ - buffer.front() = L'└'; - buffer.back() = L'┘'; - state.text = buffer; - textBuffer.Replace(contentRect.bottom, attributes, state); - - // middle lines │ │ - buffer.assign(widthSizeT, L' '); - buffer.front() = L'│'; - buffer.back() = L'│'; - for (til::CoordType y = contentRect.top; y < contentRect.bottom; ++y) - { - state.text = buffer; - textBuffer.Replace(y, attributes, state); - } - } +void COOKED_READ_DATA::_appendPopupAttr(std::wstring& output) const +{ + VtIo::FormatAttributes(output, _screenInfo.GetPopupAttributes().GetLegacyAttributes()); +} + +void COOKED_READ_DATA::_popupPush(const PopupKind kind) +try +{ + auto& popup = _popups.emplace_back(kind); + _dirty = true; switch (kind) { - case PopupKind::CopyToChar: - _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF2); - break; - case PopupKind::CopyFromChar: - _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF4); - break; case PopupKind::CommandNumber: popup.commandNumber.buffer.fill(' '); popup.commandNumber.bufferSize = 0; - _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF9); break; case PopupKind::CommandList: + popup.commandList.top = -1; + popup.commandList.height = 10; popup.commandList.selected = _history->LastDisplayed; - popup.commandList.top = popup.commandList.selected - contentSize.height / 2; - _popupDrawCommandList(popup); break; default: - assert(false); - } - - // If this is the first popup to be shown, stop the cursor from appearing/blinking - if (_popups.size() == 1) - { - textBuffer.GetCursor().SetIsPopupShown(true); + break; } } catch (...) { LOG_CAUGHT_EXCEPTION(); - // Using _popupsDone() is a convenient way to restore the buffer contents if anything in this call failed. - // This could technically dismiss an unrelated popup that was already in _popups, but reaching this point is unlikely anyways. _popupsDone(); } @@ -1252,23 +1184,8 @@ catch (...) // Pressing F7 followed by F9 (CommandNumber on top of CommandList). void COOKED_READ_DATA::_popupsDone() { - while (!_popups.empty()) - { - auto& popup = _popups.back(); - - // Restore TextBuffer contents. They could be empty if _popupPush() - // threw an exception in the middle of construction. - if (!popup.backup.empty()) - { - [[maybe_unused]] Viewport unused; - LOG_IF_FAILED(ServiceLocator::LocateGlobals().api->WriteConsoleOutputWImpl(_screenInfo, popup.backup, popup.backupRect, unused)); - } - - _popups.pop_back(); - } - - // Restore cursor blinking. - _screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(false); + _popups.clear(); + _dirty = true; } void COOKED_READ_DATA::_popupHandleInput(wchar_t wch, uint16_t vkey, DWORD modifiers) @@ -1312,16 +1229,16 @@ void COOKED_READ_DATA::_popupHandleCopyToCharInput(Popup& /*popup*/, const wchar { // See PopupKind::CopyToChar for more information about this code. const auto cmd = _history->GetLastCommand(); - const auto cursor = _buffer.GetCursorPosition(); + const auto cursor = _bufferCursor; const auto idx = cmd.find(wch, cursor); if (idx != decltype(cmd)::npos) { - // When we enter this if condition it's guaranteed that _buffer.GetCursorPosition() must be + // When we enter this if condition it's guaranteed that _bufferCursor must be // smaller than idx, which in turn implies that it's smaller than cmd.size(). // As such, calculating length is safe and str.size() == length. const auto count = idx - cursor; - _buffer.Replace(cursor, count, cmd.data() + cursor, count); + _replace(cursor, count, cmd.data() + cursor, count); } _popupsDone(); @@ -1340,10 +1257,10 @@ void COOKED_READ_DATA::_popupHandleCopyFromCharInput(Popup& /*popup*/, const wch else { // See PopupKind::CopyFromChar for more information about this code. - const auto cursor = _buffer.GetCursorPosition(); - auto idx = _buffer.Get().find(wch, cursor); - idx = std::min(idx, _buffer.Get().size()); - _buffer.Replace(cursor, idx - cursor, nullptr, 0); + const auto cursor = _bufferCursor; + auto idx = _buffer.find(wch, cursor); + idx = std::min(idx, _buffer.size()); + _replace(cursor, idx - cursor, nullptr, 0); _popupsDone(); } } @@ -1362,16 +1279,15 @@ void COOKED_READ_DATA::_popupHandleCommandNumberInput(Popup& popup, const wchar_ if (wch == UNICODE_CARRIAGERETURN) { popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = L'\0'; - _buffer.Replace(_history->RetrieveNth(std::stoi(popup.commandNumber.buffer.data()))); + _replace(_history->RetrieveNth(std::stoi(popup.commandNumber.buffer.data()))); _popupsDone(); - return; } - - if (wch >= L'0' && wch <= L'9') + else if (wch >= L'0' && wch <= L'9') { if (popup.commandNumber.bufferSize < CommandNumberMaxInputLength) { popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = wch; + _dirty = true; } } else if (wch == UNICODE_BACKSPACE) @@ -1379,19 +1295,9 @@ void COOKED_READ_DATA::_popupHandleCommandNumberInput(Popup& popup, const wchar_ if (popup.commandNumber.bufferSize > 0) { popup.commandNumber.buffer[--popup.commandNumber.bufferSize] = L' '; + _dirty = true; } } - else - { - return; - } - - RowWriteState state{ - .text = { popup.commandNumber.buffer.data(), CommandNumberMaxInputLength }, - .columnBegin = popup.contentRect.right - CommandNumberMaxInputLength, - .columnLimit = popup.contentRect.right, - }; - _screenInfo.GetTextBuffer().Replace(popup.contentRect.top, _screenInfo.GetPopupAttributes(), state); } } @@ -1401,7 +1307,7 @@ void COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t if (wch == UNICODE_CARRIAGERETURN) { - _buffer.Replace(_history->RetrieveNth(cl.selected)); + _replace(_history->RetrieveNth(cl.selected)); _popupsDone(); _handleChar(UNICODE_CARRIAGERETURN, modifiers); return; @@ -1425,7 +1331,7 @@ void COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t break; case VK_LEFT: case VK_RIGHT: - _buffer.Replace(_history->RetrieveNth(cl.selected)); + _replace(_history->RetrieveNth(cl.selected)); _popupsDone(); return; case VK_UP: @@ -1453,88 +1359,126 @@ void COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t break; case VK_PRIOR: // _popupDrawCommandList() clamps all values to valid ranges in `cl`. - cl.selected -= popup.contentRect.height(); + cl.selected -= cl.height; break; case VK_NEXT: // _popupDrawCommandList() clamps all values to valid ranges in `cl`. - cl.selected += popup.contentRect.height(); + cl.selected += cl.height; break; default: return; } - _popupDrawCommandList(popup); + _dirty = true; } -void COOKED_READ_DATA::_popupDrawPrompt(const Popup& popup, const UINT id) const +void COOKED_READ_DATA::_popupDrawPrompt(std::vector& lines, const til::size size, const UINT id, const std::wstring_view suffix) const { - const auto text = _LoadString(id); - RowWriteState state{ - .text = text, - .columnBegin = popup.contentRect.left, - .columnLimit = popup.contentRect.right, - }; - _screenInfo.GetTextBuffer().Replace(popup.contentRect.top, _screenInfo.GetPopupAttributes(), state); + auto str = _LoadString(id); + str.append(suffix); + + std::wstring line; + line.append(L"\x1b[K"); + _appendPopupAttr(line); + const auto res = _layoutLine(line, str, 0, 0, size.width); + line.append(L"\x1b[m"); + + lines.emplace_back(std::move(line), 0, res.column); } -void COOKED_READ_DATA::_popupDrawCommandList(Popup& popup) const +void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til::size size, Popup& popup) const { assert(popup.kind == PopupKind::CommandList); auto& cl = popup.commandList; - const auto max = _history->GetNumberOfCommands(); - const auto width = popup.contentRect.narrow_width(); - const auto height = std::min(popup.contentRect.height(), _history->GetNumberOfCommands()); - const auto dirtyHeight = std::max(height, cl.dirtyHeight); + const auto historySize = _history->GetNumberOfCommands(); + const auto indexWidth = gsl::narrow_cast(fmt::formatted_size(FMT_COMPILE(L"{}"), historySize)); - { - // The viewport movement of the popup is anchored around the current selection first and foremost. - cl.selected = std::clamp(cl.selected, 0, max - 1); + // The popup is half the height of the viewport, but at least 1 and at most 20 lines. + // Unless of course the history size is less than that. + const auto height = std::min(historySize, std::clamp(size.height / 2, 1, 20)); - // It then lazily follows it when the selection goes out of the viewport. - if (cl.selected < cl.top) - { - cl.top = cl.selected; - } - else if (cl.selected >= cl.top + height) - { - cl.top = cl.selected - height + 1; - } + // cl.selected may be out of bounds after a page up/down, etc., so we need to clamp it. + cl.selected = std::clamp(cl.selected, 0, historySize - 1); - cl.top = std::clamp(cl.top, 0, max - height); + // If it hasn't been initialized it yet, center the selected item. + if (cl.top < 0) + { + cl.top = std::max(0, cl.selected - height / 2); } - std::wstring buffer; - buffer.reserve(width * 2 + 4); + // If the selection is above the viewport, we go up... + cl.top = std::min(cl.top, cl.selected); + // and if the selection is below it, we go down. + cl.top = std::max(cl.top, cl.selected - height + 1); + // The value may be out of bounds, because the above min/max doesn't ensure this on its own. + cl.top = std::clamp(cl.top, 0, historySize - height); - const auto& attrRegular = _screenInfo.GetPopupAttributes(); - auto attrInverted = attrRegular; - attrInverted.Invert(); + // We also need to update the height for future page up/down movements. + cl.height = height; - RowWriteState state{ - .columnBegin = popup.contentRect.left, - .columnLimit = popup.contentRect.right, - }; + // Calculate the position of the █ track in the scrollbar among all the ▒. + // The position is offset by +1 because at off == 0 we draw the ▲. + // We add historyMax/2 to round the division result to the nearest value. + const auto historyMax = historySize - 1; + const auto trackPositionMax = height - 3; + const auto trackPosition = historyMax <= 0 ? 0 : 1 + (trackPositionMax * cl.selected + historyMax / 2) / historyMax; - for (til::CoordType off = 0; off < dirtyHeight; ++off) + for (til::CoordType off = 0; off < height; ++off) { - const auto y = popup.contentRect.top + off; - const auto historyIndex = cl.top + off; - const auto str = _history->GetNth(historyIndex); - const auto& attr = historyIndex == cl.selected ? attrInverted : attrRegular; + const auto index = cl.top + off; + const auto str = _history->GetNth(index); + const auto selected = index == cl.selected; + + std::wstring line; + line.append(L"\x1b[K"); + _appendPopupAttr(line); + + wchar_t scrollbarChar = L' '; + if (historySize > height) + { + if (off == 0) + { + scrollbarChar = L'▲'; + } + else if (off == height - 1) + { + scrollbarChar = L'▼'; + } + else + { + scrollbarChar = off == trackPosition ? L'█' : L'▒'; + } + } + line.push_back(scrollbarChar); + + if (selected) + { + line.push_back(L'▸'); + } + else + { + line.append(L"\x1b[m "); + } + + fmt::format_to(std::back_inserter(line), FMT_COMPILE(L"{:{}}: "), index, indexWidth); + + auto res = _layoutLine(line, str, 0, indexWidth + 4, size.width - 1); + if (res.offset < str.size()) + { + line.push_back(L'…'); + res.column++; + } - buffer.clear(); - if (!str.empty()) + if (selected) { - buffer.append(std::to_wstring(historyIndex)); - buffer.append(L": "); - buffer.append(str); + line.append(L"\x1b[m"); } - buffer.append(width, L' '); - state.text = buffer; - _screenInfo.GetTextBuffer().Replace(y, attr, state); + line.append(L"\r\n"); + lines.emplace_back(std::move(line), 0, 0, res.column); } - cl.dirtyHeight = height; + auto& lastLine = lines.back(); + lastLine.text.erase(lastLine.text.size() - 2); } diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index 035c3c96679..809797892a3 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -30,7 +30,7 @@ class COOKED_READ_DATA final : public ReadData bool Read(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); - void EraseBeforeResize(); + void EraseBeforeResize() const; void RedrawAfterResize(); void SetInsertMode(bool insertMode) noexcept; @@ -39,7 +39,7 @@ class COOKED_READ_DATA final : public ReadData til::point_span GetBoundaries() const noexcept; private: - static constexpr uint8_t CommandNumberMaxInputLength = 5; + static constexpr size_t CommandNumberMaxInputLength = 5; static constexpr size_t npos = static_cast(-1); enum class State : uint8_t @@ -49,38 +49,6 @@ class COOKED_READ_DATA final : public ReadData DoneWithCarriageReturn, }; - // A helper struct to ensure we keep track of _dirtyBeg while the - // underlying _buffer is being modified by COOKED_READ_DATA. - struct BufferState - { - const std::wstring& Get() const noexcept; - std::wstring Extract() noexcept - { - return std::move(_buffer); - } - void Replace(size_t offset, size_t remove, const wchar_t* input, size_t count); - void Replace(const std::wstring_view& str); - - size_t GetCursorPosition() const noexcept; - void SetCursorPosition(size_t pos) noexcept; - - bool IsClean() const noexcept; - void MarkEverythingDirty() noexcept; - void MarkAsClean() noexcept; - - std::wstring_view GetUnmodifiedTextBeforeCursor() const noexcept; - std::wstring_view GetUnmodifiedTextAfterCursor() const noexcept; - std::wstring_view GetModifiedTextBeforeCursor() const noexcept; - std::wstring_view GetModifiedTextAfterCursor() const noexcept; - - private: - std::wstring_view _slice(size_t from, size_t to) const noexcept; - - std::wstring _buffer; - size_t _dirtyBeg = npos; - size_t _cursor = 0; - }; - enum class PopupKind { // Copies text from the previous command between the current cursor position and the first instance @@ -109,15 +77,6 @@ class COOKED_READ_DATA final : public ReadData { PopupKind kind; - // The inner rectangle of the popup, excluding the border that we draw. - // In absolute TextBuffer coordinates. - til::rect contentRect; - // The area we've backed up and need to restore when we dismiss the popup. - // It'll practically always be 1 larger than contentRect in all 4 directions. - Microsoft::Console::Types::Viewport backupRect; - // The backed up buffer contents. Uses CHAR_INFO for convenience. - std::vector backup; - // Using a std::variant would be preferable in modern C++ but is practically equally annoying to use. union { @@ -126,23 +85,37 @@ class COOKED_READ_DATA final : public ReadData { // Keep 1 char space for the trailing \0 char. std::array buffer; - uint8_t bufferSize; + size_t bufferSize; } commandNumber; // Used by PopupKind::CommandList struct { + // The previous height of the popup. + til::CoordType height; // Command history index of the first row we draw in the popup. + // A value of -1 means it hasn't been initialized yet. CommandHistory::Index top; // Command history index of the currently selected row. CommandHistory::Index selected; - // Tracks the part of the popup that has previously been drawn and needs to be redrawn in the next paint. - // This becomes relevant when the length of the history changes while the popup is open (= when deleting entries). - til::CoordType dirtyHeight; } commandList; }; }; + struct LayoutResult + { + size_t offset; + til::CoordType column = 0; + }; + + struct Line + { + std::wstring text; + size_t dirtyBegOffset = 0; + til::CoordType dirtyBegColumn = 0; + til::CoordType columns = 0; + }; + static size_t _wordPrev(const std::wstring_view& chars, size_t position); static size_t _wordNext(const std::wstring_view& chars, size_t position); @@ -151,17 +124,14 @@ class COOKED_READ_DATA final : public ReadData void _handleVkey(uint16_t vkey, DWORD modifiers); void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); void _transitionState(State state) noexcept; - void _flushBuffer(); - void _erase(ptrdiff_t distance) const; - ptrdiff_t _measureChars(const std::wstring_view& text, ptrdiff_t cursorOffset) const; - ptrdiff_t _writeChars(const std::wstring_view& text) const; - ptrdiff_t _writeCharsImpl(const std::wstring_view& text, bool measureOnly, ptrdiff_t cursorOffset) const; - ptrdiff_t _measureCharsUnprocessed(const std::wstring_view& text, ptrdiff_t cursorOffset) const; - ptrdiff_t _writeCharsUnprocessed(const std::wstring_view& text) const; - til::point _offsetPosition(til::point pos, ptrdiff_t distance) const; - void _offsetCursorPosition(ptrdiff_t distance) const; - void _offsetCursorPositionAlways(ptrdiff_t distance) const; - til::CoordType _getColumnAtRelativeCursorPosition(ptrdiff_t distance) const; + void _replace(size_t offset, size_t remove, const wchar_t* input, size_t count); + void _replace(const std::wstring_view& str); + std::wstring_view _slice(size_t from, size_t to) const noexcept; + void _setCursorPosition(size_t position) noexcept; + void _redisplay(); + LayoutResult _layoutLine(std::wstring& output, const std::wstring_view& input, size_t inputOffset, til::CoordType columnBegin, til::CoordType columnLimit) const; + static void _appendCUP(std::wstring& output, til::point pos); + void _appendPopupAttr(std::wstring& output) const; void _popupPush(PopupKind kind); void _popupsDone(); @@ -170,8 +140,8 @@ class COOKED_READ_DATA final : public ReadData void _popupHandleCommandNumberInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); void _popupHandleCommandListInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); void _popupHandleInput(wchar_t wch, uint16_t vkey, DWORD keyState); - void _popupDrawPrompt(const Popup& popup, UINT id) const; - void _popupDrawCommandList(Popup& popup) const; + void _popupDrawPrompt(std::vector& lines, til::size size, UINT id, std::wstring_view suffix) const; + void _popupDrawCommandList(std::vector& lines, til::size size, Popup& popup) const; SCREEN_INFORMATION& _screenInfo; std::span _userBuffer; @@ -182,15 +152,23 @@ class COOKED_READ_DATA final : public ReadData ULONG _controlKeyState = 0; std::unique_ptr _tempHandle; - BufferState _buffer; - // _distanceCursor is the distance between the start of the prompt and the - // current cursor location in columns (including wide glyph padding columns). - ptrdiff_t _distanceCursor = 0; - // _distanceEnd is the distance between the start of the prompt and its last - // glyph at the end in columns (including wide glyph padding columns). - ptrdiff_t _distanceEnd = 0; - bool _insertMode = false; + std::wstring _buffer; + size_t _bufferDirtyBeg = npos; + size_t _bufferCursor = 0; State _state = State::Accumulating; + bool _insertMode = false; + bool _dirty = false; + + til::point _originInViewport; + // This value is in the pager coordinate space. (0,0) is the first character of the + // first line, independent on where the prompt actually appears on the screen. + til::point _pagerPromptEnd; + // The scroll position of the pager. + til::CoordType _pagerContentTop = 0; + // Contains the number of lines within the pager. + til::CoordType _pagerContentHeight = 0; std::vector _popups; + std::wstring _popupAttr; + bool _popupOpened = false; }; diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index f242f2753f0..b8b3b71e3dd 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -134,7 +134,7 @@ bool RenderData::IsCursorVisible() const noexcept { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - return cursor.IsVisible() && !cursor.IsPopupShown(); + return cursor.IsVisible(); } // Method Description: From e08ebbe570ba32c6fd5ef3bf4ac4090f321ac6ab Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 20 Jun 2024 15:13:51 +0200 Subject: [PATCH 03/21] Fix GetVirtualViewport, Fix popup alignment --- src/host/readDataCooked.cpp | 24 +++++++++--------------- src/host/screenInfo.cpp | 20 ++++++++++++-------- src/host/screenInfo.hpp | 1 + 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 2ea1a1341ed..acb337dc92d 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -110,7 +110,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, absoluteCursorPos.y = gsl::narrow_cast(cols / w); } - _screenInfo.GetVirtualViewport().ConvertToOrigin(&absoluteCursorPos); + _screenInfo.GetVirtualBufferViewport().ConvertToOrigin(&absoluteCursorPos); absoluteCursorPos.x = std::max(0, absoluteCursorPos.x); absoluteCursorPos.y = std::max(0, absoluteCursorPos.y); @@ -259,7 +259,7 @@ bool COOKED_READ_DATA::PresentingPopup() const noexcept til::point_span COOKED_READ_DATA::GetBoundaries() const noexcept { const auto viewport = _screenInfo.GetViewport(); - const auto virtualViewport = _screenInfo.GetVirtualViewport(); + const auto virtualViewport = _screenInfo.GetVirtualBufferViewport(); static constexpr til::point min; const til::point max{ viewport.RightInclusive(), viewport.BottomInclusive() }; @@ -777,7 +777,7 @@ void COOKED_READ_DATA::_redisplay() return; } - const auto size = _screenInfo.GetVirtualViewport().Dimensions(); + const auto size = _screenInfo.GetBufferSize().Dimensions(); auto originInViewportFinal = _originInViewport; til::point cursorPositionFinal; til::point pagerPromptEnd; @@ -868,16 +868,11 @@ void COOKED_READ_DATA::_redisplay() { auto& popup = _popups.back(); - // But it should not be right on the line where the prompt starts. - // That would look goofy otherwise, since there's where the text is supposed to go. - if (lines.empty()) - { - lines.emplace_back(); - } - // Ensure that the popup is not considered part of the prompt line. That is, if someone double-clicks // to select the last word in the prompt, it should not select the first word in the popup. - lines.back().text.append(L"\r\n"); + auto& lastLine = lines.back(); + lastLine.text.append(L"\r\n"); + lastLine.columns = size.width; switch (popup.kind) { @@ -913,7 +908,7 @@ void COOKED_READ_DATA::_redisplay() // a line because otherwise it won't have any space to be visible. if (_bufferCursor == _buffer.size()) { - lines.emplace_back(L" \b"); + lines.emplace_back(L" \b", 0, 0, 0); } } @@ -1383,7 +1378,7 @@ void COOKED_READ_DATA::_popupDrawPrompt(std::vector& lines, const til::siz const auto res = _layoutLine(line, str, 0, 0, size.width); line.append(L"\x1b[m"); - lines.emplace_back(std::move(line), 0, res.column); + lines.emplace_back(std::move(line), 0, 0, res.column); } void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til::size size, Popup& popup) const @@ -1467,7 +1462,6 @@ void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til if (res.offset < str.size()) { line.push_back(L'…'); - res.column++; } if (selected) @@ -1476,7 +1470,7 @@ void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til } line.append(L"\r\n"); - lines.emplace_back(std::move(line), 0, 0, res.column); + lines.emplace_back(std::move(line), 0, 0, size.width); } auto& lastLine = lines.back(); diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index daaedf87d7a..be2392c0c62 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -2451,20 +2451,24 @@ void SCREEN_INFORMATION::UpdateBottom() _virtualBottom = _viewport.BottomInclusive(); } -// Method Description: -// - Returns the "virtual" Viewport - the viewport with its bottom at -// `_virtualBottom`. For VT operations, this is essentially the mutable -// section of the buffer. -// Arguments: -// - -// Return Value: -// - the virtual terminal viewport +// Returns the section of the text buffer that would be visible on the screen +// if the user didn't scroll away vertically. It's essentially the same as +// GetVirtualBufferViewport() but includes the horizontal scroll offset. Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept { const auto newTop = _virtualBottom - _viewport.Height() + 1; return Viewport::FromDimensions({ _viewport.Left(), newTop }, _viewport.Dimensions()); } +// Returns the section of the text buffer that's addressable by VT sequences. +Viewport SCREEN_INFORMATION::GetVirtualBufferViewport() const noexcept +{ + const auto viewportHeight = _viewport.Height(); + const auto bufferWidth = _textBuffer->GetSize().Width(); + const auto top = std::max(0, _virtualBottom - viewportHeight + 1); + return Viewport::FromExclusive({ 0, top, bufferWidth, top + viewportHeight }); +} + // Method Description: // - Returns true if the character at the cursor's current position is wide. // See IsGlyphFullWidth diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 035529e0d6e..6a54b3153cb 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -115,6 +115,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console const Microsoft::Console::Types::Viewport& GetViewport() const noexcept; void SetViewport(const Microsoft::Console::Types::Viewport& newViewport, const bool updateBottom); Microsoft::Console::Types::Viewport GetVirtualViewport() const noexcept; + Microsoft::Console::Types::Viewport GetVirtualBufferViewport() const noexcept; void ProcessResizeWindow(const til::rect* const prcClientNew, const til::rect* const prcClientOld); void SetViewportSize(const til::size* const pcoordSize); From 88ef5f4d805eceeffe85c5301cf575cf628ed6cf Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 20 Jun 2024 18:11:38 +0200 Subject: [PATCH 04/21] Address feedback --- src/host/readDataCooked.cpp | 4 ++-- src/host/screenInfo.cpp | 4 ++-- src/host/screenInfo.hpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index acb337dc92d..1b86de4d43d 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -110,7 +110,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, absoluteCursorPos.y = gsl::narrow_cast(cols / w); } - _screenInfo.GetVirtualBufferViewport().ConvertToOrigin(&absoluteCursorPos); + _screenInfo.GetVtPageArea().ConvertToOrigin(&absoluteCursorPos); absoluteCursorPos.x = std::max(0, absoluteCursorPos.x); absoluteCursorPos.y = std::max(0, absoluteCursorPos.y); @@ -259,7 +259,7 @@ bool COOKED_READ_DATA::PresentingPopup() const noexcept til::point_span COOKED_READ_DATA::GetBoundaries() const noexcept { const auto viewport = _screenInfo.GetViewport(); - const auto virtualViewport = _screenInfo.GetVirtualBufferViewport(); + const auto virtualViewport = _screenInfo.GetVtPageArea(); static constexpr til::point min; const til::point max{ viewport.RightInclusive(), viewport.BottomInclusive() }; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index be2392c0c62..722020bc54e 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -2453,7 +2453,7 @@ void SCREEN_INFORMATION::UpdateBottom() // Returns the section of the text buffer that would be visible on the screen // if the user didn't scroll away vertically. It's essentially the same as -// GetVirtualBufferViewport() but includes the horizontal scroll offset. +// GetVtPageArea() but includes the horizontal scroll offset and window width. Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept { const auto newTop = _virtualBottom - _viewport.Height() + 1; @@ -2461,7 +2461,7 @@ Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept } // Returns the section of the text buffer that's addressable by VT sequences. -Viewport SCREEN_INFORMATION::GetVirtualBufferViewport() const noexcept +Viewport SCREEN_INFORMATION::GetVtPageArea() const noexcept { const auto viewportHeight = _viewport.Height(); const auto bufferWidth = _textBuffer->GetSize().Width(); diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 6a54b3153cb..bf2d5e29fd6 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -115,7 +115,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console const Microsoft::Console::Types::Viewport& GetViewport() const noexcept; void SetViewport(const Microsoft::Console::Types::Viewport& newViewport, const bool updateBottom); Microsoft::Console::Types::Viewport GetVirtualViewport() const noexcept; - Microsoft::Console::Types::Viewport GetVirtualBufferViewport() const noexcept; + Microsoft::Console::Types::Viewport GetVtPageArea() const noexcept; void ProcessResizeWindow(const til::rect* const prcClientNew, const til::rect* const prcClientOld); void SetViewportSize(const til::size* const pcoordSize); From 413991e5902ab319de2728ba7157c92905bbfd18 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 21 Jun 2024 02:11:06 +0200 Subject: [PATCH 05/21] Address feedback, Fix scrolling bug --- doc/COOKED_READ_DATA.md | 19 +++++++------------ src/host/VtIo.cpp | 2 +- src/host/readDataCooked.cpp | 7 ++++--- src/host/readDataCooked.hpp | 1 - 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/doc/COOKED_READ_DATA.md b/doc/COOKED_READ_DATA.md index f4b463f7aa6..eed82ec8f89 100644 --- a/doc/COOKED_READ_DATA.md +++ b/doc/COOKED_READ_DATA.md @@ -4,12 +4,8 @@ All of the following ✅ marks must be fulfilled during manual testing: * ASCII input -* Chinese input (中文維基百科) ❔ - * Resizing the window properly wraps/unwraps wide glyphs ❌ - Broken due to `TextBuffer::Reflow` bugs -* Surrogate pair input (🙂) ❔ - * Resizing the window properly wraps/unwraps surrogate pairs ❌ - Broken due to `TextBuffer::Reflow` bugs +* Chinese input (中文維基百科) ✅ +* Surrogate pair input (🙂) ✅ * In cmd.exe * Create 2 file: "a😊b.txt" and "a😟b.txt" * Press tab: Autocomplete to "a😊b.txt" ✅ @@ -62,21 +58,20 @@ All of the following ✅ marks must be fulfilled during manual testing: * F6 inserts Ctrl+Z ✅ * F7 without modifiers starts "command list" prompt ✅ * Escape dismisses prompt ✅ - * Minimum size of 40x10 characters ✅ - * Width expands to fit the widest history command ✅ + * Entries wider than the window width are truncated ✅ * Height expands up to 20 rows with longer histories ✅ * F9 starts "command number" prompt ✅ - * Left/Right paste replace the buffer with the given command ✅ + * Left/Right replace the buffer with the given command ✅ * And put cursor at the end of the buffer ✅ * Up/Down navigate selection through history ✅ * Stops at start/end with <10 entries ✅ * Stops at start/end with >20 entries ✅ - * Wide text rendering during pagination with >20 entries ✅ + * Scrolls through the entries if there are too many ✅ * Shift+Up/Down moves history items around ✅ * Home navigates to first entry ✅ * End navigates to last entry ✅ - * PageUp navigates by 20 items at a time or to first ✅ - * PageDown navigates by 20 items at a time or to last ✅ + * PageUp navigates by $height items at a time or to first ✅ + * PageDown navigates by $height items at a time or to last ✅ * Alt+F7 clears command history ✅ * F8 cycles through commands that start with the same text as the current buffer up until the current cursor position ✅ diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index 9a1601cb636..c4381087a0e 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -511,7 +511,7 @@ static size_t formatAttributes(char (&buffer)[16], WORD attributes) noexcept { // The Console API represents colors in BGR order, but VT represents them in RGB order. // This LUT transposes them. This is for foreground colors. Add +10 to get the background ones. - static const uint8_t lut[] = { 30, 34, 32, 36, 31, 35, 33, 37, 90, 94, 92, 96, 91, 95, 93, 97 }; + static constexpr uint8_t lut[] = { 30, 34, 32, 36, 31, 35, 33, 37, 90, 94, 92, 96, 91, 95, 93, 97 }; fg = lut[attributes & 0xf]; bg = lut[(attributes >> 4) & 0xf] + 10; } diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 1b86de4d43d..21222697883 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -777,7 +777,7 @@ void COOKED_READ_DATA::_redisplay() return; } - const auto size = _screenInfo.GetBufferSize().Dimensions(); + const auto size = _screenInfo.GetVtPageArea().Dimensions(); auto originInViewportFinal = _originInViewport; til::point cursorPositionFinal; til::point pagerPromptEnd; @@ -959,8 +959,9 @@ void COOKED_READ_DATA::_redisplay() _popupOpened = popupOpened; } - // Scroll the contents of the pager if needed, so we only need to write what actually changed. - if (const auto delta = pagerContentTop - _pagerContentTop; delta != 0) + // If we have so much text that it doesn't fit into the viewport (origin == {0,0}), + // then we can scroll the existing contents of the pager and only write what got newly uncovered. + if (const auto delta = pagerContentTop - _pagerContentTop; delta != 0 && _originInViewport == til::point{}) { const auto deltaAbs = abs(delta); til::CoordType beg = 0; diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index 809797892a3..c85b4932fae 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -169,6 +169,5 @@ class COOKED_READ_DATA final : public ReadData til::CoordType _pagerContentHeight = 0; std::vector _popups; - std::wstring _popupAttr; bool _popupOpened = false; }; From 32a12496a93de9ed24bf60f334eb7d1d9a6e9764 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 25 Jun 2024 14:41:41 +0200 Subject: [PATCH 06/21] Fix several issues with control character visualizers --- src/host/readDataCooked.cpp | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 21222697883..f985039d312 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -738,7 +738,7 @@ void COOKED_READ_DATA::_replace(size_t offset, size_t remove, const wchar_t* inp if (offset <= _bufferDirtyBeg) { const auto& textBuffer = _screenInfo.GetTextBuffer(); - _bufferDirtyBeg = textBuffer.GraphemePrev(_buffer, offset); + _bufferDirtyBeg = textBuffer.GraphemePrev(_buffer, _bufferCursor); } } @@ -843,14 +843,18 @@ void COOKED_READ_DATA::_redisplay() pagerPromptEnd = { res.column, gsl::narrow_cast(lines.size() - 1) }; // If the content got a little shorter than it was before, we need to erase the tail end. + // If the last character on a line got removed, we'll skip this code because `remaining` + // will be negative, and instead we'll erase it later when we append " \r" to the lines. // If entire lines got removed, then we'll fix this later when comparing against _pagerContentEnd.y. if (pagerPromptEnd.y <= _pagerPromptEnd.y) { - auto& line = lines.back(); const auto endX = _pagerPromptEnd.y == pagerPromptEnd.y ? _pagerPromptEnd.x : size.width; - const auto remaining = endX - line.columns; + const auto remaining = endX - pagerPromptEnd.x; + if (remaining > 0) { + auto& line = lines.back(); + // CSI K may be expensive, so use spaces if we can. if (remaining <= 8) { @@ -904,11 +908,15 @@ void COOKED_READ_DATA::_redisplay() cursorPositionFinal.x = 0; cursorPositionFinal.y++; - // If the cursor is at the end of the buffer however, we have to insert - // a line because otherwise it won't have any space to be visible. + // If the cursor is at the end of the buffer we must always show it after the last character. + // Since VT uses delayed EOL wrapping, we must write at least 1 more character to force the + // potential delayed line wrap at the end of the prompt, on the last line. + // This doubles as the code that erases the last character on the last line when backspacing. + // That's also why we append 2 spaces, because the last character may have been a ^E control + // character visualizer, which sneakily actually consists of 2 characters. if (_bufferCursor == _buffer.size()) { - lines.emplace_back(L" \b", 0, 0, 0); + lines.emplace_back(L" \r", 0, 0, 0); } } @@ -1100,7 +1108,7 @@ COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& outpu } } - const auto wch = *it++; + const auto wch = *it; wchar_t buf[8]; til::CoordType len = 0; @@ -1117,11 +1125,14 @@ COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& outpu len = 2; } - if (column + len <= columnLimit) + if (column + len > columnLimit) { - column += len; - output.append(buf, len); + break; } + + output.append(buf, len); + column += len; + it++; } const size_t offset = it - beg; From 4471e983d9e629f765054b82c0f41b8be2b42f17 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 25 Jun 2024 17:55:57 +0200 Subject: [PATCH 07/21] Fix ambiguous chars, Improve F7+F9 UX --- src/host/readDataCooked.cpp | 63 ++++++++++++++++++++++++------------- src/host/readDataCooked.hpp | 6 ++-- src/host/utils.cpp | 4 +-- src/host/utils.hpp | 2 +- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index f985039d312..4a448b07224 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -236,8 +236,14 @@ void COOKED_READ_DATA::EraseBeforeResize() const // The counter-part to EraseBeforeResize(). void COOKED_READ_DATA::RedrawAfterResize() { + // Ensure that we don't use any scroll sequences or try to clear previous pager contents. + // They have all been erased with the CSI J above. + _pagerHeight = 0; + + // Ensure that the entire buffer content is rewritten after the above CSI J. _bufferDirtyBeg = 0; _dirty = true; + _redisplay(); } @@ -870,7 +876,7 @@ void COOKED_READ_DATA::_redisplay() // Render the popups, if there are any. if (!_popups.empty()) { - auto& popup = _popups.back(); + auto& popup = _popups.front(); // Ensure that the popup is not considered part of the prompt line. That is, if someone double-clicks // to select the last word in the prompt, it should not select the first word in the popup. @@ -881,13 +887,13 @@ void COOKED_READ_DATA::_redisplay() switch (popup.kind) { case PopupKind::CopyToChar: - _popupDrawPrompt(lines, size, ID_CONSOLE_MSGCMDLINEF2, {}); + _popupDrawPrompt(lines, size.width, ID_CONSOLE_MSGCMDLINEF2, {}, {}); break; case PopupKind::CopyFromChar: - _popupDrawPrompt(lines, size, ID_CONSOLE_MSGCMDLINEF4, {}); + _popupDrawPrompt(lines, size.width, ID_CONSOLE_MSGCMDLINEF4, {}, {}); break; case PopupKind::CommandNumber: - _popupDrawPrompt(lines, size, ID_CONSOLE_MSGCMDLINEF9, { popup.commandNumber.buffer.data(), CommandNumberMaxInputLength }); + _popupDrawPrompt(lines, size.width, ID_CONSOLE_MSGCMDLINEF9, {}, { popup.commandNumber.buffer.data(), CommandNumberMaxInputLength }); break; case PopupKind::CommandList: _popupDrawCommandList(lines, size, popup); @@ -969,7 +975,7 @@ void COOKED_READ_DATA::_redisplay() // If we have so much text that it doesn't fit into the viewport (origin == {0,0}), // then we can scroll the existing contents of the pager and only write what got newly uncovered. - if (const auto delta = pagerContentTop - _pagerContentTop; delta != 0 && _originInViewport == til::point{}) + if (const auto delta = pagerContentTop - _pagerContentTop; delta != 0 && _pagerHeight == size.height && pagerHeight == size.height) { const auto deltaAbs = abs(delta); til::CoordType beg = 0; @@ -1053,7 +1059,7 @@ void COOKED_READ_DATA::_redisplay() // Clear any lines that we previously filled and are now empty. { - const auto pagerHeightPrevious = std::min(_pagerContentHeight, size.height); + const auto pagerHeightPrevious = std::min(_pagerHeight, size.height); if (pagerHeight < pagerHeightPrevious) { @@ -1074,7 +1080,7 @@ void COOKED_READ_DATA::_redisplay() _originInViewport = originInViewportFinal; _pagerPromptEnd = pagerPromptEnd; _pagerContentTop = pagerContentTop; - _pagerContentHeight = lineCount; + _pagerHeight = pagerHeight; _bufferDirtyBeg = _buffer.size(); _dirty = false; } @@ -1379,15 +1385,17 @@ void COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t _dirty = true; } -void COOKED_READ_DATA::_popupDrawPrompt(std::vector& lines, const til::size size, const UINT id, const std::wstring_view suffix) const +void COOKED_READ_DATA::_popupDrawPrompt(std::vector& lines, const til::CoordType width, const UINT id, const std::wstring_view& prefix, const std::wstring_view& suffix) const { - auto str = _LoadString(id); + std::wstring str; + str.append(prefix); + _LoadString(id, str); str.append(suffix); std::wstring line; line.append(L"\x1b[K"); _appendPopupAttr(line); - const auto res = _layoutLine(line, str, 0, 0, size.width); + const auto res = _layoutLine(line, str, 0, 0, width); line.append(L"\x1b[m"); lines.emplace_back(std::move(line), 0, 0, res.column); @@ -1402,8 +1410,13 @@ void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til const auto indexWidth = gsl::narrow_cast(fmt::formatted_size(FMT_COMPILE(L"{}"), historySize)); // The popup is half the height of the viewport, but at least 1 and at most 20 lines. - // Unless of course the history size is less than that. - const auto height = std::min(historySize, std::clamp(size.height / 2, 1, 20)); + // Unless of course the history size is less than that. We also reserve 1 additional line + // of space in case the user presses F9 which will open the "Enter command number:" popup. + const auto height = std::min(historySize, std::min(size.height / 2 - 1, 20)); + if (height < 1) + { + return; + } // cl.selected may be out of bounds after a page up/down, etc., so we need to clamp it. cl.selected = std::clamp(cl.selected, 0, historySize - 1); @@ -1430,12 +1443,13 @@ void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til const auto historyMax = historySize - 1; const auto trackPositionMax = height - 3; const auto trackPosition = historyMax <= 0 ? 0 : 1 + (trackPositionMax * cl.selected + historyMax / 2) / historyMax; + const auto stackedCommandNumberPopup = _popups.size() == 2 && _popups.back().kind == PopupKind::CommandNumber; for (til::CoordType off = 0; off < height; ++off) { const auto index = cl.top + off; const auto str = _history->GetNth(index); - const auto selected = index == cl.selected; + const auto selected = index == cl.selected && !stackedCommandNumberPopup; std::wstring line; line.append(L"\x1b[K"); @@ -1446,11 +1460,11 @@ void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til { if (off == 0) { - scrollbarChar = L'▲'; + scrollbarChar = L'▴'; } else if (off == height - 1) { - scrollbarChar = L'▼'; + scrollbarChar = L'▾'; } else { @@ -1470,11 +1484,7 @@ void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til fmt::format_to(std::back_inserter(line), FMT_COMPILE(L"{:{}}: "), index, indexWidth); - auto res = _layoutLine(line, str, 0, indexWidth + 4, size.width - 1); - if (res.offset < str.size()) - { - line.push_back(L'…'); - } + _layoutLine(line, str, 0, indexWidth + 4, size.width); if (selected) { @@ -1485,6 +1495,15 @@ void COOKED_READ_DATA::_popupDrawCommandList(std::vector& lines, const til lines.emplace_back(std::move(line), 0, 0, size.width); } - auto& lastLine = lines.back(); - lastLine.text.erase(lastLine.text.size() - 2); + if (stackedCommandNumberPopup) + { + const std::wstring_view suffix{ _popups.back().commandNumber.buffer.data(), CommandNumberMaxInputLength }; + _popupDrawPrompt(lines, size.width - 1, ID_CONSOLE_MSGCMDLINEF9, L"╰", suffix); + } + else + { + // Remove the \r\n we added to the last line, as we don't want to have an empty line at the end. + auto& lastLine = lines.back(); + lastLine.text.erase(lastLine.text.size() - 2); + } } diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index c85b4932fae..c80d42a55ef 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -140,7 +140,7 @@ class COOKED_READ_DATA final : public ReadData void _popupHandleCommandNumberInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); void _popupHandleCommandListInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); void _popupHandleInput(wchar_t wch, uint16_t vkey, DWORD keyState); - void _popupDrawPrompt(std::vector& lines, til::size size, UINT id, std::wstring_view suffix) const; + void _popupDrawPrompt(std::vector& lines, const til::CoordType width, UINT id, const std::wstring_view& prefix, const std::wstring_view& suffix) const; void _popupDrawCommandList(std::vector& lines, til::size size, Popup& popup) const; SCREEN_INFORMATION& _screenInfo; @@ -165,8 +165,8 @@ class COOKED_READ_DATA final : public ReadData til::point _pagerPromptEnd; // The scroll position of the pager. til::CoordType _pagerContentTop = 0; - // Contains the number of lines within the pager. - til::CoordType _pagerContentHeight = 0; + // Contains the viewport height for which it previously was drawn for. + til::CoordType _pagerHeight = 0; std::vector _popups; bool _popupOpened = false; diff --git a/src/host/utils.cpp b/src/host/utils.cpp index 74d08845412..0aeece0ef4f 100644 --- a/src/host/utils.cpp +++ b/src/host/utils.cpp @@ -61,7 +61,7 @@ WORD ConvertStringToDec(_In_ PCWSTR pwchToConvert, _Out_opt_ PCWSTR* const ppwch // - id - Resource id from resource.h to the string we need to load. // Return Value: // - The string resource -std::wstring _LoadString(const UINT id) +void _LoadString(const UINT id, std::wstring& out) { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); WCHAR ItemString[70]; @@ -78,7 +78,7 @@ std::wstring _LoadString(const UINT id) ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, id, ItemString, ARRAYSIZE(ItemString)); } - return std::wstring(ItemString, ItemLength); + out.append(ItemString, ItemLength); } // Routine Description: diff --git a/src/host/utils.hpp b/src/host/utils.hpp index e0b6f62e916..6692a3e8ac0 100644 --- a/src/host/utils.hpp +++ b/src/host/utils.hpp @@ -27,7 +27,7 @@ til::CoordType CalcWindowSizeY(const til::inclusive_rect& rect) noexcept; til::CoordType CalcCursorYOffsetInPixels(const til::CoordType sFontSizeY, const ULONG ulSize) noexcept; WORD ConvertStringToDec(_In_ PCWSTR pwchToConvert, _Out_opt_ PCWSTR* const ppwchEnd) noexcept; -std::wstring _LoadString(const UINT id); +void _LoadString(const UINT id, std::wstring& out); static UINT s_LoadStringEx(_In_ HINSTANCE hModule, _In_ UINT wID, _Out_writes_(cchBufferMax) LPWSTR lpBuffer, From 8f39b49d12015841a1ff5e645b39864b83964c8a Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 25 Jun 2024 23:38:50 +0200 Subject: [PATCH 08/21] Overkill initialData impl, but hopefully not kill --- src/host/readDataCooked.cpp | 168 +++++++++++++++++++++++++----------- 1 file changed, 120 insertions(+), 48 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 4a448b07224..6905ddd9f7a 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -63,12 +63,16 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, const auto& textBuffer = _screenInfo.GetTextBuffer(); const auto& cursor = textBuffer.GetCursor(); - auto absoluteCursorPos = cursor.GetPosition(); + auto cursorPos = cursor.GetPosition(); + + _screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos); + cursorPos.x = std::max(0, cursorPos.x); + cursorPos.y = std::max(0, cursorPos.y); + + _originInViewport = cursorPos; if (!initialData.empty()) { - _replace(initialData); - // The console API around `nInitialChars` in `CONSOLE_READCONSOLE_CONTROL` is pretty weird. // The way it works is that cmd.exe does a ReadConsole() with a `dwCtrlWakeupMask` that includes \t, // so when you press tab it can autocomplete the prompt based on the available file names. @@ -80,7 +84,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, // characters like Ctrl+X as "^X" and WriteConsoleW() doesn't and so the column counts don't match. // Solving these issues is technically possible, but it's also quite difficult to do so correctly. // - // But unfortunately (or fortunately) the initial (from the 1990s up to 2023) looked something like that: + // But unfortunately (or fortunately) the initial implementation (from the 1990s up to 2023) looked something like that: // cursor = cursor.GetPosition(); // cursor.x -= initialData.size(); // while (cursor.x < 0) @@ -91,30 +95,90 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, // // In other words, it assumed that the number of code units in the initial data corresponds 1:1 to // the column count. This meant that the API never supported tabs for instance (nor wide glyphs). - // The new implementation still doesn't support tabs, but it does fix support for wide glyphs. - // That seemed like a good trade-off. + // + // + // The new implementation is a lot more complex to be a little more correct. + // It replicates part of the _redisplay() logic to layout the text at various + // starting positions until it finds one that matches the current cursor position. + + const auto size = _screenInfo.GetVtPageArea().Dimensions(); - // NOTE: You can't just "measure" the length of the string in columns either, because previously written - // wide glyphs might have resulted in padding whitespace in the text buffer (see ROW::WasDoubleBytePadded). - til::CoordType columns = 0; - textBuffer.FitTextIntoColumns(initialData, til::CoordTypeMax, columns); + // Guess the initial cursor position based on the string length, assuming that 1 char = 1 column. + const auto columnRemainder = gsl::narrow_cast((initialData.size() % size.width)); + const auto bestGuessColumn = (cursorPos.x - columnRemainder + size.width) % size.width; - const int64_t w = textBuffer.GetSize().Width(); - const int64_t x = absoluteCursorPos.x; - const int64_t y = absoluteCursorPos.y; + std::wstring line; + LayoutResult res; + til::CoordType bestDistance = til::CoordTypeMax; + til::CoordType bestColumnBegin = 0; + til::CoordType bestNewlineCount = 0; - auto cols = y * w + x - columns; - cols = std::max(0, cols); + line.reserve(size.width); - absoluteCursorPos.x = gsl::narrow_cast(cols % w); - absoluteCursorPos.y = gsl::narrow_cast(cols / w); - } + // Do a brute force search for the best starting position that ends at the current cursor position. + // The search is centered around `bestGuessColumn` with offsets 0, 1, -1, 2, -2, 3, -3, ... + for (til::CoordType i = 0, attempts = 2 * size.width; i <= attempts; i++) + { + // Hilarious bit-trickery that no one can read. But it works. Check it out in a debugger. + // The idea is to use bits 1:31 as the value and bit 0 as a trigger to bit-flip the value. + // A bit-flipped positive number is negative, but offset by 1, so we add 1. Fun! + const auto offset = ((i / 2) ^ ((i & 1) - 1)) + 1; + const auto columnBegin = bestGuessColumn + offset; + + if (columnBegin < 0 || columnBegin >= size.width) + { + continue; + } + + til::CoordType newlineCount = 0; + res.column = columnBegin; + + for (size_t beg = 0; beg < initialData.size();) + { + line.clear(); + res = _layoutLine(line, initialData, beg, res.column, size.width); + beg = res.offset; + + if (res.column >= size.width) + { + res.column = 0; + newlineCount += 1; + } + } + + const auto distance = abs(res.column - cursorPos.x); + if (distance < bestDistance) + { + bestDistance = distance; + bestColumnBegin = columnBegin; + bestNewlineCount = newlineCount; + } + if (distance == 0) + { + break; + } + } - _screenInfo.GetVtPageArea().ConvertToOrigin(&absoluteCursorPos); - absoluteCursorPos.x = std::max(0, absoluteCursorPos.x); - absoluteCursorPos.y = std::max(0, absoluteCursorPos.y); + auto originInViewport = cursorPos; + originInViewport.x = bestColumnBegin; + originInViewport.y = originInViewport.y - bestNewlineCount; - _originInViewport = absoluteCursorPos; + if (originInViewport.y < 0) + { + originInViewport = {}; + } + + // We can't mark the buffer as dirty because this messes up the cursor position for cmd + // somehow when the prompt is longer than the viewport height. I haven't investigated + // why that happens, but it works decently well enough that it's not too important. + _buffer.assign(initialData); + _bufferDirtyBeg = _buffer.size(); + _bufferCursor = _buffer.size(); + + _originInViewport = originInViewport; + _pagerPromptEnd = cursorPos; + _pagerHeight = std::min(size.height, bestNewlineCount + 1); + } } // Routine Description: @@ -384,7 +448,6 @@ void COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) // It is important that we don't actually print that character out though, as it's only for the calling application to see. // That's why we flush the contents before the insertion and then ensure that the _flushBuffer() call in Read() exits early. _replace(_bufferCursor, npos, nullptr, 0); - _dirty = true; _redisplay(); _replace(_bufferCursor, 0, &wch, 1); _dirty = false; @@ -737,6 +800,12 @@ void COOKED_READ_DATA::_replace(size_t offset, size_t remove, const wchar_t* inp offset = std::min(offset, size); remove = std::min(remove, size - offset); + // Nothing to do. Avoid marking it as dirty. + if (remove == 0 && count == 0) + { + return; + } + _buffer.replace(offset, remove, input, count); _bufferCursor = offset + count; _dirty = true; @@ -1095,7 +1164,7 @@ COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& outpu output.reserve(output.size() + columnLimit - column); - while (it != end) + while (it != end && column < columnLimit) { const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return wch < L' '; }); if (it != nextControlChar) @@ -1107,38 +1176,41 @@ COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& outpu output.append(text, 0, len); column += cols; it += len; + } - if (len < text.size() || it == end) + const auto nextPlainChar = std::find_if(it, end, [](const auto& wch) { return wch >= L' '; }); + for (; it != nextPlainChar; ++it) + { + if (column >= columnLimit) { break; } - } - const auto wch = *it; - wchar_t buf[8]; - til::CoordType len = 0; + const auto wch = *it; + wchar_t buf[8]; + til::CoordType len = 0; - if (wch == UNICODE_TAB) - { - const auto remaining = columnLimit - column; - len = std::min(8 - (column & 7), remaining); - std::fill_n(&buf[0], len, L' '); - } - else - { - buf[0] = L'^'; - buf[1] = wch + L'@'; - len = 2; - } + if (wch == UNICODE_TAB) + { + const auto remaining = columnLimit - column; + len = std::min(8 - (column & 7), remaining); + std::fill_n(&buf[0], len, L' '); + } + else + { + buf[0] = L'^'; + buf[1] = wch + L'@'; + len = 2; + } - if (column + len > columnLimit) - { - break; - } + if (column + len > columnLimit) + { + break; + } - output.append(buf, len); - column += len; - it++; + output.append(buf, len); + column += len; + } } const size_t offset = it - beg; From b6e404613a31bdb79f1300bab4129838d214ff3f Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 26 Jun 2024 01:11:49 +0200 Subject: [PATCH 09/21] Fix break not breaking, Fix column not columning --- src/host/readDataCooked.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 6905ddd9f7a..bf1367126d5 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -1183,7 +1183,7 @@ COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& outpu { if (column >= columnLimit) { - break; + goto outerLoopExit; } const auto wch = *it; @@ -1205,7 +1205,7 @@ COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& outpu if (column + len > columnLimit) { - break; + goto outerLoopExit; } output.append(buf, len); @@ -1213,16 +1213,15 @@ COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& outpu } } - const size_t offset = it - beg; - - if (offset < input.size() && column < columnLimit) +outerLoopExit: + if (it != end && column < columnLimit) { - column = columnLimit; output.append(columnLimit - column, L' '); + column = columnLimit; } return { - .offset = offset, + .offset = size_t(it - beg), .column = column, }; } From 671f371b18907c1c37a45ada412768a4de1dcbf3 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 26 Jun 2024 01:12:21 +0200 Subject: [PATCH 10/21] C++ cast --- src/host/readDataCooked.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index bf1367126d5..6aa733b5138 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -1221,7 +1221,7 @@ COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& outpu } return { - .offset = size_t(it - beg), + .offset = static_cast(it - beg), .column = column, }; } From e5b8f5be71697983cf890feaafad0cdb4e53dcd8 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 1 Jul 2024 17:37:39 +0200 Subject: [PATCH 11/21] Fix cursor position after reflow --- src/host/readDataCooked.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 6aa733b5138..3e715659645 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -300,6 +300,15 @@ void COOKED_READ_DATA::EraseBeforeResize() const // The counter-part to EraseBeforeResize(). void COOKED_READ_DATA::RedrawAfterResize() { + // Get the new cursor position after the reflow. Just like how the COOKED_READ_DATA constructor did it. + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + auto absoluteCursorPos = cursor.GetPosition(); + _screenInfo.GetVtPageArea().ConvertToOrigin(&absoluteCursorPos); + absoluteCursorPos.x = std::max(0, absoluteCursorPos.x); + absoluteCursorPos.y = std::max(0, absoluteCursorPos.y); + _originInViewport = absoluteCursorPos; + // Ensure that we don't use any scroll sequences or try to clear previous pager contents. // They have all been erased with the CSI J above. _pagerHeight = 0; From 567260e4f78bfbf3cd3cba81fdca99030f2286f1 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 1 Jul 2024 18:00:42 +0200 Subject: [PATCH 12/21] Fix prompt boundaries after wrapping --- src/host/readDataCooked.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 3e715659645..52666014a7c 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -303,11 +303,11 @@ void COOKED_READ_DATA::RedrawAfterResize() // Get the new cursor position after the reflow. Just like how the COOKED_READ_DATA constructor did it. const auto& textBuffer = _screenInfo.GetTextBuffer(); const auto& cursor = textBuffer.GetCursor(); - auto absoluteCursorPos = cursor.GetPosition(); - _screenInfo.GetVtPageArea().ConvertToOrigin(&absoluteCursorPos); - absoluteCursorPos.x = std::max(0, absoluteCursorPos.x); - absoluteCursorPos.y = std::max(0, absoluteCursorPos.y); - _originInViewport = absoluteCursorPos; + auto cursorPos = cursor.GetPosition(); + _screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos); + cursorPos.x = std::max(0, cursorPos.x); + cursorPos.y = std::max(0, cursorPos.y); + _originInViewport = cursorPos; // Ensure that we don't use any scroll sequences or try to clear previous pager contents. // They have all been erased with the CSI J above. @@ -925,6 +925,11 @@ void COOKED_READ_DATA::_redisplay() } pagerPromptEnd = { res.column, gsl::narrow_cast(lines.size() - 1) }; + if (pagerPromptEnd.x >= size.width) + { + pagerPromptEnd.x = 0; + pagerPromptEnd.y++; + } // If the content got a little shorter than it was before, we need to erase the tail end. // If the last character on a line got removed, we'll skip this code because `remaining` From 4cda9753429f37ccf9d4b2383d48c2883056dbeb Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 1 Jul 2024 18:02:00 +0200 Subject: [PATCH 13/21] Fix _viewport size after shrinking --- src/host/screenInfo.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 17eba07afae..a2e68566bd1 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1413,6 +1413,7 @@ try newTextBuffer->SetCurrentAttributes(_textBuffer->GetCurrentAttributes()); _textBuffer = std::move(newTextBuffer); + _viewport = _textBuffer->GetSize().Clamp(_viewport); return STATUS_SUCCESS; } NT_CATCH_RETURN() @@ -1430,6 +1431,7 @@ try _textBuffer->GetCursor().StartDeferDrawing(); auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); _textBuffer->ResizeTraditional(coordNewScreenSize); + _viewport = _textBuffer->GetSize().Clamp(_viewport); return STATUS_SUCCESS; } NT_CATCH_RETURN() From 398d7953cbb7bf4473e234f125e355e637d31432 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 2 Jul 2024 17:25:19 +0200 Subject: [PATCH 14/21] More robust bodgy reflow hacks --- src/host/readDataCooked.cpp | 18 ++++++++++++++++-- src/host/readDataCooked.hpp | 3 ++- src/host/screenInfo.cpp | 38 +++++++++++-------------------------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 52666014a7c..f44894a03c5 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -289,8 +289,15 @@ bool COOKED_READ_DATA::Read(const bool isUnicode, size_t& numBytes, ULONG& contr // Printing wide glyphs at the end of a row results in a forced line wrap and a padding whitespace to be inserted. // When the text buffer resizes these padding spaces may vanish and the _distanceCursor and _distanceEnd measurements become inaccurate. // To fix this, this function is called before a resize and will clear the input line. Afterwards, RedrawAfterResize() will restore it. -void COOKED_READ_DATA::EraseBeforeResize() const +void COOKED_READ_DATA::EraseBeforeResize() { + if (_redrawPending) + { + return; + } + + _redrawPending = true; + std::wstring output; _appendCUP(output, _originInViewport); output.append(L"\x1b[J"); @@ -300,6 +307,13 @@ void COOKED_READ_DATA::EraseBeforeResize() const // The counter-part to EraseBeforeResize(). void COOKED_READ_DATA::RedrawAfterResize() { + if (!_redrawPending) + { + return; + } + + _redrawPending = false; + // Get the new cursor position after the reflow. Just like how the COOKED_READ_DATA constructor did it. const auto& textBuffer = _screenInfo.GetTextBuffer(); const auto& cursor = textBuffer.GetCursor(); @@ -315,7 +329,7 @@ void COOKED_READ_DATA::RedrawAfterResize() // Ensure that the entire buffer content is rewritten after the above CSI J. _bufferDirtyBeg = 0; - _dirty = true; + _dirty = !_buffer.empty(); _redisplay(); } diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index c80d42a55ef..27fee91dd8c 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -30,7 +30,7 @@ class COOKED_READ_DATA final : public ReadData bool Read(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); - void EraseBeforeResize() const; + void EraseBeforeResize(); void RedrawAfterResize(); void SetInsertMode(bool insertMode) noexcept; @@ -158,6 +158,7 @@ class COOKED_READ_DATA final : public ReadData State _state = State::Accumulating; bool _insertMode = false; bool _dirty = false; + bool _redrawPending = false; til::point _originInViewport; // This value is in the pager coordinate space. (0,0) is the first character of the diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index a2e68566bd1..2d9b5229ad9 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1037,6 +1037,11 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS const auto DeltaY = pcoordSize->height - _viewport.Height(); const auto coordScreenBufferSize = GetBufferSize().Dimensions(); + if (DeltaX == 0 && DeltaY == 0) + { + return; + } + // do adjustments on a copy that's easily manipulated. auto srNewViewport = _viewport.ToInclusive(); @@ -1120,15 +1125,11 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS // If the new bottom is supposed to be before the final line of the buffer // Check to ensure that we don't hide the prompt by collapsing the window. - // The final valid end position will be the coordinates of - // the last character displayed (including any characters - // in the input line) - til::point coordValidEnd; - Selection::Instance().GetValidAreaBoundaries(nullptr, &coordValidEnd); + const auto coordValidEnd = _textBuffer->GetCursor().GetPosition(); // If the bottom of the window when adjusted would be // above the final line of valid text... - if (srNewViewport.bottom + DeltaY < coordValidEnd.y) + if (sBottomProposed < coordValidEnd.y) { // Adjust the top of the window instead of the bottom // (so the lines slide upward) @@ -1195,19 +1196,11 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS _viewport = newViewport; Tracing::s_TraceWindowViewport(_viewport); - // In Conpty mode, call TriggerScroll here without params. By not providing - // params, the renderer will make sure to update the VtEngine with the - // updated viewport size. If we don't do this, the engine can get into a - // torn state on this frame. - // - // Without this statement, the engine won't be told about the new view size - // till the start of the next frame. If any other text gets output before - // that frame starts, there's a very real chance that it'll cause errors as - // the engine tries to invalidate those regions. - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (gci.IsInVtIoMode() && ServiceLocator::LocateGlobals().pRender) + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.HasPendingCookedRead()) { - ServiceLocator::LocateGlobals().pRender->TriggerScroll(); + gci.CookedReadData().RedrawAfterResize(); + MakeCurrentCursorVisible(); } } @@ -1413,7 +1406,6 @@ try newTextBuffer->SetCurrentAttributes(_textBuffer->GetCurrentAttributes()); _textBuffer = std::move(newTextBuffer); - _viewport = _textBuffer->GetSize().Clamp(_viewport); return STATUS_SUCCESS; } NT_CATCH_RETURN() @@ -1431,7 +1423,6 @@ try _textBuffer->GetCursor().StartDeferDrawing(); auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); _textBuffer->ResizeTraditional(coordNewScreenSize); - _viewport = _textBuffer->GetSize().Clamp(_viewport); return STATUS_SUCCESS; } NT_CATCH_RETURN() @@ -1463,13 +1454,6 @@ NT_CATCH_RETURN() { gci.CookedReadData().EraseBeforeResize(); } - const auto cookedReadRestore = wil::scope_exit([]() { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (gci.HasPendingCookedRead()) - { - gci.CookedReadData().RedrawAfterResize(); - } - }); const auto fWrapText = gci.GetWrapText(); // GH#3493: Don't reflow the alt buffer. From a63b806b89b0d068590595c741314d11f5873d36 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 9 Jul 2024 15:26:45 +0200 Subject: [PATCH 15/21] Address feedback, Add comments, Update formatAttributes --- src/host/VtIo.cpp | 52 +++++++++++++++++++++++++++---------- src/host/_stream.cpp | 2 ++ src/host/readDataCooked.cpp | 22 +++++----------- src/host/readDataCooked.hpp | 15 +++++++++++ src/host/screenInfo.cpp | 13 ++++++++++ src/host/utils.cpp | 3 +-- 6 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index 0b4de74cc2d..08e8fb9fa6b 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -519,40 +519,64 @@ bool VtIo::IsResizeQuirkEnabled() const return S_OK; } -static size_t formatAttributes(char (&buffer)[16], WORD attributes) noexcept +// Formats the given console attributes to their closest VT equivalent. +// `out` must refer to at least `formatAttributesMaxLen` characters of valid memory. +// Returns a pointer past the end. +static constexpr size_t formatAttributesMaxLen = 13; +static char* formatAttributes(char* out, WORD attributes) noexcept { - const uint8_t rv = WI_IsFlagSet(attributes, COMMON_LVB_REVERSE_VIDEO) ? 7 : 27; - uint8_t fg = 39; - uint8_t bg = 49; + // Applications expect that SetConsoleTextAttribute() completely replaces whatever attributes are currently set, + // including any potential VT-exclusive attributes. Since we don't know what those are, we must always emit a SGR 0. + // Copying 4 bytes instead of the correct 3 means we need just 1 DWORD mov. Neat. + // + // 3 bytes. + memcpy(out, "\x1b[0", 4); + out += 3; + + // 2 bytes. + if (attributes & COMMON_LVB_REVERSE_VIDEO) + { + memcpy(out, ";7", 2); + out += 2; + } // `attributes` of exactly `FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED` // are often used to indicate the default colors in Windows Console applications. - // Thus, we translate them to 39/49 (default foreground/background). + // Since we always emit SGR 0 (reset all attributes), we simply need to skip this branch. + // + // 7 bytes (";97;107"). if ((attributes & (FG_ATTRS | BG_ATTRS)) != (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED)) { // The Console API represents colors in BGR order, but VT represents them in RGB order. // This LUT transposes them. This is for foreground colors. Add +10 to get the background ones. static constexpr uint8_t lut[] = { 30, 34, 32, 36, 31, 35, 33, 37, 90, 94, 92, 96, 91, 95, 93, 97 }; - fg = lut[attributes & 0xf]; - bg = lut[(attributes >> 4) & 0xf] + 10; + const uint8_t fg = lut[attributes & 0xf]; + const uint8_t bg = lut[(attributes >> 4) & 0xf] + 10; + out = fmt::format_to(out, FMT_COMPILE(";{};{}"), fg, bg); } - return fmt::format_to(&buffer[0], FMT_COMPILE("\x1b[{};{};{}m"), rv, fg, bg) - &buffer[0]; + // 1 byte. + *out++ = 'm'; + return out; } void VtIo::FormatAttributes(std::string& target, WORD attributes) { - char buf[16]; - const auto len = formatAttributes(buf, attributes); - target.append(buf, len); + const auto len = target.size(); + const auto cap = len + formatAttributesMaxLen; + target._Resize_and_overwrite(cap, [=](char* ptr, const size_t) noexcept { + auto end = ptr + len; + end = formatAttributes(end, attributes); + return end - ptr; + }); } void VtIo::FormatAttributes(std::wstring& target, WORD attributes) { - char buf[16]; - const auto len = formatAttributes(buf, attributes); + char buf[formatAttributesMaxLen]; + const size_t len = formatAttributes(&buf[0], attributes) - &buf[0]; - wchar_t bufW[16]; + wchar_t bufW[formatAttributesMaxLen]; for (size_t i = 0; i < len; i++) { bufW[i] = buf[i]; diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index ca7b26350d0..5679557ea84 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -255,6 +255,8 @@ void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& t } } +// This is the main entrypoint for conhost to write VT to the buffer. +// This wrapper around StateMachine exists so that we can add the necessary ConPTY transformations. void WriteCharsVT(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str) { screenInfo.GetStateMachine().ProcessString(str); diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index f44894a03c5..3e917d9021c 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -61,14 +61,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, THROW_IF_FAILED(_screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, _tempHandle)); #endif - const auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - auto cursorPos = cursor.GetPosition(); - - _screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos); - cursorPos.x = std::max(0, cursorPos.x); - cursorPos.y = std::max(0, cursorPos.y); - + const auto cursorPos = _getCursorPosition(); _originInViewport = cursorPos; if (!initialData.empty()) @@ -314,14 +307,8 @@ void COOKED_READ_DATA::RedrawAfterResize() _redrawPending = false; - // Get the new cursor position after the reflow. Just like how the COOKED_READ_DATA constructor did it. - const auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - auto cursorPos = cursor.GetPosition(); - _screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos); - cursorPos.x = std::max(0, cursorPos.x); - cursorPos.y = std::max(0, cursorPos.y); - _originInViewport = cursorPos; + // Get the new cursor position after the reflow, since it may have changed. + _originInViewport = _getCursorPosition(); // Ensure that we don't use any scroll sequences or try to clear previous pager contents. // They have all been erased with the CSI J above. @@ -357,9 +344,12 @@ til::point_span COOKED_READ_DATA::GetBoundaries() const noexcept static constexpr til::point min; const til::point max{ viewport.RightInclusive(), viewport.BottomInclusive() }; + // Convert from VT-viewport-relative coordinate space back to the console one. auto beg = _originInViewport; virtualViewport.ConvertFromOrigin(&beg); + // Since the pager may be longer than the viewport is tall, we need to clamp the coordinates to still remain within + // the current viewport (the pager doesn't write outside of the viewport, since that's not supported by VT). auto end = _pagerPromptEnd; end.y -= _pagerContentTop; end = std::clamp(end, min, max); diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index 27fee91dd8c..cb4ad742dab 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -51,6 +51,7 @@ class COOKED_READ_DATA final : public ReadData enum class PopupKind { + // The F2 popup: // Copies text from the previous command between the current cursor position and the first instance // of a given char (but not including it) into the current prompt line at the current cursor position. // Basically, F3 and this prompt have identical behavior, but the prompt searches for a terminating character. @@ -63,11 +64,14 @@ class COOKED_READ_DATA final : public ReadData // Then this command, given the char "o" will turn it into // echo hell efgh CopyToChar, + // The F4 popup: // Erases text between the current cursor position and the first instance of a given char (but not including it). // It's unknown to me why this is was historically called "copy from char" as it conhost never copied anything. CopyFromChar, + // The F9 popup: // Let's you choose to replace the current prompt with one from the command history by index. CommandNumber, + // The F7 popup: // Let's you choose to replace the current prompt with one from the command history via a // visual select dialog. Among all the popups this one is the most widely used one by far. CommandList, @@ -119,6 +123,17 @@ class COOKED_READ_DATA final : public ReadData static size_t _wordPrev(const std::wstring_view& chars, size_t position); static size_t _wordNext(const std::wstring_view& chars, size_t position); + til::point _getCursorPosition() const noexcept + { + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + auto cursorPos = cursor.GetPosition(); + + _screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos); + cursorPos.x = std::max(0, cursorPos.x); + cursorPos.y = std::max(0, cursorPos.y); + return cursorPos; + } void _readCharInputLoop(); void _handleChar(wchar_t wch, DWORD modifiers); void _handleVkey(uint16_t vkey, DWORD modifiers); diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index e00973dea96..f383bff4755 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1200,7 +1200,20 @@ void SCREEN_INFORMATION::_InternalSetViewportSize(const til::size* const pcoordS _viewport = newViewport; Tracing::s_TraceWindowViewport(_viewport); + // In Conpty mode, call TriggerScroll here without params. By not providing + // params, the renderer will make sure to update the VtEngine with the + // updated viewport size. If we don't do this, the engine can get into a + // torn state on this frame. + // + // Without this statement, the engine won't be told about the new view size + // till the start of the next frame. If any other text gets output before + // that frame starts, there's a very real chance that it'll cause errors as + // the engine tries to invalidate those regions. auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.IsInVtIoMode() && ServiceLocator::LocateGlobals().pRender) + { + ServiceLocator::LocateGlobals().pRender->TriggerScroll(); + } if (gci.HasPendingCookedRead()) { gci.CookedReadData().RedrawAfterResize(); diff --git a/src/host/utils.cpp b/src/host/utils.cpp index 0aeece0ef4f..0a3162e1f57 100644 --- a/src/host/utils.cpp +++ b/src/host/utils.cpp @@ -59,8 +59,7 @@ WORD ConvertStringToDec(_In_ PCWSTR pwchToConvert, _Out_opt_ PCWSTR* const ppwch // - Retrieves string resources from our resource files. // Arguments: // - id - Resource id from resource.h to the string we need to load. -// Return Value: -// - The string resource +// - out - Receives the translated string. void _LoadString(const UINT id, std::wstring& out) { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); From 8116d5bdbacc23e48998e63e0a43610027cf4a6b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 9 Jul 2024 22:33:36 +0200 Subject: [PATCH 16/21] Address j4james' feedback from the other PR --- src/host/VtIo.cpp | 31 ++++++++++++++----------------- src/host/VtIo.hpp | 4 ++-- src/host/readDataCooked.cpp | 2 +- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index 08e8fb9fa6b..0f477395fa5 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -522,8 +522,8 @@ bool VtIo::IsResizeQuirkEnabled() const // Formats the given console attributes to their closest VT equivalent. // `out` must refer to at least `formatAttributesMaxLen` characters of valid memory. // Returns a pointer past the end. -static constexpr size_t formatAttributesMaxLen = 13; -static char* formatAttributes(char* out, WORD attributes) noexcept +static constexpr size_t formatAttributesMaxLen = 16; +static char* formatAttributes(char* out, const TextAttribute& attributes) noexcept { // Applications expect that SetConsoleTextAttribute() completely replaces whatever attributes are currently set, // including any potential VT-exclusive attributes. Since we don't know what those are, we must always emit a SGR 0. @@ -534,25 +534,22 @@ static char* formatAttributes(char* out, WORD attributes) noexcept out += 3; // 2 bytes. - if (attributes & COMMON_LVB_REVERSE_VIDEO) + if (attributes.IsReverseVideo()) { memcpy(out, ";7", 2); out += 2; } - // `attributes` of exactly `FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED` - // are often used to indicate the default colors in Windows Console applications. - // Since we always emit SGR 0 (reset all attributes), we simply need to skip this branch. - // - // 7 bytes (";97;107"). - if ((attributes & (FG_ATTRS | BG_ATTRS)) != (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED)) + // 7 bytes (";97"). + if (attributes.GetForeground().IsIndex16()) + { + out = fmt::format_to(out, FMT_COMPILE(";{}"), attributes.GetForeground().GetIndex()); + } + + // 4 bytes (";107"). + if (attributes.GetBackground().IsIndex16()) { - // The Console API represents colors in BGR order, but VT represents them in RGB order. - // This LUT transposes them. This is for foreground colors. Add +10 to get the background ones. - static constexpr uint8_t lut[] = { 30, 34, 32, 36, 31, 35, 33, 37, 90, 94, 92, 96, 91, 95, 93, 97 }; - const uint8_t fg = lut[attributes & 0xf]; - const uint8_t bg = lut[(attributes >> 4) & 0xf] + 10; - out = fmt::format_to(out, FMT_COMPILE(";{};{}"), fg, bg); + out = fmt::format_to(out, FMT_COMPILE(";{}"), attributes.GetBackground().GetIndex()); } // 1 byte. @@ -560,7 +557,7 @@ static char* formatAttributes(char* out, WORD attributes) noexcept return out; } -void VtIo::FormatAttributes(std::string& target, WORD attributes) +void VtIo::FormatAttributes(std::string& target, const TextAttribute& attributes) { const auto len = target.size(); const auto cap = len + formatAttributesMaxLen; @@ -571,7 +568,7 @@ void VtIo::FormatAttributes(std::string& target, WORD attributes) }); } -void VtIo::FormatAttributes(std::wstring& target, WORD attributes) +void VtIo::FormatAttributes(std::wstring& target, const TextAttribute& attributes) { char buf[formatAttributesMaxLen]; const size_t len = formatAttributes(&buf[0], attributes) - &buf[0]; diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp index 28a6d356869..706c982bc67 100644 --- a/src/host/VtIo.hpp +++ b/src/host/VtIo.hpp @@ -20,8 +20,8 @@ namespace Microsoft::Console::VirtualTerminal class VtIo { public: - static void FormatAttributes(std::string& target, WORD attributes); - static void FormatAttributes(std::wstring& target, WORD attributes); + static void FormatAttributes(std::string& target, const TextAttribute& attributes); + static void FormatAttributes(std::wstring& target, const TextAttribute& attributes); VtIo(); diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 3e917d9021c..4915eb89311 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -1251,7 +1251,7 @@ void COOKED_READ_DATA::_appendCUP(std::wstring& output, til::point pos) void COOKED_READ_DATA::_appendPopupAttr(std::wstring& output) const { - VtIo::FormatAttributes(output, _screenInfo.GetPopupAttributes().GetLegacyAttributes()); + VtIo::FormatAttributes(output, _screenInfo.GetPopupAttributes()); } void COOKED_READ_DATA::_popupPush(const PopupKind kind) From 1f5dc902c89af0a735866b718b7e09c8b4fa87ea Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 9 Jul 2024 22:33:48 +0200 Subject: [PATCH 17/21] Fix buffer invalidation --- src/host/readDataCooked.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 4915eb89311..f7a7a4451ed 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -826,7 +826,7 @@ void COOKED_READ_DATA::_replace(size_t offset, size_t remove, const wchar_t* inp if (offset <= _bufferDirtyBeg) { const auto& textBuffer = _screenInfo.GetTextBuffer(); - _bufferDirtyBeg = textBuffer.GraphemePrev(_buffer, _bufferCursor); + _bufferDirtyBeg = textBuffer.GraphemePrev(_buffer, offset + 1); } } From 8c7cf58bcc2ece8f57a544d1aa53e27e44221857 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 9 Jul 2024 22:35:37 +0200 Subject: [PATCH 18/21] Oh, right, we support graphemes --- src/host/readDataCooked.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index f7a7a4451ed..f305202e654 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -821,13 +821,8 @@ void COOKED_READ_DATA::_replace(size_t offset, size_t remove, const wchar_t* inp _buffer.replace(offset, remove, input, count); _bufferCursor = offset + count; + _bufferDirtyBeg = std::min(_bufferDirtyBeg, offset); _dirty = true; - - if (offset <= _bufferDirtyBeg) - { - const auto& textBuffer = _screenInfo.GetTextBuffer(); - _bufferDirtyBeg = textBuffer.GraphemePrev(_buffer, offset + 1); - } } void COOKED_READ_DATA::_replace(const std::wstring_view& str) From b3f11d5bd214d05abe083ff329adeb75788fa3b0 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 9 Jul 2024 22:59:08 +0200 Subject: [PATCH 19/21] Fix formatAttributes --- src/host/VtIo.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index 0f477395fa5..037c36b5a99 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -525,6 +525,8 @@ bool VtIo::IsResizeQuirkEnabled() const static constexpr size_t formatAttributesMaxLen = 16; static char* formatAttributes(char* out, const TextAttribute& attributes) noexcept { + static uint8_t sgr[] = { 30, 31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97 }; + // Applications expect that SetConsoleTextAttribute() completely replaces whatever attributes are currently set, // including any potential VT-exclusive attributes. Since we don't know what those are, we must always emit a SGR 0. // Copying 4 bytes instead of the correct 3 means we need just 1 DWORD mov. Neat. @@ -541,15 +543,17 @@ static char* formatAttributes(char* out, const TextAttribute& attributes) noexce } // 7 bytes (";97"). - if (attributes.GetForeground().IsIndex16()) + if (attributes.GetForeground().IsLegacy()) { - out = fmt::format_to(out, FMT_COMPILE(";{}"), attributes.GetForeground().GetIndex()); + const uint8_t index = sgr[attributes.GetForeground().GetIndex()]; + out = fmt::format_to(out, FMT_COMPILE(";{}"), index); } // 4 bytes (";107"). - if (attributes.GetBackground().IsIndex16()) + if (attributes.GetBackground().IsLegacy()) { - out = fmt::format_to(out, FMT_COMPILE(";{}"), attributes.GetBackground().GetIndex()); + const uint8_t index = sgr[attributes.GetBackground().GetIndex()] + 10; + out = fmt::format_to(out, FMT_COMPILE(";{}"), index); } // 1 byte. From 08f3bacf6b504aab79aa1f6c5ac94ed4edb27f30 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 10 Jul 2024 00:05:59 +0200 Subject: [PATCH 20/21] Forgot to finish this --- src/host/readDataCooked.cpp | 16 ++++++++++++++-- src/host/readDataCooked.hpp | 12 +----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index f305202e654..a2cb1194632 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -61,7 +61,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, THROW_IF_FAILED(_screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, _tempHandle)); #endif - const auto cursorPos = _getCursorPosition(); + const auto cursorPos = _getViewportCursorPosition(); _originInViewport = cursorPos; if (!initialData.empty()) @@ -308,7 +308,7 @@ void COOKED_READ_DATA::RedrawAfterResize() _redrawPending = false; // Get the new cursor position after the reflow, since it may have changed. - _originInViewport = _getCursorPosition(); + _originInViewport = _getViewportCursorPosition(); // Ensure that we don't use any scroll sequences or try to clear previous pager contents. // They have all been erased with the CSI J above. @@ -807,6 +807,18 @@ void COOKED_READ_DATA::_transitionState(State state) noexcept _state = state; } +til::point COOKED_READ_DATA::_getViewportCursorPosition() const noexcept +{ + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + auto cursorPos = cursor.GetPosition(); + + _screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos); + cursorPos.x = std::max(0, cursorPos.x); + cursorPos.y = std::max(0, cursorPos.y); + return cursorPos; +} + void COOKED_READ_DATA::_replace(size_t offset, size_t remove, const wchar_t* input, size_t count) { const auto size = _buffer.size(); diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index cb4ad742dab..22da5334276 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -123,22 +123,12 @@ class COOKED_READ_DATA final : public ReadData static size_t _wordPrev(const std::wstring_view& chars, size_t position); static size_t _wordNext(const std::wstring_view& chars, size_t position); - til::point _getCursorPosition() const noexcept - { - const auto& textBuffer = _screenInfo.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - auto cursorPos = cursor.GetPosition(); - - _screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos); - cursorPos.x = std::max(0, cursorPos.x); - cursorPos.y = std::max(0, cursorPos.y); - return cursorPos; - } void _readCharInputLoop(); void _handleChar(wchar_t wch, DWORD modifiers); void _handleVkey(uint16_t vkey, DWORD modifiers); void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); void _transitionState(State state) noexcept; + til::point _getViewportCursorPosition() const noexcept; void _replace(size_t offset, size_t remove, const wchar_t* input, size_t count); void _replace(const std::wstring_view& str); std::wstring_view _slice(size_t from, size_t to) const noexcept; From dc8ac1d0e04ec6c59b0d7abe17ec20a8e482fcfa Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 11 Jul 2024 16:35:39 +0200 Subject: [PATCH 21/21] Improve comments, resize_and_overwrite is terrible --- src/host/VtIo.cpp | 12 ++++-------- src/host/readDataCooked.cpp | 12 ++++++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp index 037c36b5a99..717cf3429db 100644 --- a/src/host/VtIo.cpp +++ b/src/host/VtIo.cpp @@ -542,7 +542,7 @@ static char* formatAttributes(char* out, const TextAttribute& attributes) noexce out += 2; } - // 7 bytes (";97"). + // 3 bytes (";97"). if (attributes.GetForeground().IsLegacy()) { const uint8_t index = sgr[attributes.GetForeground().GetIndex()]; @@ -563,13 +563,9 @@ static char* formatAttributes(char* out, const TextAttribute& attributes) noexce void VtIo::FormatAttributes(std::string& target, const TextAttribute& attributes) { - const auto len = target.size(); - const auto cap = len + formatAttributesMaxLen; - target._Resize_and_overwrite(cap, [=](char* ptr, const size_t) noexcept { - auto end = ptr + len; - end = formatAttributes(end, attributes); - return end - ptr; - }); + char buf[formatAttributesMaxLen]; + const size_t len = formatAttributes(&buf[0], attributes) - &buf[0]; + target.append(buf, len); } void VtIo::FormatAttributes(std::wstring& target, const TextAttribute& attributes) diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index a2cb1194632..2fc30f76ec9 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -108,14 +108,18 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, line.reserve(size.width); - // Do a brute force search for the best starting position that ends at the current cursor position. + // We're given an "end position" and a string and we need to find its starting position. + // The problem is that a wide glyph that doesn't fit into the last column of a row gets padded with a whitespace + // and then written on the next line. Because of this, multiple starting positions can result in the same end + // position and this prevents us from simply laying out the text backwards from the end position. + // To solve this, we do a brute force search for the best starting position that ends at the end position. // The search is centered around `bestGuessColumn` with offsets 0, 1, -1, 2, -2, 3, -3, ... for (til::CoordType i = 0, attempts = 2 * size.width; i <= attempts; i++) { // Hilarious bit-trickery that no one can read. But it works. Check it out in a debugger. - // The idea is to use bits 1:31 as the value and bit 0 as a trigger to bit-flip the value. - // A bit-flipped positive number is negative, but offset by 1, so we add 1. Fun! - const auto offset = ((i / 2) ^ ((i & 1) - 1)) + 1; + // The idea is to use bits 1:31 as the value (i >> 1) and bit 0 (i & 1) as a trigger to bit-flip the value. + // A bit-flipped positive number is negative, but offset by 1, so we add 1 at the end. Fun! + const auto offset = ((i >> 1) ^ ((i & 1) - 1)) + 1; const auto columnBegin = bestGuessColumn + offset; if (columnBegin < 0 || columnBegin >= size.width)