Skip to content

Commit fc0a06c

Browse files
authored
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 ✅
1 parent 6bf315a commit fc0a06c

File tree

10 files changed

+69
-37
lines changed

10 files changed

+69
-37
lines changed

src/cascadia/TerminalConnection/ConptyConnection.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -563,13 +563,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
563563
}
564564
}
565565

566-
void ConptyConnection::ClearBuffer()
566+
void ConptyConnection::ClearBuffer(bool keepCursorRow)
567567
{
568568
// If we haven't connected yet, then we really don't need to do
569569
// anything. The connection should already start clear!
570570
if (_isConnected())
571571
{
572-
THROW_IF_FAILED(ConptyClearPseudoConsole(_hPC.get()));
572+
THROW_IF_FAILED(ConptyClearPseudoConsole(_hPC.get(), keepCursorRow));
573573
}
574574
}
575575

src/cascadia/TerminalConnection/ConptyConnection.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
2525
void Resize(uint32_t rows, uint32_t columns);
2626
void ResetSize();
2727
void Close() noexcept;
28-
void ClearBuffer();
28+
void ClearBuffer(bool keepCursorRow);
2929

3030
void ShowHide(const bool show);
3131

src/cascadia/TerminalConnection/ConptyConnection.idl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Microsoft.Terminal.TerminalConnection
1515
UInt16 ShowWindow { get; };
1616

1717
void ResetSize();
18-
void ClearBuffer();
18+
void ClearBuffer(Boolean keepCursorRow);
1919

2020
void ShowHide(Boolean show);
2121

src/cascadia/TerminalControl/ControlCore.cpp

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,23 +2264,42 @@ namespace winrt::Microsoft::Terminal::Control::implementation
22642264
// - <none>
22652265
void ControlCore::ClearBuffer(Control::ClearBufferType clearType)
22662266
{
2267-
std::wstring_view command;
2268-
switch (clearType)
2269-
{
2270-
case ClearBufferType::Screen:
2271-
command = L"\x1b[H\x1b[2J";
2272-
break;
2273-
case ClearBufferType::Scrollback:
2274-
command = L"\x1b[3J";
2275-
break;
2276-
case ClearBufferType::All:
2277-
command = L"\x1b[H\x1b[2J\x1b[3J";
2278-
break;
2279-
}
2280-
22812267
{
22822268
const auto lock = _terminal->LockForWriting();
2283-
_terminal->Write(command);
2269+
// In absolute buffer coordinates, including the scrollback (= Y is offset by the scrollback height).
2270+
const auto viewport = _terminal->GetViewport();
2271+
// The absolute cursor coordinate.
2272+
const auto cursor = _terminal->GetViewportRelativeCursorPosition();
2273+
2274+
// GH#18732: Users want the row the cursor is on to be preserved across clears.
2275+
std::wstring sequence;
2276+
2277+
if (clearType == ClearBufferType::Scrollback || clearType == ClearBufferType::All)
2278+
{
2279+
sequence.append(L"\x1b[3J");
2280+
}
2281+
2282+
if (clearType == ClearBufferType::Screen || clearType == ClearBufferType::All)
2283+
{
2284+
// Erase any viewport contents below (but not including) the cursor row.
2285+
if (viewport.Height() - cursor.y > 1)
2286+
{
2287+
fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[{};1H\x1b[J"), cursor.y + 2);
2288+
}
2289+
2290+
// Erase any viewport contents above (but not including) the cursor row.
2291+
if (cursor.y > 0)
2292+
{
2293+
// An SU sequence would be simpler than this DL sequence,
2294+
// but SU isn't well standardized between terminals.
2295+
// Generally speaking, it's best avoiding it.
2296+
fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[H\x1b[{}M"), cursor.y);
2297+
}
2298+
2299+
fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[1;{}H"), cursor.x + 1);
2300+
}
2301+
2302+
_terminal->Write(sequence);
22842303
}
22852304

22862305
if (clearType == Control::ClearBufferType::Screen || clearType == Control::ClearBufferType::All)
@@ -2289,8 +2308,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
22892308
{
22902309
// Since the clearing of ConPTY occurs asynchronously, this call can result weird issues,
22912310
// where a console application still sees contents that we've already deleted, etc.
2292-
// The correct way would be for ConPTY to emit the appropriate CSI n J sequences.
2293-
conpty.ClearBuffer();
2311+
conpty.ClearBuffer(true);
22942312
}
22952313
}
22962314
}

src/cascadia/UnitTests_Control/ControlCoreTests.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ namespace ControlUnitTests
248248
_standardInit(core);
249249

250250
Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' "
251-
L"(leaving the cursor afer 'Bar')");
251+
L"(leaving the cursor after 'Bar')");
252252
for (auto i = 0; i < 40; ++i)
253253
{
254254
conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n"));
@@ -285,7 +285,7 @@ namespace ControlUnitTests
285285
_standardInit(core);
286286

287287
Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' "
288-
L"(leaving the cursor afer 'Bar')");
288+
L"(leaving the cursor after 'Bar')");
289289
for (auto i = 0; i < 40; ++i)
290290
{
291291
conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n"));
@@ -304,9 +304,9 @@ namespace ControlUnitTests
304304

305305
Log::Comment(L"Check the buffer after the clear");
306306
VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height());
307-
VERIFY_ARE_EQUAL(41, core->ScrollOffset());
307+
VERIFY_ARE_EQUAL(21, core->ScrollOffset());
308308
VERIFY_ARE_EQUAL(20, core->ViewHeight());
309-
VERIFY_ARE_EQUAL(61, core->BufferHeight());
309+
VERIFY_ARE_EQUAL(41, core->BufferHeight());
310310

311311
// In this test, we can't actually check if we cleared the buffer
312312
// contents. ConPTY will handle the actual clearing of the buffer
@@ -322,7 +322,7 @@ namespace ControlUnitTests
322322
_standardInit(core);
323323

324324
Log::Comment(L"Print 40 rows of 'Foo', and a single row of 'Bar' "
325-
L"(leaving the cursor afer 'Bar')");
325+
L"(leaving the cursor after 'Bar')");
326326
for (auto i = 0; i < 40; ++i)
327327
{
328328
conn->WriteInput(winrt_wstring_to_array_view(L"Foo\r\n"));

src/host/PtySignalInputThread.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,13 @@ try
124124
}
125125
case PtySignal::ClearBuffer:
126126
{
127-
_DoClearBuffer();
127+
ClearBufferData msg = { 0 };
128+
if (!_GetData(&msg, sizeof(msg)))
129+
{
130+
return S_OK;
131+
}
132+
133+
_DoClearBuffer(msg.keepCursorRow != 0);
128134
break;
129135
}
130136
case PtySignal::ResizeWindow:
@@ -180,7 +186,7 @@ void PtySignalInputThread::_DoResizeWindow(const ResizeWindowData& data)
180186
_api.ResizeWindow(data.sx, data.sy);
181187
}
182188

