Skip to content
Open
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
102 changes: 102 additions & 0 deletions src/XTerm.NET.Tests/Buffer/BufferTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using XTerm.Buffer;
using XTerm.Common;
using XTerm.Options;

namespace XTerm.Tests.Buffer;

Expand Down Expand Up @@ -989,6 +990,100 @@ public void AlternateBuffer_NoScrollback_ScrollUpAtTopOfScreen_YBaseRemainsZero(
Assert.Equal(0, buffer.YDisp);
}

[Fact]
public void ScrollUp_TopAnchoredPartialRegion_PreservesRowsBelowRegion()
{
var buffer = new TerminalBuffer(10, 5, 100);

SetCell(buffer, 0, "A");
SetCell(buffer, 1, "B");
SetCell(buffer, 2, "C");
SetCell(buffer, 3, "D");
SetCell(buffer, 4, ">");

buffer.SetScrollRegion(0, 3);
buffer.ScrollUp(1);

Assert.Equal(0, buffer.YBase);
Assert.Equal("B", buffer.GetLine(0)?[0].Content);
Assert.Equal("C", buffer.GetLine(1)?[0].Content);
Assert.Equal("D", buffer.GetLine(2)?[0].Content);
Assert.True(buffer.GetLine(3)?[0].IsSpace());
Assert.Equal(">", buffer.GetLine(4)?[0].Content);
}

[Fact]
public void InsertLines_WithScrollback_UsesActiveBufferCoordinates()
{
var terminal = new Terminal(new TerminalOptions { Cols = 10, Rows = 5, Scrollback = 100 });

terminal.Write("s1\r\ns2\r\ns3\r\ns4\r\ns5\r\n");
var yBase = terminal.Buffer.YBase;

SetCell(terminal.Buffer, yBase + 0, "A");
SetCell(terminal.Buffer, yBase + 1, "B");
SetCell(terminal.Buffer, yBase + 2, "C");
SetCell(terminal.Buffer, yBase + 3, "D");
SetCell(terminal.Buffer, yBase + 4, ">");

terminal.Write("\x1b[1;4r\x1b[1;1H\x1b[1L");

Assert.Equal(yBase, terminal.Buffer.YBase);
Assert.True(terminal.Buffer.GetLine(yBase + 0)?[0].IsSpace());
Assert.Equal("A", terminal.Buffer.GetLine(yBase + 1)?[0].Content);
Assert.Equal("B", terminal.Buffer.GetLine(yBase + 2)?[0].Content);
Assert.Equal("C", terminal.Buffer.GetLine(yBase + 3)?[0].Content);
Assert.Equal(">", terminal.Buffer.GetLine(yBase + 4)?[0].Content);
}

[Fact]
public void DeleteLines_WithScrollback_UsesActiveBufferCoordinates()
{
var terminal = new Terminal(new TerminalOptions { Cols = 10, Rows = 5, Scrollback = 100 });

terminal.Write("s1\r\ns2\r\ns3\r\ns4\r\ns5\r\n");
var yBase = terminal.Buffer.YBase;

SetCell(terminal.Buffer, yBase + 0, "A");
SetCell(terminal.Buffer, yBase + 1, "B");
SetCell(terminal.Buffer, yBase + 2, "C");
SetCell(terminal.Buffer, yBase + 3, "D");
SetCell(terminal.Buffer, yBase + 4, ">");

terminal.Write("\x1b[1;4r\x1b[1;1H\x1b[1M");

Assert.Equal(yBase, terminal.Buffer.YBase);
Assert.Equal("B", terminal.Buffer.GetLine(yBase + 0)?[0].Content);
Assert.Equal("C", terminal.Buffer.GetLine(yBase + 1)?[0].Content);
Assert.Equal("D", terminal.Buffer.GetLine(yBase + 2)?[0].Content);
Assert.True(terminal.Buffer.GetLine(yBase + 3)?[0].IsSpace());
Assert.Equal(">", terminal.Buffer.GetLine(yBase + 4)?[0].Content);
}

