diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 5c0c14b0df9..1a189277aef 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -406,6 +406,7 @@ DECNKM DECNRCM DECOM decommit +DECPCCM DECPCTERM DECPS DECRARA @@ -414,6 +415,7 @@ DECREQTPARM DECRLM DECRPM DECRQCRA +DECRQDE DECRQM DECRQPSR DECRQSS @@ -2123,6 +2125,7 @@ XIn XManifest XMath xorg +XPan XResource xsi xstyler @@ -2142,6 +2145,7 @@ YCast YCENTER YCount YLimit +YPan YSubstantial YVIRTUALSCREEN YWalk diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 75b822b1386..bf1120f421a 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -131,8 +131,7 @@ class Microsoft::Terminal::Core::Terminal final : // These methods are defined in TerminalApi.cpp void ReturnResponse(const std::wstring_view response) override; Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() noexcept override; - TextBuffer& GetTextBuffer() noexcept override; - til::rect GetViewport() const noexcept override; + BufferState GetBufferAndViewport() noexcept override; void SetViewportPosition(const til::point position) noexcept override; void SetTextAttributes(const TextAttribute& attrs) noexcept override; void SetSystemMode(const Mode mode, const bool enabled) noexcept override; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 9b85b18d5ff..aa06c101295 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -34,14 +34,9 @@ Microsoft::Console::VirtualTerminal::StateMachine& Terminal::GetStateMachine() n return *_stateMachine; } -TextBuffer& Terminal::GetTextBuffer() noexcept +ITerminalApi::BufferState Terminal::GetBufferAndViewport() noexcept { - return _activeBuffer(); -} - -til::rect Terminal::GetViewport() const noexcept -{ - return til::rect{ _GetMutableViewport().ToInclusive() }; + return { _activeBuffer(), til::rect{ _GetMutableViewport().ToInclusive() }, !_inAltBuffer() }; } void Terminal::SetViewportPosition(const til::point position) noexcept diff --git a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp index 195eb17fcc3..1e55fb1f6e9 100644 --- a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp @@ -44,6 +44,11 @@ namespace TerminalCoreUnitTests VERIFY_ARE_EQUAL(selection, expected); } + TextBuffer& GetTextBuffer(Terminal& term) + { + return term.GetBufferAndViewport().buffer; + } + TEST_METHOD(SelectUnit) { Terminal term{ Terminal::TestDummyMarker{} }; @@ -394,7 +399,7 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Simulate click at (x,y) = (5,10) @@ -417,7 +422,7 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Simulate click at (x,y) = (5,10) @@ -440,11 +445,11 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Insert wide glyph at position (7,11) - term.GetTextBuffer().GetCursor().SetPosition({ 7, 11 }); + GetTextBuffer(term).GetCursor().SetPosition({ 7, 11 }); term.Write(burrito); // Simulate ALT + click at (x,y) = (5,8) @@ -496,7 +501,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (5,10) @@ -540,7 +545,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"C:\\Terminal>"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate click at (x,y) = (15,10) @@ -568,7 +573,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (5,10) @@ -597,7 +602,7 @@ namespace TerminalCoreUnitTests // Insert text at position (21,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (21,10) @@ -685,7 +690,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Step 1: Create a selection on "doubleClickMe" diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp index 9f002db5098..157bad1805d 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp @@ -152,7 +152,8 @@ void TerminalApiTest::CursorVisibility() VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); - term.GetTextBuffer().GetCursor().SetIsVisible(false); + auto& textBuffer = term.GetBufferAndViewport().buffer; + textBuffer.GetCursor().SetIsVisible(false); VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsVisible()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index fb20d74b75b..151a472f69d 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -52,25 +52,16 @@ StateMachine& ConhostInternalGetSet::GetStateMachine() } // Routine Description: -// - Retrieves the text buffer for the active output buffer. +// - Retrieves the text buffer and virtual viewport for the active output +// buffer. Also returns a flag indicating whether it's the main buffer. // Arguments: // - // Return Value: -// - a reference to the TextBuffer instance. -TextBuffer& ConhostInternalGetSet::GetTextBuffer() +// - a tuple with the buffer reference, viewport, and main buffer flag. +ITerminalApi::BufferState ConhostInternalGetSet::GetBufferAndViewport() { - return _io.GetActiveOutputBuffer().GetTextBuffer(); -} - -// Routine Description: -// - Retrieves the virtual viewport of the active output buffer. -// Arguments: -// - -// Return Value: -// - the exclusive coordinates of the viewport. -til::rect ConhostInternalGetSet::GetViewport() const -{ - return _io.GetActiveOutputBuffer().GetVirtualViewport().ToExclusive(); + auto& info = _io.GetActiveOutputBuffer(); + return { info.GetTextBuffer(), info.GetVirtualViewport().ToExclusive(), info.Next == nullptr }; } // Routine Description: diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 01d8abaf17f..36283087ac2 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -32,8 +32,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: void ReturnResponse(const std::wstring_view response) override; Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() override; - TextBuffer& GetTextBuffer() override; - til::rect GetViewport() const override; + BufferState GetBufferAndViewport() override; void SetViewportPosition(const til::point position) override; void SetTextAttributes(const TextAttribute& attrs) override; diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 52852bed6a2..a0a74d8c83a 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -531,6 +531,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes ATT610_StartCursorBlink = DECPrivateMode(12), DECTCEM_TextCursorEnableMode = DECPrivateMode(25), XTERM_EnableDECCOLMSupport = DECPrivateMode(40), + DECPCCM_PageCursorCouplingMode = DECPrivateMode(64), DECNKM_NumericKeypadMode = DECPrivateMode(66), DECBKM_BackarrowKeyMode = DECPrivateMode(67), DECLRMM_LeftRightMarginMode = DECPrivateMode(69), diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index eed892f5a94..e3976996433 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -49,6 +49,12 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool DeleteCharacter(const VTInt count) = 0; // DCH virtual bool ScrollUp(const VTInt distance) = 0; // SU virtual bool ScrollDown(const VTInt distance) = 0; // SD + virtual bool NextPage(const VTInt pageCount) = 0; // NP + virtual bool PrecedingPage(const VTInt pageCount) = 0; // PP + virtual bool PagePositionAbsolute(const VTInt page) = 0; // PPA + virtual bool PagePositionRelative(const VTInt pageCount) = 0; // PPR + virtual bool PagePositionBack(const VTInt pageCount) = 0; // PPB + virtual bool RequestDisplayedExtent() = 0; // DECRQDE virtual bool InsertLine(const VTInt distance) = 0; // IL virtual bool DeleteLine(const VTInt distance) = 0; // DL virtual bool InsertColumn(const VTInt distance) = 0; // DECIC diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index 4381c4ecbdd..2c3aec044ed 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -39,9 +39,15 @@ namespace Microsoft::Console::VirtualTerminal virtual void ReturnResponse(const std::wstring_view response) = 0; + struct BufferState + { + TextBuffer& buffer; + til::rect viewport; + bool isMainBuffer; + }; + virtual StateMachine& GetStateMachine() = 0; - virtual TextBuffer& GetTextBuffer() = 0; - virtual til::rect GetViewport() const = 0; + virtual BufferState GetBufferAndViewport() = 0; virtual void SetViewportPosition(const til::point position) = 0; virtual bool IsVtInputEnabled() const = 0; diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp index cb06c985c91..a102368ec5b 100644 --- a/src/terminal/adapter/InteractDispatch.cpp +++ b/src/terminal/adapter/InteractDispatch.cpp @@ -108,7 +108,7 @@ bool InteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulatio _api.ShowWindow(false); return true; case DispatchTypes::WindowManipulationType::RefreshWindow: - _api.GetTextBuffer().TriggerRedrawAll(); + _api.GetBufferAndViewport().buffer.TriggerRedrawAll(); return true; case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: // TODO:GH#1765 We should introduce a better `ResizeConpty` function to @@ -135,7 +135,7 @@ bool InteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulatio bool InteractDispatch::MoveCursor(const VTInt row, const VTInt col) { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); + const auto viewport = _api.GetBufferAndViewport().viewport; // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. // Apply boundary tests to ensure the cursor isn't outside the viewport rectangle. diff --git a/src/terminal/adapter/PageManager.cpp b/src/terminal/adapter/PageManager.cpp new file mode 100644 index 00000000000..5408b249738 --- /dev/null +++ b/src/terminal/adapter/PageManager.cpp @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "PageManager.hpp" +#include "../../renderer/base/renderer.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +Page::Page(TextBuffer& buffer, const til::rect& viewport, const til::CoordType number) noexcept : + _buffer{ buffer }, + _viewport{ viewport }, + _number(number) +{ +} + +TextBuffer& Page::Buffer() const noexcept +{ + return _buffer; +} + +til::rect Page::Viewport() const noexcept +{ + return _viewport; +} + +til::CoordType Page::Number() const noexcept +{ + return _number; +} + +Cursor& Page::Cursor() const noexcept +{ + return _buffer.GetCursor(); +} + +const TextAttribute& Page::Attributes() const noexcept +{ + return _buffer.GetCurrentAttributes(); +} + +void Page::SetAttributes(const TextAttribute& attr, ITerminalApi* api) const +{ + _buffer.SetCurrentAttributes(attr); + // If the api parameter was specified, we need to pass the new attributes + // through to the api. This occurs when there's a potential for the colors + // to be changed, which may require some legacy remapping in conhost. + if (api) + { + api->SetTextAttributes(attr); + } +} + +til::CoordType Page::Top() const noexcept +{ + // If we ever support vertical window panning, the page top won't + // necessarily align with the viewport top, so it's best we always + // treat them as distinct properties. + return _viewport.top; +} + +til::CoordType Page::Bottom() const noexcept +{ + // Similarly, the page bottom won't always match the viewport bottom. + return _viewport.bottom; +} + +til::CoordType Page::Width() const noexcept +{ + // The page width could also one day be different from the buffer width, + // so again it's best treated as a distinct property. + return _buffer.GetSize().Width(); +} + +til::CoordType Page::Height() const noexcept +{ + return Bottom() - Top(); +} + +til::CoordType Page::BufferHeight() const noexcept +{ + return _buffer.GetSize().Height(); +} + +til::CoordType Page::XPanOffset() const noexcept +{ + return _viewport.left; +} + +til::CoordType Page::YPanOffset() const noexcept +{ + return 0; // Vertical panning is not yet supported +} + +PageManager::PageManager(ITerminalApi& api, Renderer& renderer) noexcept : + _api{ api }, + _renderer{ renderer } +{ +} + +void PageManager::Reset() +{ + _activePageNumber = 1; + _visiblePageNumber = 1; + _buffers = {}; +} + +Page PageManager::Get(const til::CoordType pageNumber) const +{ + const auto requestedPageNumber = std::min(std::max(pageNumber, 1), MAX_PAGES); + auto [visibleBuffer, visibleViewport, isMainBuffer] = _api.GetBufferAndViewport(); + + // If we're not in the main buffer (either because an app has enabled the + // alternate buffer mode, or switched the conhost screen buffer), then VT + // paging doesn't apply, so we disregard the requested page number and just + // use the visible buffer (with a fixed page number of 1). + if (!isMainBuffer) + { + return { visibleBuffer, visibleViewport, 1 }; + } + + // If the requested page number happens to be the visible page, then we + // can also just use the visible buffer as is. + if (requestedPageNumber == _visiblePageNumber) + { + return { visibleBuffer, visibleViewport, _visiblePageNumber }; + } + + // Otherwise we're working with a background buffer, so we need to + // retrieve that from the buffer array, and resize it to match the + // active page size. + const auto pageSize = visibleViewport.size(); + auto& pageBuffer = _getBuffer(requestedPageNumber, pageSize); + return { pageBuffer, til::rect{ pageSize }, requestedPageNumber }; +} + +Page PageManager::ActivePage() const +{ + return Get(_activePageNumber); +} + +Page PageManager::VisiblePage() const +{ + return Get(_visiblePageNumber); +} + +void PageManager::MoveTo(const til::CoordType pageNumber, const bool makeVisible) +{ + auto [visibleBuffer, visibleViewport, isMainBuffer] = _api.GetBufferAndViewport(); + if (!isMainBuffer) + { + return; + } + + const auto pageSize = visibleViewport.size(); + const auto visibleTop = visibleViewport.top; + const auto wasVisible = _activePageNumber == _visiblePageNumber; + const auto newPageNumber = std::min(std::max(pageNumber, 1), MAX_PAGES); + auto redrawRequired = false; + + // If we're changing the visible page, what we do is swap out the current + // visible page into its backing buffer, and swap in the new page from the + // backing buffer to the main buffer. That way the rest of the system only + // ever has to deal with the main buffer. + if (makeVisible && _visiblePageNumber != newPageNumber) + { + const auto& newBuffer = _getBuffer(newPageNumber, pageSize); + auto& saveBuffer = _getBuffer(_visiblePageNumber, pageSize); + for (auto i = 0; i < pageSize.height; i++) + { + saveBuffer.GetMutableRowByOffset(i).CopyFrom(visibleBuffer.GetRowByOffset(visibleTop + i)); + } + for (auto i = 0; i < pageSize.height; i++) + { + visibleBuffer.GetMutableRowByOffset(visibleTop + i).CopyFrom(newBuffer.GetRowByOffset(i)); + } + _visiblePageNumber = newPageNumber; + redrawRequired = true; + } + + // If the active page was previously visible, and is now still visible, + // there is no need to update any buffer properties, because we'll have + // been using the main buffer in both cases. + const auto isVisible = newPageNumber == _visiblePageNumber; + if (!wasVisible || !isVisible) + { + // Otherwise we need to copy the properties from the old buffer to the + // new, so we retain the current attributes and cursor position. This + // is only needed if they are actually different. + auto& oldBuffer = wasVisible ? visibleBuffer : _getBuffer(_activePageNumber, pageSize); + auto& newBuffer = isVisible ? visibleBuffer : _getBuffer(newPageNumber, pageSize); + if (&oldBuffer != &newBuffer) + { + // When copying the cursor position, we need to adjust the y + // coordinate to account for scrollback in the visible buffer. + const auto oldTop = wasVisible ? visibleTop : 0; + const auto newTop = isVisible ? visibleTop : 0; + auto position = oldBuffer.GetCursor().GetPosition(); + position.y = position.y - oldTop + newTop; + newBuffer.SetCurrentAttributes(oldBuffer.GetCurrentAttributes()); + newBuffer.CopyProperties(oldBuffer); + newBuffer.GetCursor().SetPosition(position); + } + // If we moved from the visible buffer to a background buffer we need + // to hide the cursor in the visible buffer. This is because the page + // number is like a third dimension in the cursor coordinate system. + // If the cursor isn't on the visible page, it's the same as if its + // x/y coordinates are outside the visible viewport. + if (wasVisible && !isVisible) + { + visibleBuffer.GetCursor().SetIsVisible(false); + } + } + + _activePageNumber = newPageNumber; + if (redrawRequired) + { + _renderer.TriggerRedrawAll(); + } +} + +void PageManager::MoveRelative(const til::CoordType pageCount, const bool makeVisible) +{ + MoveTo(_activePageNumber + pageCount, makeVisible); +} + +void PageManager::MakeActivePageVisible() +{ + if (_activePageNumber != _visiblePageNumber) + { + MoveTo(_activePageNumber, true); + } +} + +TextBuffer& PageManager::_getBuffer(const til::CoordType pageNumber, const til::size pageSize) const +{ + auto& buffer = til::at(_buffers, pageNumber - 1); + if (buffer == nullptr) + { + // Page buffers are created on demand, and are sized to match the active + // page dimensions without any scrollback rows. + buffer = std::make_unique(pageSize, TextAttribute{}, 0, false, _renderer); + } + else if (buffer->GetSize().Dimensions() != pageSize) + { + // If a buffer already exists for the page, and the page dimensions have + // changed while it was inactive, it will need to be resized. + // TODO: We don't currently reflow the existing content in this case, but + // that may be something we want to reconsider. + buffer->ResizeTraditional(pageSize); + } + return *buffer; +} diff --git a/src/terminal/adapter/PageManager.hpp b/src/terminal/adapter/PageManager.hpp new file mode 100644 index 00000000000..ab913902609 --- /dev/null +++ b/src/terminal/adapter/PageManager.hpp @@ -0,0 +1,67 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- PageManager.hpp + +Abstract: +- This manages the text buffers required by the VT paging operations. +--*/ + +#pragma once + +#include "ITerminalApi.hpp" +#include "til.h" + +namespace Microsoft::Console::VirtualTerminal +{ + class Page + { + public: + Page(TextBuffer& buffer, const til::rect& viewport, const til::CoordType number) noexcept; + TextBuffer& Buffer() const noexcept; + til::rect Viewport() const noexcept; + til::CoordType Number() const noexcept; + Cursor& Cursor() const noexcept; + const TextAttribute& Attributes() const noexcept; + void SetAttributes(const TextAttribute& attr, ITerminalApi* api = nullptr) const; + til::CoordType Top() const noexcept; + til::CoordType Bottom() const noexcept; + til::CoordType Width() const noexcept; + til::CoordType Height() const noexcept; + til::CoordType BufferHeight() const noexcept; + til::CoordType XPanOffset() const noexcept; + til::CoordType YPanOffset() const noexcept; + + private: + TextBuffer& _buffer; + til::rect _viewport; + til::CoordType _number; + }; + + class PageManager + { + using Renderer = Microsoft::Console::Render::Renderer; + + public: + PageManager(ITerminalApi& api, Renderer& renderer) noexcept; + void Reset(); + Page Get(const til::CoordType pageNumber) const; + Page ActivePage() const; + Page VisiblePage() const; + void MoveTo(const til::CoordType pageNumber, const bool makeVisible); + void MoveRelative(const til::CoordType pageCount, const bool makeVisible); + void MakeActivePageVisible(); + + private: + TextBuffer& _getBuffer(const til::CoordType pageNumber, const til::size pageSize) const; + + ITerminalApi& _api; + Renderer& _renderer; + til::CoordType _activePageNumber = 1; + til::CoordType _visiblePageNumber = 1; + static constexpr til::CoordType MAX_PAGES = 6; + mutable std::array, MAX_PAGES> _buffers; + }; +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 02c22db8eec..4135cac88d6 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -22,7 +22,8 @@ AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettin _renderSettings{ renderSettings }, _terminalInput{ terminalInput }, _usingAltBuffer(false), - _termOutput() + _termOutput(), + _pages{ api, renderer } { } @@ -72,15 +73,15 @@ void AdaptDispatch::PrintString(const std::wstring_view string) void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& textBuffer = page.Buffer(); + auto& cursor = page.Cursor(); auto cursorPosition = cursor.GetPosition(); const auto wrapAtEOL = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto& attributes = page.Attributes(); - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(textBuffer.GetSize().Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y); if (cursorPosition.x <= rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) @@ -106,7 +107,7 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) // different position from where the EOL was marked. if (delayedCursorPosition == cursorPosition) { - _DoLineFeed(textBuffer, true, true); + _DoLineFeed(page, true, true); cursorPosition = cursor.GetPosition(); // We need to recalculate the width when moving to a new line. lineWidth = textBuffer.GetLineWidth(cursorPosition.y); @@ -266,19 +267,19 @@ bool AdaptDispatch::CursorPrevLine(const VTInt distance) // Routine Description: // - Returns the coordinates of the vertical scroll margins. // Arguments: -// - viewport - The viewport rect (exclusive). -// - absolute - Should coordinates be absolute or relative to the viewport. +// - page - The page that the margins will apply to. +// - absolute - Should coordinates be absolute or relative to the page top. // Return Value: // - A std::pair containing the top and bottom coordinates (inclusive). -std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept +std::pair AdaptDispatch::_GetVerticalMargins(const Page& page, const bool absolute) noexcept { // If the top is out of range, reset the margins completely. - const auto bottommostRow = viewport.bottom - viewport.top - 1; + const auto bottommostRow = page.Height() - 1; if (_scrollMargins.top >= bottommostRow) { _scrollMargins.top = _scrollMargins.bottom = 0; } - // If margins aren't set, use the full extent of the viewport. + // If margins aren't set, use the full extent of the page. const auto marginsSet = _scrollMargins.top < _scrollMargins.bottom; auto topMargin = marginsSet ? _scrollMargins.top : 0; auto bottomMargin = marginsSet ? _scrollMargins.bottom : bottommostRow; @@ -286,8 +287,8 @@ std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport bottomMargin = std::min(bottomMargin, bottommostRow); if (absolute) { - topMargin += viewport.top; - bottomMargin += viewport.top; + topMargin += page.Top(); + bottomMargin += page.Top(); } return { topMargin, bottomMargin }; } @@ -295,13 +296,13 @@ std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport // Routine Description: // - Returns the coordinates of the horizontal scroll margins. // Arguments: -// - bufferWidth - The width of the buffer +// - pageWidth - The width of the page // Return Value: // - A std::pair containing the left and right coordinates (inclusive). -std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType bufferWidth) noexcept +std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType pageWidth) noexcept { // If the left is out of range, reset the margins completely. - const auto rightmostColumn = bufferWidth - 1; + const auto rightmostColumn = pageWidth - 1; if (_scrollMargins.left >= rightmostColumn) { _scrollMargins.left = _scrollMargins.right = 0; @@ -326,13 +327,12 @@ std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType bu bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins) { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); - const auto bufferWidth = textBuffer.GetSize().Width(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); + const auto pageWidth = page.Width(); const auto cursorPosition = cursor.GetPosition(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); // For relative movement, the given offsets will be relative to // the current cursor position. @@ -340,10 +340,10 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col auto col = cursorPosition.x; // But if the row is absolute, it will be relative to the top of the - // viewport, or the top margin, depending on the origin mode. + // page, or the top margin, depending on the origin mode. if (rowOffset.IsAbsolute) { - row = _modes.test(Mode::Origin) ? topMargin : viewport.top; + row = _modes.test(Mode::Origin) ? topMargin : page.Top(); } // And if the column is absolute, it'll be relative to column 0, @@ -355,10 +355,10 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col } // Adjust the base position by the given offsets and clamp the results. - // The row is constrained within the viewport's vertical boundaries, + // The row is constrained within the page's vertical boundaries, // while the column is constrained by the buffer width. - row = std::clamp(row + rowOffset.Value, viewport.top, viewport.bottom - 1); - col = std::clamp(col + colOffset.Value, 0, bufferWidth - 1); + row = std::clamp(row + rowOffset.Value, page.Top(), page.Bottom() - 1); + col = std::clamp(col + colOffset.Value, 0, pageWidth - 1); // If the operation needs to be clamped inside the margins, or the origin // mode is relative (which always requires margin clamping), then the row @@ -398,7 +398,7 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col } // Finally, attempt to set the adjusted cursor position back into the console. - cursor.SetPosition(textBuffer.ClampPositionWithinLine({ col, row })); + cursor.SetPosition(page.Buffer().ClampPositionWithinLine({ col, row })); _ApplyCursorMovementFlags(cursor); return true; @@ -490,29 +490,28 @@ bool AdaptDispatch::CursorPosition(const VTInt line, const VTInt column) bool AdaptDispatch::CursorSaveState() { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); // The cursor is given to us by the API as relative to the whole buffer. - // But in VT speak, the cursor row should be relative to the current viewport top. - auto cursorPosition = textBuffer.GetCursor().GetPosition(); - cursorPosition.y -= viewport.top; + // But in VT speak, the cursor row should be relative to the current page top. + auto cursorPosition = page.Cursor().GetPosition(); + cursorPosition.y -= page.Top(); // Although if origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } // VT is also 1 based, not 0 based, so correct by 1. auto& savedCursorState = _savedCursorState.at(_usingAltBuffer); savedCursorState.Column = cursorPosition.x + 1; savedCursorState.Row = cursorPosition.y + 1; - savedCursorState.IsDelayedEOLWrap = textBuffer.GetCursor().IsDelayedEOLWrap(); + savedCursorState.Page = page.Number(); + savedCursorState.IsDelayedEOLWrap = page.Cursor().IsDelayedEOLWrap(); savedCursorState.IsOriginModeRelative = _modes.test(Mode::Origin); - savedCursorState.Attributes = attributes; + savedCursorState.Attributes = page.Attributes(); savedCursorState.TermOutput = _termOutput; return true; @@ -533,17 +532,21 @@ bool AdaptDispatch::CursorRestoreState() // Restore the origin mode first, since the cursor coordinates may be relative. _modes.set(Mode::Origin, savedCursorState.IsOriginModeRelative); + // Restore the page number. + PagePositionAbsolute(savedCursorState.Page); + // We can then restore the position with a standard CUP operation. CursorPosition(savedCursorState.Row, savedCursorState.Column); // If the delayed wrap flag was set when the cursor was saved, we need to restore that now. + const auto page = _pages.ActivePage(); if (savedCursorState.IsDelayedEOLWrap) { - _api.GetTextBuffer().GetCursor().DelayEOLWrap(); + page.Cursor().DelayEOLWrap(); } // Restore text attributes. - _api.SetTextAttributes(savedCursorState.Attributes); + page.SetAttributes(savedCursorState.Attributes, &_api); // Restore designated character sets. _termOutput.RestoreFrom(savedCursorState.TermOutput); @@ -556,10 +559,10 @@ bool AdaptDispatch::CursorRestoreState() // the Erase Color mode is set, we use the default attributes, but when reset, // we use the active color attributes with the character attributes cleared. // Arguments: -// - textBuffer - Target buffer that is being erased. +// - page - Target page that is being erased. // Return Value: // - The erase TextAttribute value. -TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) const noexcept +TextAttribute AdaptDispatch::_GetEraseAttributes(const Page& page) const noexcept { if (_modes.test(Mode::EraseColor)) { @@ -567,7 +570,7 @@ TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) c } else { - auto eraseAttributes = textBuffer.GetCurrentAttributes(); + auto eraseAttributes = page.Attributes(); eraseAttributes.SetStandardErase(); return eraseAttributes; } @@ -576,13 +579,14 @@ TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) c // Routine Description: // - Scrolls an area of the buffer in a vertical direction. // Arguments: -// - textBuffer - Target buffer to be scrolled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be scrolled. +// - fillRect - Area of the page that will be affected. // - delta - Distance to move (positive is down, negative is up). // Return Value: // - -void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta) +void AdaptDispatch::_ScrollRectVertically(const Page& page, const til::rect& scrollRect, const VTInt delta) { + auto& textBuffer = page.Buffer(); const auto absoluteDelta = std::min(std::abs(delta), scrollRect.height()); if (absoluteDelta < scrollRect.height()) { @@ -590,7 +594,7 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec const auto width = scrollRect.width(); const auto height = scrollRect.height() - absoluteDelta; const auto actualDelta = delta > 0 ? absoluteDelta : -absoluteDelta; - if (width == textBuffer.GetSize().Width()) + if (width == page.Width()) { // If the scrollRect is the full width of the buffer, we can scroll // more efficiently by rotating the row storage. @@ -621,8 +625,8 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec auto eraseRect = scrollRect; eraseRect.top = delta > 0 ? scrollRect.top : (scrollRect.bottom - absoluteDelta); eraseRect.bottom = eraseRect.top + absoluteDelta; - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(eraseRect.top, eraseRect.bottom); @@ -631,13 +635,14 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec // Routine Description: // - Scrolls an area of the buffer in a horizontal direction. // Arguments: -// - textBuffer - Target buffer to be scrolled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be scrolled. +// - fillRect - Area of the page that will be affected. // - delta - Distance to move (positive is right, negative is left). // Return Value: // - -void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta) +void AdaptDispatch::_ScrollRectHorizontally(const Page& page, const til::rect& scrollRect, const VTInt delta) { + auto& textBuffer = page.Buffer(); const auto absoluteDelta = std::min(std::abs(delta), scrollRect.width()); if (absoluteDelta < scrollRect.width()) { @@ -669,8 +674,8 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r auto eraseRect = scrollRect; eraseRect.left = delta > 0 ? scrollRect.left : (scrollRect.right - absoluteDelta); eraseRect.right = eraseRect.left + absoluteDelta; - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); } // Routine Description: @@ -682,20 +687,19 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r // - void AdaptDispatch::_InsertDeleteCharacterHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; - const auto lineWidth = textBuffer.GetLineWidth(row); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto page = _pages.ActivePage(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; + const auto lineWidth = page.Buffer().GetLineWidth(row); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = (row >= topMargin && row <= bottomMargin) ? _GetHorizontalMargins(lineWidth) : std::make_pair(0, lineWidth - 1); if (col >= leftMargin && col <= rightMargin) { - _ScrollRectHorizontally(textBuffer, { col, row, rightMargin + 1, row + 1 }, delta); + _ScrollRectHorizontally(page, { col, row, rightMargin + 1, row + 1 }, delta); // The ICH and DCH controls are expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); } } @@ -728,15 +732,15 @@ bool AdaptDispatch::DeleteCharacter(const VTInt count) // Routine Description: // - Fills an area of the buffer with a given character and attributes. // Arguments: -// - textBuffer - Target buffer to be filled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be filled. +// - fillRect - Area of the page that will be affected. // - fillChar - Character to be written to the buffer. // - fillAttrs - Attributes to be written to the buffer. // Return Value: // - -void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const +void AdaptDispatch::_FillRect(const Page& page, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const { - textBuffer.FillRect(fillRect, fillChar, fillAttrs); + page.Buffer().FillRect(fillRect, fillChar, fillAttrs); _api.NotifyAccessibilityChange(fillRect); } @@ -751,28 +755,28 @@ void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, // - True. bool AdaptDispatch::EraseCharacters(const VTInt numChars) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto startCol = textBuffer.GetCursor().GetPosition().x; - const auto endCol = std::min(startCol + numChars, textBuffer.GetLineWidth(row)); + const auto page = _pages.ActivePage(); + const auto row = page.Cursor().GetPosition().y; + const auto startCol = page.Cursor().GetPosition().x; + const auto endCol = std::min(startCol + numChars, page.Buffer().GetLineWidth(row)); // The ECH control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes); return true; } // Routine Description: -// - ED - Erases a portion of the current viewable area (viewport) of the console. +// - ED - Erases a portion of the current page of the console. // Arguments: // - eraseType - Determines whether to erase: // From beginning (top-left corner) to the cursor // From cursor to end (bottom-right corner) -// The entire viewport area -// The scrollback (outside the viewport area) +// The entire page +// The scrollback (outside the page area) // Return Value: // - True if handled successfully. False otherwise. bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) @@ -783,7 +787,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // Scrollback clears erase everything in the "scrollback" of a *nix terminal // Everything that's scrolled off the screen so far. // Or if it's an Erase All, then we also need to handle that specially - // by moving the current contents of the viewport into the scrollback. + // by moving the current contents of the page into the scrollback. if (eraseType == DispatchTypes::EraseType::Scrollback) { return _EraseScrollback(); @@ -793,18 +797,18 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) return _EraseAll(); } - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + auto& textBuffer = page.Buffer(); + const auto pageWidth = page.Width(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The ED control is expected to reset the delayed wrap flag. // The special case variants above ("erase all" and "erase scrollback") // take care of that themselves when they set the cursor position. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); // When erasing the display, every line that is erased in full should be // reset to single width. When erasing to the end, this could include @@ -814,15 +818,15 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // the line is double width). if (eraseType == DispatchTypes::EraseType::FromBeginning) { - textBuffer.ResetLineRenditionRange(viewport.top, row); - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, row }, whitespace, eraseAttributes); - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); + textBuffer.ResetLineRenditionRange(page.Top(), row); + _FillRect(page, { 0, page.Top(), pageWidth, row }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); } if (eraseType == DispatchTypes::EraseType::ToEnd) { - textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, viewport.bottom); - _FillRect(textBuffer, { col, row, bufferWidth, row + 1 }, whitespace, eraseAttributes); - _FillRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }, whitespace, eraseAttributes); + textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, page.Bottom()); + _FillRect(page, { col, row, pageWidth, row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row + 1, pageWidth, page.Bottom() }, whitespace, eraseAttributes); } return true; @@ -836,24 +840,25 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // - True if handled successfully. False otherwise. bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The EL control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::ToEnd: - _FillRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::All: - _FillRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; default: return false; @@ -863,17 +868,17 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) // Routine Description: // - Selectively erases unprotected cells in an area of the buffer. // Arguments: -// - textBuffer - Target buffer to be erased. -// - eraseRect - Area of the buffer that will be affected. +// - page - Target page to be erased. +// - eraseRect - Area of the page that will be affected. // Return Value: // - -void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect) +void AdaptDispatch::_SelectiveEraseRect(const Page& page, const til::rect& eraseRect) { if (eraseRect) { for (auto row = eraseRect.top; row < eraseRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); + auto& rowBuffer = page.Buffer().GetMutableRowByOffset(row); for (auto col = eraseRect.left; col < eraseRect.right; col++) { // Only unprotected cells are affected. @@ -881,7 +886,7 @@ void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& { // The text is cleared but the attributes are left as is. rowBuffer.ClearCell(col); - textBuffer.TriggerRedraw(Viewport::FromCoord({ col, row })); + page.Buffer().TriggerRedraw(Viewport::FromCoord({ col, row })); } } } @@ -890,37 +895,36 @@ void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& } // Routine Description: -// - DECSED - Selectively erases unprotected cells in a portion of the viewport. +// - DECSED - Selectively erases unprotected cells in a portion of the page. // Arguments: // - eraseType - Determines whether to erase: // From beginning (top-left corner) to the cursor // From cursor to end (bottom-right corner) -// The entire viewport area +// The entire page area // Return Value: // - True if handled successfully. False otherwise. bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The DECSED control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _SelectiveEraseRect(textBuffer, { 0, viewport.top, bufferWidth, row }); - _SelectiveEraseRect(textBuffer, { 0, row, col + 1, row + 1 }); + _SelectiveEraseRect(page, { 0, page.Top(), pageWidth, row }); + _SelectiveEraseRect(page, { 0, row, col + 1, row + 1 }); return true; case DispatchTypes::EraseType::ToEnd: - _SelectiveEraseRect(textBuffer, { col, row, bufferWidth, row + 1 }); - _SelectiveEraseRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }); + _SelectiveEraseRect(page, { col, row, pageWidth, row + 1 }); + _SelectiveEraseRect(page, { 0, row + 1, pageWidth, page.Bottom() }); return true; case DispatchTypes::EraseType::All: - _SelectiveEraseRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }); + _SelectiveEraseRect(page, { 0, page.Top(), pageWidth, page.Bottom() }); return true; default: return false; @@ -938,23 +942,24 @@ bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType erase // - True if handled successfully. False otherwise. bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The DECSEL control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _SelectiveEraseRect(textBuffer, { 0, row, col + 1, row + 1 }); + _SelectiveEraseRect(page, { 0, row, col + 1, row + 1 }); return true; case DispatchTypes::EraseType::ToEnd: - _SelectiveEraseRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }); + _SelectiveEraseRect(page, { col, row, textBuffer.GetLineWidth(row), row + 1 }); return true; case DispatchTypes::EraseType::All: - _SelectiveEraseRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }); + _SelectiveEraseRect(page, { 0, row, textBuffer.GetLineWidth(row), row + 1 }); return true; default: return false; @@ -964,18 +969,18 @@ bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseTyp // Routine Description: // - Changes the attributes of each cell in a rectangular area of the buffer. // Arguments: -// - textBuffer - Target buffer to be changed. -// - changeRect - A rectangular area of the buffer that will be affected. +// - page - Target page to be changed. +// - changeRect - A rectangular area of the page that will be affected. // - changeOps - Changes that will be applied to each of the attributes. // Return Value: // - -void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps) +void AdaptDispatch::_ChangeRectAttributes(const Page& page, const til::rect& changeRect, const ChangeOps& changeOps) { if (changeRect) { for (auto row = changeRect.top; row < changeRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); + auto& rowBuffer = page.Buffer().GetMutableRowByOffset(row); for (auto col = changeRect.left; col < changeRect.right; col++) { auto attr = rowBuffer.GetAttrByColumn(col); @@ -998,7 +1003,7 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec rowBuffer.ReplaceAttributes(col, col + 1, attr); } } - textBuffer.TriggerRedraw(Viewport::FromExclusive(changeRect)); + page.Buffer().TriggerRedraw(Viewport::FromExclusive(changeRect)); _api.NotifyAccessibilityChange(changeRect); } } @@ -1014,16 +1019,15 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec // - void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps) { - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - const auto changeRect = _CalculateRectArea(changeArea.top, changeArea.left, changeArea.bottom, changeArea.right, bufferSize); + const auto page = _pages.ActivePage(); + const auto changeRect = _CalculateRectArea(page, changeArea.top, changeArea.left, changeArea.bottom, changeArea.right); const auto lineCount = changeRect.height(); // If the change extent is rectangular, we can apply the change with a // single call. The same is true for a stream extent that is only one line. if (_modes.test(Mode::RectangularChangeExtent) || lineCount == 1) { - _ChangeRectAttributes(textBuffer, changeRect, changeOps); + _ChangeRectAttributes(page, changeRect, changeOps); } // If the stream extent is more than one line we require three passes. The // top line is altered from the left offset up to the end of the line. The @@ -1032,10 +1036,10 @@ void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, c // must be greater than the left, otherwise the operation is ignored. else if (lineCount > 1 && changeRect.right > changeRect.left) { - const auto bufferWidth = bufferSize.width; - _ChangeRectAttributes(textBuffer, { changeRect.origin(), til::size{ bufferWidth - changeRect.left, 1 } }, changeOps); - _ChangeRectAttributes(textBuffer, { { 0, changeRect.top + 1 }, til::size{ bufferWidth, lineCount - 2 } }, changeOps); - _ChangeRectAttributes(textBuffer, { { 0, changeRect.bottom - 1 }, til::size{ changeRect.right, 1 } }, changeOps); + const auto pageWidth = page.Width(); + _ChangeRectAttributes(page, { changeRect.origin(), til::size{ pageWidth - changeRect.left, 1 } }, changeOps); + _ChangeRectAttributes(page, { { 0, changeRect.top + 1 }, til::size{ pageWidth, lineCount - 2 } }, changeOps); + _ChangeRectAttributes(page, { { 0, changeRect.bottom - 1 }, til::size{ changeRect.right, 1 } }, changeOps); } } @@ -1043,25 +1047,26 @@ void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, c // - Helper method to calculate the applicable buffer coordinates for use with // the various rectangular area operations. // Arguments: +// - page - The target page. // - top - The first row of the area. // - left - The first column of the area. // - bottom - The last row of the area (inclusive). // - right - The last column of the area (inclusive). -// - bufferSize - The size of the target buffer. // Return value: // - An exclusive rect with the absolute buffer coordinates. -til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize) +til::rect AdaptDispatch::_CalculateRectArea(const Page& page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - const auto viewport = _api.GetViewport(); + const auto pageWidth = page.Width(); + const auto pageHeight = page.Height(); // We start by calculating the margin offsets and maximum dimensions. - // If the origin mode isn't set, we use the viewport extent. - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, false); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferSize.width); + // If the origin mode isn't set, we use the page extent. + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, false); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); const auto yOffset = _modes.test(Mode::Origin) ? topMargin : 0; - const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : viewport.height(); + const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : pageHeight; const auto xOffset = _modes.test(Mode::Origin) ? leftMargin : 0; - const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : bufferSize.width; + const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : pageWidth; auto fillRect = til::inclusive_rect{}; fillRect.left = left + xOffset; @@ -1077,9 +1082,9 @@ til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, c fillRect.top = std::min(fillRect.top, yMaximum) - 1; fillRect.bottom = std::min(fillRect.bottom, yMaximum) - 1; - // To get absolute coordinates we offset with the viewport top. - fillRect.top += viewport.top; - fillRect.bottom += viewport.top; + // To get absolute coordinates we offset with the page top. + fillRect.top += page.Top(); + fillRect.bottom += page.Top(); return til::rect{ fillRect }; } @@ -1198,25 +1203,22 @@ bool AdaptDispatch::ReverseAttributesRectangularArea(const VTInt top, const VTIn // - left - The first column of the source area. // - bottom - The last row of the source area (inclusive). // - right - The last column of the source area (inclusive). -// - page - The source page number (unused for now). +// - page - The source page number. // - dstTop - The first row of the destination. // - dstLeft - The first column of the destination. -// - dstPage - The destination page number (unused for now). +// - dstPage - The destination page number. // Return Value: // - True. -bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt /*page*/, const VTInt dstTop, const VTInt dstLeft, const VTInt /*dstPage*/) +bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt page, const VTInt dstTop, const VTInt dstLeft, const VTInt dstPage) { - // GH#13892 We don't yet support the paging extension, so for now we ignore - // the page parameters. This is the same as if the maximum page count was 1. - - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - const auto srcRect = _CalculateRectArea(top, left, bottom, right, bufferSize); + const auto src = _pages.Get(page); + const auto dst = _pages.Get(dstPage); + const auto srcRect = _CalculateRectArea(src, top, left, bottom, right); const auto dstBottom = dstTop + srcRect.height() - 1; const auto dstRight = dstLeft + srcRect.width() - 1; - const auto dstRect = _CalculateRectArea(dstTop, dstLeft, dstBottom, dstRight, bufferSize); + const auto dstRect = _CalculateRectArea(dst, dstTop, dstLeft, dstBottom, dstRight); - if (dstRect && dstRect.origin() != srcRect.origin()) + if (dstRect && (dstRect.origin() != srcRect.origin() || src.Number() != dst.Number())) { // If the source is bigger than the available space at the destination // it needs to be clipped, so we only care about the destination size. @@ -1228,18 +1230,18 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const // Note that we read two cells from the source before we start writing // to the target, so a two-cell DBCS character can't accidentally delete // itself when moving one cell horizontally. - auto next = OutputCell(*textBuffer.GetCellDataAt(srcPos)); + auto next = OutputCell(*src.Buffer().GetCellDataAt(srcPos)); do { const auto current = next; const auto currentSrcPos = srcPos; srcView.WalkInBounds(srcPos, walkDirection); - next = OutputCell(*textBuffer.GetCellDataAt(srcPos)); + next = OutputCell(*src.Buffer().GetCellDataAt(srcPos)); // If the source position is offscreen (which can occur on double // width lines), then we shouldn't copy anything to the destination. - if (currentSrcPos.x < textBuffer.GetLineWidth(currentSrcPos.y)) + if (currentSrcPos.x < src.Buffer().GetLineWidth(currentSrcPos.y)) { - textBuffer.WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos); + dst.Buffer().WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos); } } while (dstView.WalkInBounds(dstPos, walkDirection)); _api.NotifyAccessibilityChange(dstRect); @@ -1261,8 +1263,8 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const // - True. bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto fillRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + const auto page = _pages.ActivePage(); + const auto fillRect = _CalculateRectArea(page, top, left, bottom, right); // The standard only allows for characters in the range of the GL and GR // character set tables, but we also support additional Unicode characters @@ -1274,8 +1276,8 @@ bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c if (glChar || grChar || unicodeChar) { const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast(charValue)); - const auto& fillAttributes = textBuffer.GetCurrentAttributes(); - _FillRect(textBuffer, fillRect, { &fillChar, 1 }, fillAttributes); + const auto& fillAttributes = page.Attributes(); + _FillRect(page, fillRect, { &fillChar, 1 }, fillAttributes); } return true; @@ -1293,10 +1295,10 @@ bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c // - True. bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto page = _pages.ActivePage(); + const auto eraseRect = _CalculateRectArea(page, top, left, bottom, right); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); return true; } @@ -1312,9 +1314,9 @@ bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, cons // - True. bool AdaptDispatch::SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); - _SelectiveEraseRect(textBuffer, eraseRect); + const auto page = _pages.ActivePage(); + const auto eraseRect = _CalculateRectArea(page, top, left, bottom, right); + _SelectiveEraseRect(page, eraseRect); return true; } @@ -1346,7 +1348,7 @@ bool AdaptDispatch::SelectAttributeChangeExtent(const DispatchTypes::ChangeExten // the buffer memory. // Arguments: // - id - a numeric label used to identify the request. -// - page - The page number (unused for now). +// - page - The page number. // - top - The first row of the area. // - left - The first column of the area. // - bottom - The last row of the area (inclusive). @@ -1359,7 +1361,9 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p // If this feature is not enabled, we'll just report a zero checksum. if constexpr (Feature_VtChecksumReport::IsEnabled()) { - if (page == 1) + // If the page number is 0, then we're meant to return a checksum of all + // of the pages, but we have no need for that, so we'll just return 0. + if (page != 0) { // As part of the checksum, we need to include the color indices of each // cell, and in the case of default colors, those indices come from the @@ -1370,8 +1374,8 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p defaultFgIndex = defaultFgIndex < 16 ? defaultFgIndex : 7; defaultBgIndex = defaultBgIndex < 16 ? defaultBgIndex : 0; - const auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + const auto target = _pages.Get(page); + const auto eraseRect = _CalculateRectArea(target, top, left, bottom, right); for (auto row = eraseRect.top; row < eraseRect.bottom; row++) { for (auto col = eraseRect.left; col < eraseRect.right; col++) @@ -1381,7 +1385,7 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p // predate Unicode, though, so we'd need a custom mapping table // to lookup the correct checksums. Considering this is only for // testing at the moment, that doesn't seem worth the effort. - const auto cell = textBuffer.GetCellDataAt({ col, row }); + const auto cell = target.Buffer().GetCellDataAt({ col, row }); for (auto ch : cell->Chars()) { // That said, I've made a special allowance for U+2426, @@ -1431,14 +1435,14 @@ bool AdaptDispatch::SetLineRendition(const LineRendition rendition) // The line rendition can't be changed if left/right margins are allowed. if (!_modes.test(Mode::AllowDECSLRM)) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - textBuffer.SetCurrentLineRendition(rendition, eraseAttributes); + const auto page = _pages.ActivePage(); + const auto eraseAttributes = _GetEraseAttributes(page); + page.Buffer().SetCurrentLineRendition(rendition, eraseAttributes); // There is some variation in how this was handled by the different DEC // terminals, but the STD 070 reference (on page D-13) makes it clear that // the delayed wrap (aka the Last Column Flag) was expected to be reset when // line rendition controls were executed. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); } return true; } @@ -1630,7 +1634,7 @@ void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const } // Routine Description: -// - CPR and DECXCPR- Reports the current cursor position within the viewport, +// - CPR and DECXCPR- Reports the current cursor position within the page, // as well as the current page number if this is an extended report. // Arguments: // - extendedReport - Set to true if the report should include the page number @@ -1638,32 +1642,30 @@ void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const // - void AdaptDispatch::_CursorPositionReport(const bool extendedReport) { - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); + const auto page = _pages.ActivePage(); // First pull the cursor position relative to the entire buffer out of the console. - til::point cursorPosition{ textBuffer.GetCursor().GetPosition() }; + til::point cursorPosition{ page.Cursor().GetPosition() }; - // Now adjust it for its position in respect to the current viewport top. - cursorPosition.y -= viewport.top; + // Now adjust it for its position in respect to the current page top. + cursorPosition.y -= page.Top(); - // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. + // NOTE: 1,1 is the top-left corner of the page in VT-speak, so add 1. cursorPosition.x++; cursorPosition.y++; // If the origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } // Now send it back into the input channel of the console. if (extendedReport) { - // An extended report should also include the page number, but for now - // we hard-code it to 1, since we don't yet support paging (GH#13892). - const auto pageNumber = 1; + // An extended report also includes the page number. + const auto pageNumber = page.Number(); const auto response = wil::str_printf(L"\x1b[?%d;%d;%dR", cursorPosition.y, cursorPosition.x, pageNumber); _api.ReturnResponse(response); } @@ -1711,12 +1713,10 @@ void AdaptDispatch::_MacroChecksumReport(const VTParameter id) const // - void AdaptDispatch::_ScrollMovement(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); + const auto page = _pages.ActivePage(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); } // Routine Description: @@ -1743,6 +1743,88 @@ bool AdaptDispatch::ScrollDown(const VTInt uiDistance) return true; } +// Routine Description: +// - NP - Moves the active position one or more pages ahead, and moves the +// cursor to home. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::NextPage(const VTInt pageCount) +{ + PagePositionRelative(pageCount); + return CursorPosition(1, 1); +} + +// Routine Description: +// - PP - Moves the active position one or more pages back, and moves the +// cursor to home. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PrecedingPage(const VTInt pageCount) +{ + PagePositionBack(pageCount); + return CursorPosition(1, 1); +} + +// Routine Description: +// - PPA - Moves the active position to the specified page number, without +// altering the cursor coordinates. +// Arguments: +// - page - Destination page +// Return Value: +// - True. +bool AdaptDispatch::PagePositionAbsolute(const VTInt page) +{ + _pages.MoveTo(page, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - PPR - Moves the active position one or more pages ahead, without altering +// the cursor coordinates. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PagePositionRelative(const VTInt pageCount) +{ + _pages.MoveRelative(pageCount, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - PPB - Moves the active position one or more pages back, without altering +// the cursor coordinates. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PagePositionBack(const VTInt pageCount) +{ + _pages.MoveRelative(-pageCount, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - DECRQDE - Requests the area of page memory that is currently visible. +// Arguments: +// - None +// Return Value: +// - True. +bool AdaptDispatch::RequestDisplayedExtent() +{ + const auto page = _pages.VisiblePage(); + const auto width = page.Viewport().width(); + const auto height = page.Viewport().height(); + const auto left = page.XPanOffset() + 1; + const auto top = page.YPanOffset() + 1; + _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{};{};{};{};{}\"w"), height, width, left, top, page.Number())); + return true; +} + // Routine Description: // - DECCOLM not only sets the number of columns, but also clears the screen buffer, // resets the page margins and origin mode, and places the cursor at 1,1 @@ -1755,10 +1837,10 @@ void AdaptDispatch::_SetColumnMode(const bool enable) // Only proceed if DECCOLM is allowed. Return true, as this is technically a successful handling. if (_modes.test(Mode::AllowDECCOLM) && !_api.IsConsolePty()) { - const auto viewport = _api.GetViewport(); - const auto viewportHeight = viewport.bottom - viewport.top; - const auto viewportWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); - _api.ResizeWindow(viewportWidth, viewportHeight); + const auto page = _pages.VisiblePage(); + const auto pageHeight = page.Height(); + const auto pageWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); + _api.ResizeWindow(pageWidth, pageHeight); _modes.set(Mode::Column, enable); _modes.reset(Mode::Origin, Mode::AllowDECSLRM); CursorPosition(1, 1); @@ -1781,8 +1863,8 @@ void AdaptDispatch::_SetAlternateScreenBufferMode(const bool enable) if (enable) { CursorSaveState(); - const auto& textBuffer = _api.GetTextBuffer(); - _api.UseAlternateScreenBuffer(_GetEraseAttributes(textBuffer)); + const auto page = _pages.ActivePage(); + _api.UseAlternateScreenBuffer(_GetEraseAttributes(page)); _usingAltBuffer = true; } else @@ -1860,21 +1942,28 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con // Resetting DECAWM should also reset the delayed wrap flag. if (!enable) { - _api.GetTextBuffer().GetCursor().ResetDelayEOLWrap(); + _pages.ActivePage().Cursor().ResetDelayEOLWrap(); } return true; case DispatchTypes::ModeParams::DECARM_AutoRepeatMode: _terminalInput.SetInputMode(TerminalInput::Mode::AutoRepeat, enable); return !_PassThroughInputModes(); case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(enable); + _pages.ActivePage().Cursor().SetBlinkingAllowed(enable); return !_api.IsConsolePty(); case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: - _api.GetTextBuffer().GetCursor().SetIsVisible(enable); + _pages.ActivePage().Cursor().SetIsVisible(enable); return true; case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: _modes.set(Mode::AllowDECCOLM, enable); return true; + case DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode: + _modes.set(Mode::PageCursorCoupling, enable); + if (enable) + { + _pages.MakeActivePageVisible(); + } + return true; case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode: _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, enable); return !_PassThroughInputModes(); @@ -1887,9 +1976,8 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con if (enable) { // If we've allowed left/right margins, we can't have line renditions. - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); + const auto page = _pages.ActivePage(); + page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom()); } return true; case DispatchTypes::ModeParams::DECECM_EraseColorMode: @@ -2011,10 +2099,10 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AutoRepeat); break; case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - enabled = _api.GetTextBuffer().GetCursor().IsBlinkingAllowed(); + enabled = _pages.ActivePage().Cursor().IsBlinkingAllowed(); break; case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: - enabled = _api.GetTextBuffer().GetCursor().IsVisible(); + enabled = _pages.ActivePage().Cursor().IsVisible(); break; case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: // DECCOLM is not supported in conpty mode @@ -2023,6 +2111,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) enabled = _modes.test(Mode::AllowDECCOLM); } break; + case DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode: + enabled = _modes.test(Mode::PageCursorCoupling); + break; case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode: enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Keypad); break; @@ -2100,20 +2191,17 @@ bool AdaptDispatch::SetKeypadMode(const bool fApplicationMode) // - void AdaptDispatch::_InsertDeleteLineHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto col = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin) { // We emulate inserting and deleting by scrolling the area between the cursor and the bottom margin. - _ScrollRectVertically(textBuffer, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta); + _ScrollRectVertically(page, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta); // The IL and DL controls are also expected to move the cursor to the left margin. cursor.SetXPosition(leftMargin); @@ -2161,20 +2249,17 @@ bool AdaptDispatch::DeleteLine(const VTInt distance) // - void AdaptDispatch::_InsertDeleteColumnHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - - const auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + const auto& cursor = page.Cursor(); const auto col = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin) { // We emulate inserting and deleting by scrolling the area between the cursor and the right margin. - _ScrollRectHorizontally(textBuffer, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); + _ScrollRectHorizontally(page, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); } } @@ -2246,23 +2331,23 @@ void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin, til::CoordType actualTop = topMargin; til::CoordType actualBottom = bottomMargin; - const auto viewport = _api.GetViewport(); - const auto screenHeight = viewport.bottom - viewport.top; + const auto page = _pages.ActivePage(); + const auto pageHeight = page.Height(); // The default top margin is line 1 if (actualTop == 0) { actualTop = 1; } - // The default bottom margin is the screen height + // The default bottom margin is the page height if (actualBottom == 0) { - actualBottom = screenHeight; + actualBottom = pageHeight; } // The top margin must be less than the bottom margin, and the - // bottom margin must be less than or equal to the screen height - if (actualTop < actualBottom && actualBottom <= screenHeight) + // bottom margin must be less than or equal to the page height + if (actualTop < actualBottom && actualBottom <= pageHeight) { - if (actualTop == 1 && actualBottom == screenHeight) + if (actualTop == 1 && actualBottom == pageHeight) { // Client requests setting margins to the entire screen // - clear them instead of setting them. @@ -2323,23 +2408,23 @@ void AdaptDispatch::_DoSetLeftRightScrollingMargins(const VTInt leftMargin, til::CoordType actualLeft = leftMargin; til::CoordType actualRight = rightMargin; - const auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); // The default left margin is column 1 if (actualLeft == 0) { actualLeft = 1; } - // The default right margin is the buffer width + // The default right margin is the page width if (actualRight == 0) { - actualRight = bufferWidth; + actualRight = pageWidth; } // The left margin must be less than the right margin, and the // right margin must be less than or equal to the buffer width - if (actualLeft < actualRight && actualRight <= bufferWidth) + if (actualLeft < actualRight && actualRight <= pageWidth) { - if (actualLeft == 1 && actualRight == bufferWidth) + if (actualLeft == 1 && actualRight == pageWidth) { // Client requests setting margins to the entire screen // - clear them instead of setting them. @@ -2416,20 +2501,20 @@ bool AdaptDispatch::CarriageReturn() // Routine Description: // - Helper method for executing a line feed, possibly preceded by carriage return. // Arguments: -// - textBuffer - Target buffer on which the line feed is executed. +// - page - Target page on which the line feed is executed. // - withReturn - Set to true if a carriage return should be performed as well. // - wrapForced - Set to true is the line feed was the result of the line wrapping. // Return Value: // - -void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced) +void AdaptDispatch::_DoLineFeed(const Page& page, const bool withReturn, const bool wrapForced) { - const auto viewport = _api.GetViewport(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto bufferHeight = textBuffer.GetSize().Height(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + auto& textBuffer = page.Buffer(); + const auto pageWidth = page.Width(); + const auto bufferHeight = page.BufferHeight(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); - auto& cursor = textBuffer.GetCursor(); + auto& cursor = page.Cursor(); const auto currentPosition = cursor.GetPosition(); auto newPosition = currentPosition; @@ -2451,36 +2536,36 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c { // If we're not at the bottom margin, or outside the horizontal margins, // then there's no scrolling, so we make sure we don't move past the - // bottom of the viewport. - newPosition.y = std::min(currentPosition.y + 1, viewport.bottom - 1); + // bottom of the page. + newPosition.y = std::min(currentPosition.y + 1, page.Bottom() - 1); newPosition = textBuffer.ClampPositionWithinLine(newPosition); } - else if (topMargin > viewport.top || leftMargin > 0 || rightMargin < bufferWidth - 1) + else if (topMargin > page.Top() || leftMargin > 0 || rightMargin < pageWidth - 1) { - // If the top margin isn't at the top of the viewport, or the + // If the top margin isn't at the top of the page, or the // horizontal margins are set, then we're just scrolling the margin // area and the cursor stays where it is. - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); } - else if (viewport.bottom < bufferHeight) + else if (page.Bottom() < bufferHeight) { - // If the top margin is at the top of the viewport, then we'll scroll + // If the top margin is at the top of the page, then we'll scroll // the content up by panning the viewport down, and also move the cursor // down a row. But we only do this if the viewport hasn't yet reached // the end of the buffer. - _api.SetViewportPosition({ viewport.left, viewport.top + 1 }); + _api.SetViewportPosition({ page.XPanOffset(), page.Top() + 1 }); newPosition.y++; - // And if the bottom margin didn't cover the full viewport, we copy the - // lower part of the viewport down so it remains static. But for a full + // And if the bottom margin didn't cover the full page, we copy the + // lower part of the page down so it remains static. But for a full // pan we reset the newly revealed row with the erase attributes. - if (bottomMargin < viewport.bottom - 1) + if (bottomMargin < page.Bottom() - 1) { - _ScrollRectVertically(textBuffer, { 0, bottomMargin + 1, bufferWidth, viewport.bottom + 1 }, 1); + _ScrollRectVertically(page, { 0, bottomMargin + 1, pageWidth, page.Bottom() + 1 }, 1); } else { - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); textBuffer.GetMutableRowByOffset(newPosition.y).Reset(eraseAttributes); } } @@ -2489,7 +2574,7 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // If the viewport has reached the end of the buffer, we can't pan down, // so we cycle the row coordinates, which effectively scrolls the buffer // content up. In this case we don't need to move the cursor down. - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); textBuffer.IncrementCircularBuffer(eraseAttributes); _api.NotifyBufferRotation(1); @@ -2499,11 +2584,11 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c cursor.SetIsOn(false); textBuffer.TriggerScroll({ 0, -1 }); - // And again, if the bottom margin didn't cover the full viewport, we - // copy the lower part of the viewport down so it remains static. - if (bottomMargin < viewport.bottom - 1) + // And again, if the bottom margin didn't cover the full page, we + // copy the lower part of the page down so it remains static. + if (bottomMargin < page.Bottom() - 1) { - _ScrollRectVertically(textBuffer, { 0, bottomMargin, bufferWidth, bufferHeight }, 1); + _ScrollRectVertically(page, { 0, bottomMargin, pageWidth, bufferHeight }, 1); } } @@ -2520,17 +2605,17 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // - True if handled successfully. False otherwise. bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType) { - auto& textBuffer = _api.GetTextBuffer(); + const auto page = _pages.ActivePage(); switch (lineFeedType) { case DispatchTypes::LineFeedType::DependsOnMode: - _DoLineFeed(textBuffer, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false); + _DoLineFeed(page, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false); return true; case DispatchTypes::LineFeedType::WithoutReturn: - _DoLineFeed(textBuffer, false, false); + _DoLineFeed(page, false, false); return true; case DispatchTypes::LineFeedType::WithReturn: - _DoLineFeed(textBuffer, true, false); + _DoLineFeed(page, true, false); return true; default: return false; @@ -2546,23 +2631,22 @@ bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType) // - True. bool AdaptDispatch::ReverseLineFeed() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the top of the margin area, we shift the buffer // contents down, to emulate inserting a line at that point. if (cursorPosition.y == topMargin && cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin) { - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); } - else if (cursorPosition.y > viewport.top) + else if (cursorPosition.y > page.Top()) { - // Otherwise we move the cursor up, but not past the top of the viewport. + // Otherwise we move the cursor up, but not past the top of the page. cursor.SetPosition(textBuffer.ClampPositionWithinLine({ cursorPosition.x, cursorPosition.y - 1 })); _ApplyCursorMovementFlags(cursor); } @@ -2578,18 +2662,16 @@ bool AdaptDispatch::ReverseLineFeed() // - True. bool AdaptDispatch::BackIndex() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the left of the margin area, we shift the buffer right. if (cursorPosition.x == leftMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) { - _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); + _ScrollRectHorizontally(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); } // Otherwise we move the cursor left, but not past the start of the line. else if (cursorPosition.x > 0) @@ -2609,21 +2691,19 @@ bool AdaptDispatch::BackIndex() // - True. bool AdaptDispatch::ForwardIndex() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the right of the margin area, we shift the buffer left. if (cursorPosition.x == rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) { - _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); + _ScrollRectHorizontally(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); } // Otherwise we move the cursor right, but not past the end of the line. - else if (cursorPosition.x < textBuffer.GetLineWidth(cursorPosition.y) - 1) + else if (cursorPosition.x < page.Buffer().GetLineWidth(cursorPosition.y) - 1) { cursor.SetXPosition(cursorPosition.x + 1); _ApplyCursorMovementFlags(cursor); @@ -2651,11 +2731,10 @@ bool AdaptDispatch::SetWindowTitle(std::wstring_view title) // - True. bool AdaptDispatch::HorizontalTabSet() { - const auto& textBuffer = _api.GetTextBuffer(); - const auto width = textBuffer.GetSize().Dimensions().width; - const auto column = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto column = page.Cursor().GetPosition().x; - _InitTabStopsForWidth(width); + _InitTabStopsForWidth(page.Width()); _tabStopColumns.at(column) = true; return true; @@ -2672,15 +2751,14 @@ bool AdaptDispatch::HorizontalTabSet() // - True. bool AdaptDispatch::ForwardTab(const VTInt numTabs) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); auto column = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto width = textBuffer.GetLineWidth(row); + const auto width = page.Buffer().GetLineWidth(row); auto tabsPerformed = 0; - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width); const auto clampToMargin = row >= topMargin && row <= bottomMargin && column <= rightMargin; const auto maxColumn = clampToMargin ? rightMargin : width - 1; @@ -2721,15 +2799,14 @@ bool AdaptDispatch::ForwardTab(const VTInt numTabs) // - True. bool AdaptDispatch::BackwardsTab(const VTInt numTabs) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); auto column = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto width = textBuffer.GetLineWidth(row); + const auto width = page.Buffer().GetLineWidth(row); auto tabsPerformed = 0; - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width); const auto clampToMargin = row >= topMargin && row <= bottomMargin && column >= leftMargin; const auto minColumn = clampToMargin ? leftMargin : 0; @@ -2780,11 +2857,10 @@ bool AdaptDispatch::TabClear(const DispatchTypes::TabClearType clearType) // - void AdaptDispatch::_ClearSingleTabStop() { - const auto& textBuffer = _api.GetTextBuffer(); - const auto width = textBuffer.GetSize().Dimensions().width; - const auto column = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto column = page.Cursor().GetPosition().x; - _InitTabStopsForWidth(width); + _InitTabStopsForWidth(page.Width()); _tabStopColumns.at(column) = false; } @@ -3021,7 +3097,7 @@ bool AdaptDispatch::AnnounceCodeStructure(const VTInt ansiLevel) // True if handled successfully. False otherwise. bool AdaptDispatch::SoftReset() { - _api.GetTextBuffer().GetCursor().SetIsVisible(true); // Cursor enabled. + _pages.ActivePage().Cursor().SetIsVisible(true); // Cursor enabled. // Replace mode; Absolute cursor addressing; Disallow left/right margins. _modes.reset(Mode::InsertReplace, Mode::Origin, Mode::AllowDECSLRM); @@ -3086,6 +3162,9 @@ bool AdaptDispatch::HardReset() _usingAltBuffer = false; } + // Reset all page buffers. + _pages.Reset(); + // Completely reset the TerminalOutput state. _termOutput = {}; if (_initialCodePage.has_value()) @@ -3126,7 +3205,7 @@ bool AdaptDispatch::HardReset() _api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, false); // Restore cursor blinking mode. - _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(true); + _pages.ActivePage().Cursor().SetBlinkingAllowed(true); // Delete all current tab stops and reapply TabSet(DispatchTypes::TabSetType::SetEvery8Columns); @@ -3136,7 +3215,7 @@ bool AdaptDispatch::HardReset() _fontBuffer = nullptr; // Reset internal modes to their initial state - _modes = {}; + _modes = { Mode::PageCursorCoupling }; // Clear and release the macro buffer. if (_macroBuffer) @@ -3172,18 +3251,16 @@ bool AdaptDispatch::HardReset() // - True. bool AdaptDispatch::ScreenAlignmentPattern() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Dimensions().width; + const auto page = _pages.ActivePage(); // Fill the screen with the letter E using the default attributes. - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }, L"E", {}); + _FillRect(page, { 0, page.Top(), page.Width(), page.Bottom() }, L"E", {}); // Reset the line rendition for all of these rows. - textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); + page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom()); // Reset the meta/extended attributes (but leave the colors unchanged). - auto attr = textBuffer.GetCurrentAttributes(); + auto attr = page.Attributes(); attr.SetStandardErase(); - _api.SetTextAttributes(attr); + page.SetAttributes(attr); // Reset the origin mode to absolute, and disallow left/right margins. _modes.reset(Mode::Origin, Mode::AllowDECSLRM); // Clear the scrolling margins. @@ -3198,8 +3275,8 @@ bool AdaptDispatch::ScreenAlignmentPattern() //Routine Description: // - Erase Scrollback (^[[3J - ED extension by xterm) // Because conhost doesn't exactly have a scrollback, We have to be tricky here. -// We need to move the entire viewport to 0,0, and clear everything outside -// (0, 0, viewportWidth, viewportHeight) To give the appearance that +// We need to move the entire page to 0,0, and clear everything outside +// (0, 0, pageWidth, pageHeight) To give the appearance that // everything above the viewport was cleared. // We don't want to save the text BELOW the viewport, because in *nix, there isn't anything there // (There isn't a scroll-forward, only a scrollback) @@ -3209,19 +3286,15 @@ bool AdaptDispatch::ScreenAlignmentPattern() // - True if handled successfully. False otherwise. bool AdaptDispatch::_EraseScrollback() { - const auto viewport = _api.GetViewport(); - const auto top = viewport.top; - const auto height = viewport.bottom - viewport.top; - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.VisiblePage(); + auto& cursor = page.Cursor(); const auto row = cursor.GetPosition().y; - textBuffer.ClearScrollback(top, height); + page.Buffer().ClearScrollback(page.Top(), page.Height()); // Move the viewport - _api.SetViewportPosition({ viewport.left, 0 }); + _api.SetViewportPosition({ page.XPanOffset(), 0 }); // Move the cursor to the same relative location. - cursor.SetYPosition(row - top); + cursor.SetYPosition(row - page.Top()); cursor.SetHasMoved(true); // GH#2715 - If this succeeded, but we're in a conpty, return `false` to @@ -3245,25 +3318,26 @@ bool AdaptDispatch::_EraseScrollback() // - True if handled successfully. False otherwise. bool AdaptDispatch::_EraseAll() { - const auto viewport = _api.GetViewport(); - const auto viewportHeight = viewport.bottom - viewport.top; - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize(); + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); + const auto pageHeight = page.Height(); + const auto bufferHeight = page.BufferHeight(); + auto& textBuffer = page.Buffer(); const auto inPtyMode = _api.IsConsolePty(); - // Stash away the current position of the cursor within the viewport. + // Stash away the current position of the cursor within the page. // We'll need to restore the cursor to that same relative position, after // we move the viewport. - auto& cursor = textBuffer.GetCursor(); - const auto row = cursor.GetPosition().y - viewport.top; + auto& cursor = page.Cursor(); + const auto row = cursor.GetPosition().y - page.Top(); - // Calculate new viewport position. Typically we want to move one line below + // Calculate new page position. Typically we want to move one line below // the last non-space row, but if the last non-space character is the very // start of the buffer, then we shouldn't move down at all. const auto lastChar = textBuffer.GetLastNonSpaceCharacter(); - auto newViewportTop = lastChar == til::point{} ? 0 : lastChar.y + 1; - auto newViewportBottom = newViewportTop + viewportHeight; - const auto delta = newViewportBottom - (bufferSize.Height()); + auto newPageTop = lastChar == til::point{} ? 0 : lastChar.y + 1; + auto newPageBottom = newPageTop + pageHeight; + const auto delta = newPageBottom - bufferHeight; if (delta > 0) { for (auto i = 0; i < delta; i++) @@ -3271,8 +3345,8 @@ bool AdaptDispatch::_EraseAll() textBuffer.IncrementCircularBuffer(); } _api.NotifyBufferRotation(delta); - newViewportTop -= delta; - newViewportBottom -= delta; + newPageTop -= delta; + newPageBottom -= delta; // We don't want to trigger a scroll in pty mode, because we're going to // pass through the ED sequence anyway, and this will just result in the // buffer being scrolled up by two pages instead of one. @@ -3281,26 +3355,31 @@ bool AdaptDispatch::_EraseAll() textBuffer.TriggerScroll({ 0, -delta }); } } - // Move the viewport - _api.SetViewportPosition({ viewport.left, newViewportTop }); + // Move the viewport if necessary. + if (newPageTop != page.Top()) + { + _api.SetViewportPosition({ page.XPanOffset(), newPageTop }); + } // Restore the relative cursor position - cursor.SetYPosition(row + newViewportTop); + cursor.SetYPosition(row + newPageTop); cursor.SetHasMoved(true); - // Erase all the rows in the current viewport. - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, { 0, newViewportTop, bufferSize.Width(), newViewportBottom }, whitespace, eraseAttributes); + // Erase all the rows in the current page. + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, { 0, newPageTop, pageWidth, newPageBottom }, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. - textBuffer.ResetLineRenditionRange(newViewportTop, newViewportBottom); + textBuffer.ResetLineRenditionRange(newPageTop, newPageBottom); // GH#5683 - If this succeeded, but we're in a conpty, return `false` to // make the state machine propagate this ED sequence to the connected // terminal application. While we're in conpty mode, when the client // requests a Erase All operation, we need to manually tell the // connected terminal to do the same thing, so that the terminal will - // move it's own buffer contents into the scrollback. - return !inPtyMode; + // move it's own buffer contents into the scrollback. But this only + // applies if we're in the active buffer, since this should have no + // visible effect for an inactive buffer. + return !(inPtyMode && textBuffer.IsActiveBuffer()); } //Routine Description: @@ -3353,7 +3432,7 @@ bool AdaptDispatch::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) return false; } - auto& cursor = _api.GetTextBuffer().GetCursor(); + auto& cursor = _pages.ActivePage().Cursor(); cursor.SetType(actualType); cursor.SetBlinkingAllowed(fEnableBlinking); @@ -3515,14 +3594,17 @@ bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy _api.ShowWindow(false); return true; case DispatchTypes::WindowManipulationType::RefreshWindow: - _api.GetTextBuffer().TriggerRedrawAll(); + _pages.VisiblePage().Buffer().TriggerRedrawAll(); return true; case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: _api.ResizeWindow(parameter2.value_or(0), parameter1.value_or(0)); return true; case DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters: - _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[8;{};{}t"), _api.GetViewport().height(), _api.GetTextBuffer().GetSize().Width())); + { + const auto page = _pages.VisiblePage(); + _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[8;{};{}t"), page.Height(), page.Width())); return true; + } default: return false; } @@ -3536,12 +3618,12 @@ bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy // - true bool AdaptDispatch::AddHyperlink(const std::wstring_view uri, const std::wstring_view params) { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); - const auto id = textBuffer.GetHyperlinkId(uri, params); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); + const auto id = page.Buffer().GetHyperlinkId(uri, params); attr.SetHyperlinkId(id); - textBuffer.SetCurrentAttributes(attr); - textBuffer.AddHyperlinkToMap(uri, id); + page.SetAttributes(attr); + page.Buffer().AddHyperlinkToMap(uri, id); return true; } @@ -3551,10 +3633,10 @@ bool AdaptDispatch::AddHyperlink(const std::wstring_view uri, const std::wstring // - true bool AdaptDispatch::EndHyperlink() { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetHyperlinkId(0); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); return true; } @@ -3657,7 +3739,7 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view string) // This seems like basically the same as 133;B - the end of the prompt, the start of the commandline. else if (subParam == 12) { - _api.GetTextBuffer().StartCommand(); + _pages.ActivePage().Buffer().StartCommand(); return true; } @@ -3701,7 +3783,7 @@ bool AdaptDispatch::DoITerm2Action(const std::wstring_view string) bool handled = false; if (action == L"SetMark") { - _api.GetTextBuffer().StartPrompt(); + _pages.ActivePage().Buffer().StartPrompt(); handled = true; } @@ -3747,19 +3829,19 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) { case L'A': // FTCS_PROMPT { - _api.GetTextBuffer().StartPrompt(); + _pages.ActivePage().Buffer().StartPrompt(); handled = true; break; } case L'B': // FTCS_COMMAND_START { - _api.GetTextBuffer().StartCommand(); + _pages.ActivePage().Buffer().StartCommand(); handled = true; break; } case L'C': // FTCS_COMMAND_EXECUTED { - _api.GetTextBuffer().StartOutput(); + _pages.ActivePage().Buffer().StartOutput(); handled = true; break; } @@ -3780,7 +3862,7 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) UINT_MAX; } - _api.GetTextBuffer().EndCurrentCommand(error); + _pages.ActivePage().Buffer().EndCurrentCommand(error); handled = true; break; @@ -4270,7 +4352,7 @@ void AdaptDispatch::_ReportSGRSetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r0"sv); - const auto& attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& attr = _pages.ActivePage().Attributes(); const auto ulStyle = attr.GetUnderlineStyle(); // For each boolean attribute that is set, we add the appropriate // parameter value to the response string. @@ -4339,8 +4421,8 @@ void AdaptDispatch::_ReportDECSTBMSetting() fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto viewport = _api.GetViewport(); - const auto [marginTop, marginBottom] = _GetVerticalMargins(viewport, false); + const auto page = _pages.ActivePage(); + const auto [marginTop, marginBottom] = _GetVerticalMargins(page, false); // VT origin is at 1,1 so we need to add 1 to these margins. fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginTop + 1, marginBottom + 1); @@ -4363,8 +4445,8 @@ void AdaptDispatch::_ReportDECSLRMSetting() fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto bufferWidth = _api.GetTextBuffer().GetSize().Width(); - const auto [marginLeft, marginRight] = _GetHorizontalMargins(bufferWidth); + const auto pageWidth = _pages.ActivePage().Width(); + const auto [marginLeft, marginRight] = _GetHorizontalMargins(pageWidth); // VT origin is at 1,1 so we need to add 1 to these margins. fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginLeft + 1, marginRight + 1); @@ -4387,7 +4469,7 @@ void AdaptDispatch::_ReportDECSCASetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto& attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& attr = _pages.ActivePage().Attributes(); response.append(attr.IsProtected() ? L"1"sv : L"0"sv); // The '"q' indicates this is an DECSCA response, and ST ends the sequence. @@ -4505,31 +4587,27 @@ ITermDispatch::StringHandler AdaptDispatch::RestorePresentationState(const Dispa // - None void AdaptDispatch::_ReportCursorInformation() { - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + const auto& cursor = page.Cursor(); + const auto& attributes = page.Attributes(); // First pull the cursor position relative to the entire buffer out of the console. til::point cursorPosition{ cursor.GetPosition() }; - // Now adjust it for its position in respect to the current viewport top. - cursorPosition.y -= viewport.top; + // Now adjust it for its position in respect to the current page top. + cursorPosition.y -= page.Top(); - // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. + // NOTE: 1,1 is the top-left corner of the page in VT-speak, so add 1. cursorPosition.x++; cursorPosition.y++; // If the origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } - // Paging is not supported yet (GH#13892). - const auto pageNumber = 1; - // Only some of the rendition attributes are reported. // Bit Attribute // 1 bold @@ -4575,7 +4653,7 @@ void AdaptDispatch::_ReportCursorInformation() FMT_COMPILE(L"\033P1$u{};{};{};{};{};{};{};{};{};{}{}{}{}\033\\"), cursorPosition.y, cursorPosition.x, - pageNumber, + page.Number(), renditionAttributes, characterAttributes, flags, @@ -4612,7 +4690,6 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() VTParameter row{}; VTParameter column{}; }; - auto& textBuffer = _api.GetTextBuffer(); return [&, state = State{}](const auto ch) mutable { if (numeric.test(state.field)) { @@ -4634,7 +4711,7 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() } else if (state.field == Field::Page) { - // Paging is not supported yet (GH#13892). + PagePositionAbsolute(state.value); } else if (state.field == Field::GL && state.value <= 3) { @@ -4659,19 +4736,21 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() state.value = ch; if (state.field == Field::SGR) { - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetIntense(state.value & 1); attr.SetUnderlineStyle(state.value & 2 ? UnderlineStyle::SinglyUnderlined : UnderlineStyle::NoUnderline); attr.SetBlinking(state.value & 4); attr.SetReverseVideo(state.value & 8); attr.SetInvisible(state.value & 16); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); } else if (state.field == Field::Attr) { - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetProtected(state.value & 1); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); } else if (state.field == Field::Sizes) { @@ -4697,7 +4776,8 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() // above, so we only need to worry about setting it. if (delayedEOLWrap) { - textBuffer.GetCursor().DelayEOLWrap(); + const auto page = _pages.ActivePage(); + page.Cursor().DelayEOLWrap(); } } } @@ -4744,7 +4824,7 @@ void AdaptDispatch::_ReportTabStops() // In order to be compatible with the original hardware terminals, we only // report tab stops up to the current buffer width, even though there may // be positions recorded beyond that limit. - const auto width = _api.GetTextBuffer().GetSize().Dimensions().width; + const auto width = _pages.ActivePage().Width(); _InitTabStopsForWidth(width); using namespace std::string_view_literals; @@ -4780,7 +4860,7 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops() // In order to be compatible with the original hardware terminals, we need // to be able to set tab stops up to at least 132 columns, even though the // current buffer width may be less than that. - const auto width = std::max(_api.GetTextBuffer().GetSize().Dimensions().width, 132); + const auto width = std::max(_pages.ActivePage().Width(), 132); _ClearAllTabStops(); _InitTabStopsForWidth(width); diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 36bb0312145..dbea2fcd887 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -18,6 +18,7 @@ Author(s): #include "ITerminalApi.hpp" #include "FontBuffer.hpp" #include "MacroBuffer.hpp" +#include "PageManager.hpp" #include "terminalOutput.hpp" #include "../input/terminalInput.hpp" #include "../../types/inc/sgrStack.hpp" @@ -81,6 +82,12 @@ namespace Microsoft::Console::VirtualTerminal bool RequestTerminalParameters(const DispatchTypes::ReportingPermission permission) override; // DECREQTPARM bool ScrollUp(const VTInt distance) override; // SU bool ScrollDown(const VTInt distance) override; // SD + bool NextPage(const VTInt pageCount) override; // NP + bool PrecedingPage(const VTInt pageCount) override; // PP + bool PagePositionAbsolute(const VTInt page) override; // PPA + bool PagePositionRelative(const VTInt pageCount) override; // PPR + bool PagePositionBack(const VTInt pageCount) override; // PPB + bool RequestDisplayedExtent() override; // DECRQDE bool InsertLine(const VTInt distance) override; // IL bool DeleteLine(const VTInt distance) override; // DL bool InsertColumn(const VTInt distance) override; // DECIC @@ -178,7 +185,8 @@ namespace Microsoft::Console::VirtualTerminal AllowDECCOLM, AllowDECSLRM, EraseColor, - RectangularChangeExtent + RectangularChangeExtent, + PageCursorCoupling }; enum class ScrollDirection { @@ -189,6 +197,7 @@ namespace Microsoft::Console::VirtualTerminal { VTInt Row = 1; VTInt Column = 1; + VTInt Page = 1; bool IsDelayedEOLWrap = false; bool IsOriginModeRelative = false; TextAttribute Attributes = {}; @@ -214,20 +223,20 @@ namespace Microsoft::Console::VirtualTerminal }; void _WriteToBuffer(const std::wstring_view string); - std::pair _GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept; + std::pair _GetVerticalMargins(const Page& page, const bool absolute) noexcept; std::pair _GetHorizontalMargins(const til::CoordType bufferWidth) noexcept; bool _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins); void _ApplyCursorMovementFlags(Cursor& cursor) noexcept; - void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const; - void _SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect); - void _ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps); + void _FillRect(const Page& page, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const; + void _SelectiveEraseRect(const Page& page, const til::rect& eraseRect); + void _ChangeRectAttributes(const Page& page, const til::rect& changeRect, const ChangeOps& changeOps); void _ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps); - til::rect _CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize); + til::rect _CalculateRectArea(const Page& page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right); bool _EraseScrollback(); bool _EraseAll(); - TextAttribute _GetEraseAttributes(const TextBuffer& textBuffer) const noexcept; - void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); - void _ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); + TextAttribute _GetEraseAttributes(const Page& page) const noexcept; + void _ScrollRectVertically(const Page& page, const til::rect& scrollRect, const VTInt delta); + void _ScrollRectHorizontally(const Page& page, const til::rect& scrollRect, const VTInt delta); void _InsertDeleteCharacterHelper(const VTInt delta); void _InsertDeleteLineHelper(const VTInt delta); void _InsertDeleteColumnHelper(const VTInt delta); @@ -240,7 +249,7 @@ namespace Microsoft::Console::VirtualTerminal const VTInt rightMargin, const bool homeCursor = false); - void _DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced); + void _DoLineFeed(const Page& page, const bool withReturn, const bool wrapForced); void _DeviceStatusReport(const wchar_t* parameters) const; void _CursorPositionReport(const bool extendedReport); @@ -281,6 +290,7 @@ namespace Microsoft::Console::VirtualTerminal RenderSettings& _renderSettings; TerminalInput& _terminalInput; TerminalOutput _termOutput; + PageManager _pages; std::unique_ptr _fontBuffer; std::shared_ptr _macroBuffer; std::optional _initialCodePage; @@ -295,7 +305,7 @@ namespace Microsoft::Console::VirtualTerminal til::inclusive_rect _scrollMargins; - til::enumset _modes; + til::enumset _modes{ Mode::PageCursorCoupling }; SgrStack _sgrStack; diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index ac1b0180eff..f7dab41cd1c 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -422,9 +422,10 @@ void AdaptDispatch::_ApplyGraphicsOptions(const VTParameters options, // - True. bool AdaptDispatch::SetGraphicsRendition(const VTParameters options) { - auto attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); _ApplyGraphicsOptions(options, attr); - _api.SetTextAttributes(attr); + page.SetAttributes(attr, &_api); return true; } @@ -438,8 +439,8 @@ bool AdaptDispatch::SetGraphicsRendition(const VTParameters options) // - True. bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); for (size_t i = 0; i < options.size(); i++) { const LogicalAttributeOptions opt = options.at(i); @@ -456,7 +457,7 @@ bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) break; } } - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); return true; } @@ -470,7 +471,7 @@ bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) // - True. bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) { - const auto& currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& currentAttributes = _pages.ActivePage().Attributes(); _sgrStack.Push(currentAttributes, options); return true; } @@ -484,7 +485,8 @@ bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) // - True. bool AdaptDispatch::PopGraphicsRendition() { - const auto& currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); - _api.SetTextAttributes(_sgrStack.Pop(currentAttributes)); + const auto page = _pages.ActivePage(); + const auto& currentAttributes = page.Attributes(); + page.SetAttributes(_sgrStack.Pop(currentAttributes), &_api); return true; } diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index 2780c818dfa..a05c8e5b83e 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -15,6 +15,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 798522c651f..501f6ce9bdc 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -36,6 +36,9 @@ Source Files + + Source Files + @@ -74,6 +77,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index a7a07a29814..3ffd6bed7c7 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -34,6 +34,7 @@ SOURCES= \ ..\FontBuffer.cpp \ ..\InteractDispatch.cpp \ ..\MacroBuffer.cpp \ + ..\PageManager.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 82bb4f21320..3a1d19a0964 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -42,6 +42,12 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool DeleteCharacter(const VTInt /*count*/) override { return false; } // DCH bool ScrollUp(const VTInt /*distance*/) override { return false; } // SU bool ScrollDown(const VTInt /*distance*/) override { return false; } // SD + bool NextPage(const VTInt /*pageCount*/) override { return false; } // NP + bool PrecedingPage(const VTInt /*pageCount*/) override { return false; } // PP + bool PagePositionAbsolute(const VTInt /*page*/) override { return false; } // PPA + bool PagePositionRelative(const VTInt /*pageCount*/) override { return false; } // PPR + bool PagePositionBack(const VTInt /*pageCount*/) override { return false; } // PPB + bool RequestDisplayedExtent() override { return false; } // DECRQDE bool InsertLine(const VTInt /*distance*/) override { return false; } // IL bool DeleteLine(const VTInt /*distance*/) override { return false; } // DL bool InsertColumn(const VTInt /*distance*/) override { return false; } // DECIC diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 5cd5fbdc29e..c0ff50e5748 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -81,14 +81,10 @@ class TestGetSet final : public ITerminalApi return *_stateMachine; } - TextBuffer& GetTextBuffer() override + BufferState GetBufferAndViewport() override { - return *_textBuffer.get(); - } - - til::rect GetViewport() const override - { - return { _viewport.left, _viewport.top, _viewport.right, _viewport.bottom }; + const auto viewport = til::rect{ _viewport.left, _viewport.top, _viewport.right, _viewport.bottom }; + return { *_textBuffer.get(), viewport, true }; } void SetViewportPosition(const til::point /*position*/) override @@ -1575,14 +1571,23 @@ class AdapterTest coordCursorExpected.x++; coordCursorExpected.y++; - // Until we support paging (GH#13892) the reported page number should always be 1. - const auto pageExpected = 1; + // By default, the initial page number should be 1. + auto pageExpected = 1; VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExtendedCursorPositionReport, {})); wchar_t pwszBuffer[50]; swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.y, coordCursorExpected.x, pageExpected); _testGetSet->ValidateInputEvent(pwszBuffer); + + // Now test with the page number set to 3. + pageExpected = 3; + _pDispatch->PagePositionAbsolute(pageExpected); + + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExtendedCursorPositionReport, {})); + + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.y, coordCursorExpected.x, pageExpected); + _testGetSet->ValidateInputEvent(pwszBuffer); } TEST_METHOD(DeviceStatus_MacroSpaceReportTest) @@ -1746,6 +1751,42 @@ class AdapterTest VERIFY_THROWS(_pDispatch->TertiaryDeviceAttributes(), std::exception); } + TEST_METHOD(RequestDisplayedExtentTests) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Verify DECRQDE response in home position"); + _testGetSet->PrepData(); + _testGetSet->_viewport.left = 0; + _testGetSet->_viewport.right = 80; + _testGetSet->_viewport.top = 0; + _testGetSet->_viewport.bottom = 24; + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;1;1;1\"w"); + + Log::Comment(L"Test 2: Verify DECRQDE response when panned horizontally"); + _testGetSet->_viewport.left += 5; + _testGetSet->_viewport.right += 5; + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;1\"w"); + + Log::Comment(L"Test 3: Verify DECRQDE response on page 3"); + _pDispatch->PagePositionAbsolute(3); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;3\"w"); + + Log::Comment(L"Test 3: Verify DECRQDE response when active page not visible"); + _pDispatch->ResetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(1); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;3\"w"); + + Log::Comment(L"Test 4: Verify DECRQDE response when page 1 visible again"); + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;1\"w"); + } + TEST_METHOD(RequestTerminalParametersTests) { Log::Comment(L"Starting test..."); @@ -3263,7 +3304,7 @@ class AdapterTest setMacroText(63, L"Macro 63"); const auto getBufferOutput = [&]() { - const auto& textBuffer = _testGetSet->GetTextBuffer(); + const auto& textBuffer = _testGetSet->GetBufferAndViewport().buffer; const auto cursorPos = textBuffer.GetCursor().GetPosition(); return textBuffer.GetRowByOffset(cursorPos.y).GetText().substr(0, cursorPos.x); }; @@ -3314,7 +3355,8 @@ class AdapterTest { _testGetSet->PrepData(); _pDispatch->WindowManipulation(DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters, NULL, NULL); - const std::wstring expectedResponse = fmt::format(L"\033[8;{};{}t", _testGetSet->GetViewport().height(), _testGetSet->GetTextBuffer().GetSize().Width()); + const auto [textBuffer, viewport, _] = _testGetSet->GetBufferAndViewport(); + const std::wstring expectedResponse = fmt::format(L"\033[8;{};{}t", viewport.height(), textBuffer.GetSize().Width()); _testGetSet->ValidateInputEvent(expectedResponse.c_str()); } @@ -3345,6 +3387,89 @@ class AdapterTest VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;30;{ "foo": "what;ever", "bar": 2 })")); } + TEST_METHOD(PageMovementTests) + { + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + auto& pages = _pDispatch->_pages; + const auto startPos = pages.ActivePage().Cursor().GetPosition(); + const auto homePos = til::point{ 0, pages.ActivePage().Top() }; + + // Testing PPA (page position absolute) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->PagePositionAbsolute(3); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPA 3 moves to page 3"); + _pDispatch->PagePositionAbsolute(VTParameter{}); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PPA with omitted page moves to 1"); + _pDispatch->PagePositionAbsolute(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"PPA is clamped at page 6"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + _pDispatch->PagePositionAbsolute(1); // Reset to page 1 + + // Testing PPR (page position relative) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->PagePositionRelative(2); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPR 2 moves forward 2 pages"); + _pDispatch->PagePositionRelative(VTParameter{}); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PPR with omitted count moves forward 1"); + _pDispatch->PagePositionRelative(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"PPR is clamped at page 6"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing PPB (page position back) + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"Initial page is 6"); + _pDispatch->PagePositionBack(2); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PPB 2 moves back 2 pages"); + _pDispatch->PagePositionBack(VTParameter{}); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPB with omitted count moves back 1"); + _pDispatch->PagePositionBack(9999); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PPB is clamped at page 1"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing NP (next page) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->NextPage(2); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"NP 2 moves forward 2 pages"); + _pDispatch->NextPage(VTParameter{}); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"NP with omitted count moves forward 1"); + _pDispatch->NextPage(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"NP is clamped at page 6"); + VERIFY_ARE_EQUAL(homePos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position is reset to home"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing PP (preceding page) + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"Initial page is 6"); + _pDispatch->PrecedingPage(2); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PP 2 moves back 2 pages"); + _pDispatch->PrecedingPage(VTParameter{}); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PP with omitted count moves back 1"); + _pDispatch->PrecedingPage(9999); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PP is clamped at page 1"); + VERIFY_ARE_EQUAL(homePos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position is reset to home"); + + // Testing DECPCCM (page cursor coupling mode) + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(2); + VERIFY_ARE_EQUAL(2, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(2, pages.VisiblePage().Number(), L"Visible page should follow active if DECPCCM set"); + _pDispatch->ResetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(4); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(2, pages.VisiblePage().Number(), L"Visible page should not change if DECPCCM reset"); + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(4, pages.VisiblePage().Number(), L"Active page should become visible when DECPCCM set"); + + // Reset to page 1 + _pDispatch->PagePositionAbsolute(1); + } + private: TerminalInput _terminalInput; std::unique_ptr _testGetSet; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index f4b3fcbf677..7568f154677 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -556,6 +556,12 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete case CsiActionCodes::SD_ScrollDown: success = _dispatch->ScrollDown(parameters.at(0)); break; + case CsiActionCodes::NP_NextPage: + success = _dispatch->NextPage(parameters.at(0)); + break; + case CsiActionCodes::PP_PrecedingPage: + success = _dispatch->PrecedingPage(parameters.at(0)); + break; case CsiActionCodes::ANSISYSRC_CursorRestore: success = _dispatch->CursorRestoreState(); break; @@ -601,6 +607,15 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete } success = true; break; + case CsiActionCodes::PPA_PagePositionAbsolute: + success = _dispatch->PagePositionAbsolute(parameters.at(0)); + break; + case CsiActionCodes::PPR_PagePositionRelative: + success = _dispatch->PagePositionRelative(parameters.at(0)); + break; + case CsiActionCodes::PPB_PagePositionBack: + success = _dispatch->PagePositionBack(parameters.at(0)); + break; case CsiActionCodes::DECSCUSR_SetCursorStyle: success = _dispatch->SetCursorStyle(parameters.at(0)); break; @@ -610,6 +625,9 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete case CsiActionCodes::DECSCA_SetCharacterProtectionAttribute: success = _dispatch->SetCharacterProtectionAttribute(parameters); break; + case CsiActionCodes::DECRQDE_RequestDisplayedExtent: + success = _dispatch->RequestDisplayedExtent(); + break; case CsiActionCodes::XT_PushSgr: case CsiActionCodes::XT_PushSgrAlias: success = _dispatch->PushGraphicsRendition(parameters); diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 64fbf81503e..0970e45dee2 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -122,6 +122,8 @@ namespace Microsoft::Console::VirtualTerminal DCH_DeleteCharacter = VTID("P"), SU_ScrollUp = VTID("S"), SD_ScrollDown = VTID("T"), + NP_NextPage = VTID("U"), + PP_PrecedingPage = VTID("V"), DECST8C_SetTabEvery8Columns = VTID("?W"), ECH_EraseCharacters = VTID("X"), CBT_CursorBackTab = VTID("Z"), @@ -147,9 +149,13 @@ namespace Microsoft::Console::VirtualTerminal DTTERM_WindowManipulation = VTID("t"), // NOTE: Overlaps with DECSLPP. Fix when/if implemented. ANSISYSRC_CursorRestore = VTID("u"), DECREQTPARM_RequestTerminalParameters = VTID("x"), + PPA_PagePositionAbsolute = VTID(" P"), + PPR_PagePositionRelative = VTID(" Q"), + PPB_PagePositionBack = VTID(" R"), DECSCUSR_SetCursorStyle = VTID(" q"), DECSTR_SoftReset = VTID("!p"), DECSCA_SetCharacterProtectionAttribute = VTID("\"q"), + DECRQDE_RequestDisplayedExtent = VTID("\"v"), XT_PushSgrAlias = VTID("#p"), XT_PopSgrAlias = VTID("#q"), XT_PushSgr = VTID("#{"),