183-
void PtySignalInputThread::_DoClearBuffer() const
189+
void PtySignalInputThread::_DoClearBuffer(const bool keepCursorRow) const
184190
{
185191
LockConsole();
186192
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
@@ -196,8 +202,11 @@ void PtySignalInputThread::_DoClearBuffer() const
196202

197203
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
198204
auto& screenInfo = gci.GetActiveOutputBuffer();
199-
auto& stateMachine = screenInfo.GetStateMachine();
200-
stateMachine.ProcessString(L"\x1b[H\x1b[2J");
205+
auto& tb = screenInfo.GetTextBuffer();
206+
const auto cursor = tb.GetCursor().GetPosition();
207+
208+
tb.ClearScrollback(cursor.y, keepCursorRow ? 1 : 0);
209+
tb.GetCursor().SetPosition({ keepCursorRow ? cursor.x : 0, 0 });
201210
}
202211

203212
void PtySignalInputThread::_DoShowHide(const ShowHideData& data)

src/host/PtySignalInputThread.hpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ namespace Microsoft::Console
5555
unsigned short show; // used as a bool, but passed as a ushort
5656
};
5757

58+
struct ClearBufferData
59+
{
60+
unsigned short keepCursorRow;
61+
};
62+
5863
struct SetParentData
5964
{
6065
uint64_t handle;
@@ -64,7 +69,7 @@ namespace Microsoft::Console
6469
[[nodiscard]] bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer);
6570
void _DoResizeWindow(const ResizeWindowData& data);
6671
void _DoSetWindowParent(const SetParentData& data);
67-
void _DoClearBuffer() const;
72+
void _DoClearBuffer(bool keepCursorRow) const;
6873
void _DoShowHide(const ShowHideData& data);
6974
void _Shutdown();
7075

src/inc/conpty-static.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput
3838
CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(HANDLE hToken, COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
3939

4040
CONPTY_EXPORT HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size);
41-
CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC);
41+
CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC, BOOL keepCursorRow);
4242
CONPTY_EXPORT HRESULT WINAPI ConptyShowHidePseudoConsole(HPCON hPC, bool show);
4343
CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent);
4444
CONPTY_EXPORT HRESULT WINAPI ConptyReleasePseudoConsole(HPCON hPC);

src/winconpty/winconpty.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,15 +278,16 @@ HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const CO
278278
// Return Value:
279279
// - S_OK if the call succeeded, else an appropriate HRESULT for failing to
280280
// write the clear message to the pty.
281-
HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty)
281+
static HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty, BOOL keepCursorRow) noexcept
282282
{
283283
if (pPty == nullptr)
284284
{
285285
return E_INVALIDARG;
286286
}
287287

288-
unsigned short signalPacket[1];
288+
unsigned short signalPacket[2];
289289
signalPacket[0] = PTY_SIGNAL_CLEAR_WINDOW;
290+
signalPacket[1] = keepCursorRow ? 1 : 0;
290291

291292
const auto fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr);
292293
return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError());
@@ -492,13 +493,13 @@ extern "C" HRESULT WINAPI ConptyResizePseudoConsole(_In_ HPCON hPC, _In_ COORD s
492493
// - This is used exclusively by ConPTY to support GH#1193, GH#1882. This allows
493494
// a terminal to clear the contents of the ConPTY buffer, which is important
494495
// if the user would like to be able to clear the terminal-side buffer.
495-
extern "C" HRESULT WINAPI ConptyClearPseudoConsole(_In_ HPCON hPC)
496+
extern "C" HRESULT WINAPI ConptyClearPseudoConsole(_In_ HPCON hPC, BOOL keepCursorRow)
496497
{
497498
const PseudoConsole* const pPty = (PseudoConsole*)hPC;
498499
auto hr = pPty == nullptr ? E_INVALIDARG : S_OK;
499500
if (SUCCEEDED(hr))
500501
{
501-
hr = _ClearPseudoConsole(pPty);
502+
hr = _ClearPseudoConsole(pPty, keepCursorRow);
502503
}
503504
return hr;
504505
}

src/winconpty/winconpty.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ HRESULT _CreatePseudoConsole(const HANDLE hToken,
6868
_Inout_ PseudoConsole* pPty);
6969

7070
HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const COORD size);
71-
HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty);
7271
HRESULT _ShowHidePseudoConsole(_In_ const PseudoConsole* const pPty, const bool show);
7372
HRESULT _ReparentPseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const HWND newParent);
7473
void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty);

0 commit comments

Comments
 (0)