[Fact]
public void DeleteLines_OutsideScrollRegion_PreservesReservedPromptRow()
{
var terminal = new Terminal(new TerminalOptions { Cols = 10, Rows = 5, Scrollback = 100 });

terminal.Write("s1\r\ns2\r\ns3\r\ns4\r\ns5\r\n");
var yBase = terminal.Buffer.YBase;

SetCell(terminal.Buffer, yBase + 0, "A");
SetCell(terminal.Buffer, yBase + 1, "B");
SetCell(terminal.Buffer, yBase + 2, "C");
SetCell(terminal.Buffer, yBase + 3, "D");
SetCell(terminal.Buffer, yBase + 4, ">");

terminal.Write("\x1b[1;4r\x1b[5;1H\x1b[1M");

Assert.Equal(yBase, terminal.Buffer.YBase);
Assert.Equal("A", terminal.Buffer.GetLine(yBase + 0)?[0].Content);
Assert.Equal("B", terminal.Buffer.GetLine(yBase + 1)?[0].Content);
Assert.Equal("C", terminal.Buffer.GetLine(yBase + 2)?[0].Content);
Assert.Equal("D", terminal.Buffer.GetLine(yBase + 3)?[0].Content);
Assert.Equal(">", terminal.Buffer.GetLine(yBase + 4)?[0].Content);
}

[Fact]
public void AlternateBuffer_NoScrollback_MultipleScrollOperations_YBaseRemainsZero()
{
Expand Down Expand Up @@ -1092,5 +1187,12 @@ public void NormalBuffer_WithScrollback_ScrollUpAtTop_YBaseIncrements()
Assert.Equal(5, buffer.YDisp);
}

private static void SetCell(TerminalBuffer buffer, int row, string content)
{
var line = buffer.GetLine(row);
var cell = new BufferCell { Content = content, Width = 1 };
line?.SetCell(0, ref cell);
}

#endregion
}
9 changes: 4 additions & 5 deletions src/XTerm.NET/Buffer/TerminalBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,10 @@ public void ScrollUp(int lines, bool isWrapped = false)
// Create a new blank line that will be inserted at the bottom of the scroll region
var newLine = GetBlankLine(AttributeData.Default, isWrapped);

// Only use the scrollback path if:
// 1. Scroll region starts at top (_scrollTop == 0)
// 2. We actually have scrollback capacity (MaxLength > _rows)
// For alternate buffer (no scrollback), always use the in-place scroll logic.
if (_scrollTop == 0 && _lines.MaxLength > _rows)
// Only the full-screen scroll region contributes to scrollback.
// Top-anchored partial regions reserve rows below the margin and
// must scroll in place so prompts/status rows are not promoted.
if (_scrollTop == 0 && _scrollBottom == _rows - 1 && _lines.MaxLength > _rows)
{
// When scrollTop is 0, the top line goes into scrollback.
// In xterm.js: push new line first, then increment yBase and yDisp.
Expand Down
7 changes: 5 additions & 2 deletions src/XTerm.NET/InputHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ private void InsertLines(Params parameters)

for (int i = 0; i < count; i++)
{
_buffer.Lines.Splice(_buffer.ScrollBottom, 1);
_buffer.Lines.Splice(_buffer.YBase + _buffer.ScrollBottom, 1);
_buffer.Lines.Splice(_buffer.Y + _buffer.YBase, 0,
_buffer.GetBlankLine(_curAttr));
}
Expand All @@ -924,11 +924,14 @@ private void InsertLines(Params parameters)
private void DeleteLines(Params parameters)
{
var count = Math.Max(parameters.GetParam(0, 1), 1);
// Only works in scroll region
if (_buffer.Y < _buffer.ScrollTop || _buffer.Y > _buffer.ScrollBottom)
return;

for (int i = 0; i < count; i++)
{
_buffer.Lines.Splice(_buffer.Y + _buffer.YBase, 1);
_buffer.Lines.Splice(_buffer.ScrollBottom, 0,
_buffer.Lines.Splice(_buffer.YBase + _buffer.ScrollBottom, 0,
_buffer.GetBlankLine(_curAttr));
}
}
Expand Down