Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/XTerm.NET.Tests/InputHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,128 @@ public void HandleCsi_EraseInDisplay_ClearAll()
}
}

private static bool BufferContains(Terminal terminal, string text)
{
for (int i = 0; i < terminal.Buffer.Length; i++)
{
if (terminal.Buffer.Lines[i]?.TranslateToString(true).Contains(text) == true)
return true;
}

return false;
}

/// <summary>
/// CSI 3 J discards the scrollback. Not blanks it — discards it, so the lines are gone from the buffer
/// and cannot be scrolled back to.
///
/// Mode 3 used to share mode 2's body, which erases the VISIBLE screen and never touches the scrollback:
/// the opposite of what it asks for. Downstream that surfaced as `cls` in cmd.exe leaving the history
/// reachable with the mouse wheel, unlike conhost and Windows Terminal.
/// </summary>
[Fact]
public void HandleCsi_EraseInDisplay_EraseScrollback_DiscardsHistory()
{
// Arrange - write enough to push lines above the visible screen
var terminal = CreateTerminal(rows: 5);
for (int i = 0; i < 20; i++)
{
terminal.Write($"line {i}\r\n");
}

Assert.True(terminal.Buffer.YBase > 0, "test needs scrollback to exist before erasing it");
var lengthBefore = terminal.Buffer.Length;
var handler = new InputHandler(terminal);

var params_ = new Params();
params_.AddParam(3); // Erase scrollback

// Act
handler.HandleCsi("J", params_);

// Assert - the history is gone, not merely blank
Assert.Equal(0, terminal.Buffer.YBase);
Assert.Equal(0, terminal.Buffer.ViewportY);
Assert.True(terminal.Buffer.Length < lengthBefore,
$"the scrollback should have been discarded: was {lengthBefore}, now {terminal.Buffer.Length}");
}

/// <summary>
/// And it leaves the visible screen alone — the two modes are complements, not variations. cmd.exe's
/// `cls` relies on that: it clears the screen itself, line by line, then sends CSI 3 J for the rest.
/// </summary>
[Fact]
public void HandleCsi_EraseInDisplay_EraseScrollback_KeepsTheVisibleScreen()
{
// Arrange
var terminal = CreateTerminal(rows: 5);
for (int i = 0; i < 20; i++)
{
terminal.Write($"line {i}\r\n");
}
terminal.Write("KEEPME");

var handler = new InputHandler(terminal);
var params_ = new Params();
params_.AddParam(3);

// Act
handler.HandleCsi("J", params_);

// Assert
Assert.True(BufferContains(terminal, "KEEPME"), "erasing the scrollback must not erase the screen");
}

/// <summary>
/// The buffer must still be usable afterwards. YBase and YDisp are absolute indices into the line list,
/// so trimming from the start without moving them leaves the visible screen pointing at an offset that
/// no longer exists, and the next write runs off the end.
/// </summary>
[Fact]
public void HandleCsi_EraseInDisplay_EraseScrollback_LeavesTheBufferWritable()
{
// Arrange
var terminal = CreateTerminal(rows: 5);
for (int i = 0; i < 20; i++)
{
terminal.Write($"line {i}\r\n");
}

var handler = new InputHandler(terminal);
var params_ = new Params();
params_.AddParam(3);
handler.HandleCsi("J", params_);

// Act - this threw IndexOutOfRangeException when the indices were left stale
var exception = Record.Exception(() => terminal.Write("AFTERWARDS\r\n"));

// Assert
Assert.Null(exception);
Assert.True(BufferContains(terminal, "AFTERWARDS"), "text written after the erase should render");
}

/// <summary>Erasing a scrollback that does not exist yet is a no-op, not a crash.</summary>
[Fact]
public void HandleCsi_EraseInDisplay_EraseScrollback_WithNoScrollback_IsHarmless()
{
// Arrange
var terminal = CreateTerminal(rows: 24);
terminal.Write("only one line");
Assert.Equal(0, terminal.Buffer.YBase);

var handler = new InputHandler(terminal);
var params_ = new Params();
params_.AddParam(3);

// Act
var exception = Record.Exception(() => handler.HandleCsi("J", params_));

// Assert
Assert.Null(exception);
Assert.Equal(0, terminal.Buffer.YBase);
Assert.True(BufferContains(terminal, "only one line"), "the screen is untouched");
}

[Fact]
public void HandleCsi_EraseInLine_ErasesToLeft()
{
Expand Down
22 changes: 22 additions & 0 deletions src/XTerm.NET/Buffer/TerminalBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,28 @@ public void ScrollToTop()
_yDisp = 0;
}

/// <summary>
/// Discards the scrollback — every line above the visible screen — leaving the visible screen and the
/// cursor untouched.
/// </summary>
/// <remarks>
/// <para>This is what <c>CSI 3 J</c> asks for, and it is a different operation from erasing: the lines
/// are REMOVED from the buffer rather than blanked, so the history is genuinely gone and cannot be
/// scrolled back to.</para>
/// <para><c>_yBase</c> and <c>_yDisp</c> must move with the lines. They are absolute indices into the
/// buffer, so trimming from the start without adjusting them leaves the visible screen indexed at an
/// offset that no longer exists, and the next write runs off the end of the list.</para>
/// </remarks>
public void ClearScrollback()
{
if (_yBase == 0)
return;

_lines.TrimStart(_yBase);
_yBase = 0;
_yDisp = 0;
}

/// <summary>
/// Scrolls the viewport by a relative number of lines.
/// </summary>
Expand Down
14 changes: 12 additions & 2 deletions src/XTerm.NET/InputHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -876,13 +876,23 @@ private void EraseInDisplay(Params parameters)
}
EraseInLine(parameters); // Current line to cursor
break;
case 2: // Erase all
case 3: // Erase scrollback (extension)
case 2: // Erase all — the visible screen only; the scrollback is kept
for (int i = 0; i < _terminal.Rows; i++)
{
_buffer.Lines[_buffer.YBase + i]?.Fill(emptyCell);
}
break;
case 3: // Erase scrollback (xterm extension) — the scrollback only; the screen is kept
// Previously shared the body above, which erases the VISIBLE screen and never touches the
// scrollback: the opposite of what mode 3 asks for. The two modes are complements, not
// variations, so a caller wanting both sends 2 and 3 — which is exactly what cmd.exe's
// `cls` does under ConPTY (it clears the screen line by line, then sends CSI 3 J).
//
// Discarding rather than blanking is the point: blanked lines are still scrollable, so the
// history stayed reachable with the mouse wheel even though the terminal had been told to
// throw it away.
_buffer.ClearScrollback();
break;
}
}

Expand Down
Loading