Preserve the cursor row during Clear Buffer (#18976)

Introduces an ABI change to the ConptyClearPseudoConsole signal.
Otherwise, we have to make it so that the API call always retains
the row the cursor is on, but I feel like that makes it worse.

Closes #18732
Closes #18878

## Validation Steps Performed
* Launch `ConsoleMonitor.exe`
* Create some text above & below the cursor in PowerShell
* Clear Buffer
* Buffer is cleared except for the cursor row 
* ...same in ConPTY 
This commit is contained in:
Leonard Hecker 2025-06-25 18:31:28 +02:00 committed by GitHub
parent 6bf315a4c9
commit fc0a06c3b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 69 additions and 37 deletions

View File

@ -563,13 +563,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
} }
} }
void ConptyConnection::ClearBuffer() void ConptyConnection::ClearBuffer(bool keepCursorRow)
{ {
// If we haven't connected yet, then we really don't need to do // If we haven't connected yet, then we really don't need to do
// anything. The connection should already start clear! // anything. The connection should already start clear!
if (_isConnected()) if (_isConnected())
{ {
THROW_IF_FAILED(ConptyClearPseudoConsole(_hPC.get())); THROW_IF_FAILED(ConptyClearPseudoConsole(_hPC.get(), keepCursorRow));
} }
} }

View File

@ -25,7 +25,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
void Resize(uint32_t rows, uint32_t columns); void Resize(uint32_t rows, uint32_t columns);
void ResetSize(); void ResetSize();
void Close() noexcept; void Close() noexcept;
void ClearBuffer(); void ClearBuffer(bool keepCursorRow);
void ShowHide(const bool show); void ShowHide(const bool show);

View File

@ -15,7 +15,7 @@ namespace Microsoft.Terminal.TerminalConnection
UInt16 ShowWindow { get; }; UInt16 ShowWindow { get; };
void ResetSize(); void ResetSize();
void ClearBuffer(); void ClearBuffer(Boolean keepCursorRow);
void ShowHide(Boolean show); void ShowHide(Boolean show);

View File

