diff --git a/src/XTerm.NET.Tests/Buffer/BufferTests.cs b/src/XTerm.NET.Tests/Buffer/BufferTests.cs index 5db96af..ba1d539 100644 --- a/src/XTerm.NET.Tests/Buffer/BufferTests.cs +++ b/src/XTerm.NET.Tests/Buffer/BufferTests.cs @@ -1,5 +1,6 @@ using XTerm.Buffer; using XTerm.Common; +using XTerm.Options; namespace XTerm.Tests.Buffer; @@ -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() { @@ -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 } diff --git a/src/XTerm.NET/Buffer/TerminalBuffer.cs b/src/XTerm.NET/Buffer/TerminalBuffer.cs index 412c07c..ce5da2e 100644 --- a/src/XTerm.NET/Buffer/TerminalBuffer.cs +++ b/src/XTerm.NET/Buffer/TerminalBuffer.cs @@ -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. diff --git a/src/XTerm.NET/InputHandler.cs b/src/XTerm.NET/InputHandler.cs index 5e2daf7..c811661 100644 --- a/src/XTerm.NET/InputHandler.cs +++ b/src/XTerm.NET/InputHandler.cs @@ -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)); } @@ -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)); } }