Add support for VT paging operations (#16615)

This PR adds support for multiples pages in the VT architecture, along
with new operations for moving between those pages: `NP` (Next Page),
`PP` (Preceding Page), `PPA` (Page Position Absolute), `PPR` (Page
Position Relative), and `PPB` (Page Position Back).

There's also a new mode, `DECPCCM` (Page Cursor Coupling Mode), which
determines whether or not the active page is also the visible page, and
a new query sequence, `DECRQDE` (Request Displayed Extent), which can be
used to query the visible page.

## References and Relevant Issues

When combined with `DECCRA` (Copy Rectangular Area), which can copy
between pages, you can layer content on top of existing output, and
still restore the original data afterwards. So this could serve as an
alternative solution to #10810.

## Detailed Description of the Pull Request / Additional comments

On the original DEC terminals that supported paging, you couldn't have
both paging and scrollback at the same time - only the one or the other.
But modern terminals typically allow both, so we support that too.

The way it works, the currently visible page will be attached to the
scrollback, and any content that scrolls off the top will thus be saved.
But the background pages will not have scrollback, so their content is
lost if it scrolls off the top.

And when the screen is resized, only the visible page will be reflowed.
Background pages are not affected by a resize until they become active.
At that point they just receive the traditional style of resize, where
the content is clipped or padded to match the new dimensions.

I'm not sure this is the best way to handle resizing, but we can always
consider other approaches once people have had a chance to try it out.

## Validation Steps Performed

I've added some unit tests covering the new operations, and also done a
lot of manual testing.

Closes #13892
Tests added/passed
This commit is contained in:
James Holderness 2024-05-17 22:49:23 +01:00 committed by GitHub
parent 097a2c1136
commit 4a243f0445
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1059 additions and 475 deletions

View File

@ -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

View File

@ -131,8 +131,7 @@ public:
// 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;

View File

@ -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

View File

@ -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"

View File

@ -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());

View File

@ -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:
// - <none>
// 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:
// - <none>
// 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:

View File

@ -32,8 +32,7 @@ public:
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;

View File

@ -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),

View File

@ -49,6 +49,12 @@ public:
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

View File

@ -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;

View File

@ -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.

View File

@ -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<TextBuffer>(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;
}

View File

@ -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<std::unique_ptr<TextBuffer>, MAX_PAGES> _buffers;
};
}

File diff suppressed because it is too large Load Diff

View File

@ -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<int, int> _GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept;
std::pair<int, int> _GetVerticalMargins(const Page& page, const bool absolute) noexcept;
std::pair<int, int> _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> _fontBuffer;
std::shared_ptr<MacroBuffer> _macroBuffer;
std::optional<unsigned int> _initialCodePage;
@ -295,7 +305,7 @@ namespace Microsoft::Console::VirtualTerminal
til::inclusive_rect _scrollMargins;
til::enumset<Mode> _modes;
til::enumset<Mode> _modes{ Mode::PageCursorCoupling };
SgrStack _sgrStack;

View File

@ -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;
}

View File

@ -15,6 +15,7 @@
<ClCompile Include="..\FontBuffer.cpp" />
<ClCompile Include="..\InteractDispatch.cpp" />
<ClCompile Include="..\MacroBuffer.cpp" />
<ClCompile Include="..\PageManager.cpp" />
<ClCompile Include="..\adaptDispatchGraphics.cpp" />
<ClCompile Include="..\terminalOutput.cpp" />
<ClCompile Include="..\precomp.cpp">
@ -29,6 +30,7 @@
<ClInclude Include="..\InteractDispatch.hpp" />
<ClInclude Include="..\ITerminalApi.hpp" />
<ClInclude Include="..\MacroBuffer.hpp" />
<ClInclude Include="..\PageManager.hpp" />
<ClInclude Include="..\precomp.h" />
<ClInclude Include="..\terminalOutput.hpp" />
<ClInclude Include="..\ITermDispatch.hpp" />

View File

@ -36,6 +36,9 @@
<ClCompile Include="..\MacroBuffer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\PageManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\adaptDispatch.hpp">
@ -74,6 +77,9 @@
<ClInclude Include="..\MacroBuffer.hpp">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\PageManager.hpp">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />

View File

@ -34,6 +34,7 @@ SOURCES= \
..\FontBuffer.cpp \
..\InteractDispatch.cpp \
..\MacroBuffer.cpp \
..\PageManager.cpp \
..\adaptDispatchGraphics.cpp \
..\terminalOutput.cpp \

View File

@ -42,6 +42,12 @@ public:
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

View File

@ -81,14 +81,10 @@ public:
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 @@ public:
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 @@ public:
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 @@ public:
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 @@ public:
{
_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 @@ public:
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> _testGetSet;

View File

@ -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);

View File

@ -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("#{"),