@ -2264,23 +2264,42 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - <none> // - <none>
void ControlCore::ClearBuffer(Control::ClearBufferType clearType) void ControlCore::ClearBuffer(Control::ClearBufferType clearType)
{ {
std::wstring_view command;
switch (clearType)
{
case ClearBufferType::Screen:
command = L"\x1b[H\x1b[2J";
break;
case ClearBufferType::Scrollback:
command = L"\x1b[3J";
break;
case ClearBufferType::All:
command = L"\x1b[H\x1b[2J\x1b[3J";
break;
}
{ {
const auto lock = _terminal->LockForWriting(); const auto lock = _terminal->LockForWriting();
_terminal->Write(command); // In absolute buffer coordinates, including the scrollback (= Y is offset by the scrollback height).
const auto viewport = _terminal->GetViewport();
// The absolute cursor coordinate.
const auto cursor = _terminal->GetViewportRelativeCursorPosition();
// GH#18732: Users want the row the cursor is on to be preserved across clears.
std::wstring sequence;
if (clearType == ClearBufferType::Scrollback || clearType == ClearBufferType::All)
{
sequence.append(L"\x1b[3J");
}
if (clearType == ClearBufferType::Screen || clearType == ClearBufferType::All)
{
// Erase any viewport contents below (but not including) the cursor row.
if (viewport.Height() - cursor.y > 1)
{
fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[{};1H\x1b[J"), cursor.y + 2);
}
// Erase any viewport contents above (but not including) the cursor row.
if (cursor.y > 0)
{
// An SU sequence would be simpler than this DL sequence,
// but SU isn't well standardized between terminals.
// Generally speaking, it's best avoiding it.
fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[H\x1b[{}M"), cursor.y);
}
fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[1;{}H"), cursor.x + 1);
}
_terminal->Write(sequence);
} }
if (clearType == Control::ClearBufferType::Screen || clearType == Control::ClearBufferType::All) if (clearType == Control::ClearBufferType::Screen || clearType == Control::ClearBufferType::All)
@ -2289,8 +2308,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{ {
// Since the clearing of ConPTY occurs asynchronously, this call can result weird issues, // Since the clearing of ConPTY occurs asynchronously, this call can result weird issues,
// where a console application still sees contents that we've already deleted, etc. // where a console application still sees contents that we've already deleted, etc.
// The correct way would be for ConPTY to emit the appropriate CSI n J sequences. conpty.ClearBuffer(true);
conpty.ClearBuffer();
} }
} }
} }

View File

@ -248,7 +248,7 @@ namespace ControlUnitTests
_standardInit(core); _standardInit(core);
Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' "
L"(leaving the cursor afer 'Bar')"); L"(leaving the cursor after 'Bar')");
for (auto i = 0; i < 40; ++i) for (auto i = 0; i < 40; ++i)
{ {
conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n"));
@ -285,7 +285,7 @@ namespace ControlUnitTests
_standardInit(core); _standardInit(core);
Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' "
L"(leaving the cursor afer 'Bar')"); L"(leaving the cursor after 'Bar')");
for (auto i = 0; i < 40; ++i) for (auto i = 0; i < 40; ++i)
{ {
conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n"));
@ -304,9 +304,9 @@ namespace ControlUnitTests
Log::Comment(L"Check the buffer after the clear"); Log::Comment(L"Check the buffer after the clear");
VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height());
VERIFY_ARE_EQUAL(41, core->ScrollOffset()); VERIFY_ARE_EQUAL(21, core->ScrollOffset());
VERIFY_ARE_EQUAL(20, core->ViewHeight()); VERIFY_ARE_EQUAL(20, core->ViewHeight());
VERIFY_ARE_EQUAL(61, core->BufferHeight()); VERIFY_ARE_EQUAL(41, core->BufferHeight());
// In this test, we can't actually check if we cleared the buffer // In this test, we can't actually check if we cleared the buffer
// contents. ConPTY will handle the actual clearing of the buffer // contents. ConPTY will handle the actual clearing of the buffer
@ -322,7 +322,7 @@ namespace ControlUnitTests
_standardInit(core); _standardInit(core);
Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' " Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' "
L"(leaving the cursor afer 'Bar')"); L"(leaving the cursor after 'Bar')");
for (auto i = 0; i < 40; ++i) for (auto i = 0; i < 40; ++i)
{ {
conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n")); conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n"));

View File

@ -124,7 +124,13 @@ try
} }
case PtySignal::ClearBuffer: case PtySignal::ClearBuffer:
{ {
_DoClearBuffer(); ClearBufferData msg = { 0 };
if (!_GetData(&msg, sizeof(msg)))
{
return S_OK;
}
_DoClearBuffer(msg.keepCursorRow != 0);
break; break;
} }
case PtySignal::ResizeWindow: case PtySignal::ResizeWindow:
@ -180,7 +186,7 @@ void PtySignalInputThread::_DoResizeWindow(const ResizeWindowData& data)
_api.ResizeWindow(data.sx, data.sy); _api.ResizeWindow(data.sx, data.sy);
} }
void PtySignalInputThread::_DoClearBuffer() const void PtySignalInputThread::_DoClearBuffer(const bool keepCursorRow) const
{ {
LockConsole(); LockConsole();
auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
@ -196,8 +202,11 @@ void PtySignalInputThread::_DoClearBuffer() const
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& screenInfo = gci.GetActiveOutputBuffer(); auto& screenInfo = gci.GetActiveOutputBuffer();
auto& stateMachine = screenInfo.GetStateMachine(); auto& tb = screenInfo.GetTextBuffer();
stateMachine.ProcessString(L"\x1b[H\x1b[2J"); const auto cursor = tb.GetCursor().GetPosition();
tb.ClearScrollback(cursor.y, keepCursorRow ? 1 : 0);
tb.GetCursor().SetPosition({ keepCursorRow ? cursor.x : 0, 0 });
} }
void PtySignalInputThread::_DoShowHide(const ShowHideData& data) void PtySignalInputThread::_DoShowHide(const ShowHideData& data)

View File

@ -55,6 +55,11 @@ namespace Microsoft::Console
unsigned short show; // used as a bool, but passed as a ushort unsigned short show; // used as a bool, but passed as a ushort
}; };
struct ClearBufferData
{
unsigned short keepCursorRow;
};
struct SetParentData struct SetParentData
{ {
uint64_t handle; uint64_t handle;
@ -64,7 +69,7 @@ namespace Microsoft::Console
[[nodiscard]] bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer); [[nodiscard]] bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer);
void _DoResizeWindow(const ResizeWindowData& data); void _DoResizeWindow(const ResizeWindowData& data);
void _DoSetWindowParent(const SetParentData& data); void _DoSetWindowParent(const SetParentData& data);
void _DoClearBuffer() const; void _DoClearBuffer(bool keepCursorRow) const;
void _DoShowHide(const ShowHideData& data); void _DoShowHide(const ShowHideData& data);
void _Shutdown(); void _Shutdown();

View File

@ -38,7 +38,7 @@ CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput
CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(HANDLE hToken, COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(HANDLE hToken, COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
CONPTY_EXPORT HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size); CONPTY_EXPORT HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size);
CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC); CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC, BOOL keepCursorRow);
CONPTY_EXPORT HRESULT WINAPI ConptyShowHidePseudoConsole(HPCON hPC, bool show); CONPTY_EXPORT HRESULT WINAPI ConptyShowHidePseudoConsole(HPCON hPC, bool show);
CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent); CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent);
CONPTY_EXPORT HRESULT WINAPI ConptyReleasePseudoConsole(HPCON hPC); CONPTY_EXPORT HRESULT WINAPI ConptyReleasePseudoConsole(HPCON hPC);

View File

@ -278,15 +278,16 @@ HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const CO
// Return Value: // Return Value:
// - S_OK if the call succeeded, else an appropriate HRESULT for failing to // - S_OK if the call succeeded, else an appropriate HRESULT for failing to
// write the clear message to the pty. // write the clear message to the pty.
HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty) static HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty, BOOL keepCursorRow) noexcept
{ {
if (pPty == nullptr) if (pPty == nullptr)
{ {
return E_INVALIDARG; return E_INVALIDARG;
} }
unsigned short signalPacket[1]; unsigned short signalPacket[2];
signalPacket[0] = PTY_SIGNAL_CLEAR_WINDOW; signalPacket[0] = PTY_SIGNAL_CLEAR_WINDOW;
signalPacket[1] = keepCursorRow ? 1 : 0;
const auto fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); const auto fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr);
return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError());
@ -492,13 +493,13 @@ extern "C" HRESULT WINAPI ConptyResizePseudoConsole(_In_ HPCON hPC, _In_ COORD s
// - This is used exclusively by ConPTY to support GH#1193, GH#1882. This allows // - This is used exclusively by ConPTY to support GH#1193, GH#1882. This allows
// a terminal to clear the contents of the ConPTY buffer, which is important // a terminal to clear the contents of the ConPTY buffer, which is important
// if the user would like to be able to clear the terminal-side buffer. // if the user would like to be able to clear the terminal-side buffer.
extern "C" HRESULT WINAPI ConptyClearPseudoConsole(_In_ HPCON hPC) extern "C" HRESULT WINAPI ConptyClearPseudoConsole(_In_ HPCON hPC, BOOL keepCursorRow)
{ {
const PseudoConsole* const pPty = (PseudoConsole*)hPC; const PseudoConsole* const pPty = (PseudoConsole*)hPC;
auto hr = pPty == nullptr ? E_INVALIDARG : S_OK; auto hr = pPty == nullptr ? E_INVALIDARG : S_OK;
if (SUCCEEDED(hr)) if (SUCCEEDED(hr))
{ {
hr = _ClearPseudoConsole(pPty); hr = _ClearPseudoConsole(pPty, keepCursorRow);
} }
return hr; return hr;
} }

View File

@ -68,7 +68,6 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken,
_Inout_ PseudoConsole* pPty); _Inout_ PseudoConsole* pPty);
HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const COORD size); HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const COORD size);
HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty);
HRESULT _ShowHidePseudoConsole(_In_ const PseudoConsole* const pPty, const bool show); HRESULT _ShowHidePseudoConsole(_In_ const PseudoConsole* const pPty, const bool show);
HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const HWND newParent); HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const HWND newParent);
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty); void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty);