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
53 changes: 53 additions & 0 deletions FIXES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
# Resize reflow for the normal buffer

## Summary

This change ports xterm.js 5.5.0 resize reflow into XTerm.NET. Shrinking column width re-wraps long logical lines onto additional `IsWrapped` rows instead of truncating them; growing width merges wrapped groups back. The alternate buffer is excluded via an explicit `hasScrollback: false` constructor flag.

A related latent bug is fixed: when the buffer was at capacity and row count shrank, `CircularList.Resize` ran before trimming and kept the oldest lines, silently discarding the live screen bottom. Resize now trims from the top (raising `Trimmed`) before shrinking capacity.

A second bug, in the reflow itself, is fixed here: shrinking a buffer that held an EMPTY wrapped group threw `IndexOutOfRangeException`. `ReflowSmallerGetNewLineLengths` loops while `cellsAvailable < cellsNeeded`, so a group whose trimmed length is zero returns an empty array, and `ReflowSmaller` read `[Length - 1]` from it. Only a one-row group can be empty -- every row of a group except the last counts as a full row of cells regardless of content -- so it takes a blank continuation row at index 0 with an unwrapped row beneath, which is what the scrollback leaves once the row being continued is trimmed away. Twelve spaces at six columns, two further lines, and a narrowing resize reproduce it through `Terminal.Write` alone.

## Why

Without reflow, shrinking a terminal window and growing it back left every long line permanently truncated at the narrowest width — the primary defect tracked as ISS-007. Scrollback lines were also lost on capacity shrink because `CircularList.Resize` preserved the wrong end of the buffer.

## Resize edge cases found in review

Six further defects, each reproduced before being fixed and each reachable from ordinary use:

- **One-column reflow hung, then threw `OutOfMemoryException`.** A wide glyph at the wrap boundary made the new line length zero, so `ReflowSmallerGetNewLineLengths` never advanced and appended rows until the list could not grow. A wide glyph cannot be shown in one column, so it is clipped.
- **The viewport adjustment popped rows the outer loop was still walking**, throwing `IndexOutOfRangeException`.
- **A line expanding past the remaining capacity indexed below zero** in the batched rebuild, throwing. Rows that do not fit are the oldest, which capacity trimming discards anyway.
- **`Math.Min` dropped the cursor's lower bound.** Moving to the new column count was the point of that change, but a negative cursor -- which `SetCursorRaw` exists to allow -- survived the resize and left the buffer reporting an out-of-bounds position.
- **The viewport was shifted by the trim amount rather than recomputed.** A 5-row buffer with 5 of scrollback resized to 3 rows showed rows 3..5 of 8, with the live bottom unseen at row 7 and later output landing outside the visible area.
- **A zero-row buffer could never be initialised by a later resize**, because the row-fill loop had moved inside a "has lines" guard. `Lines.Length` stayed 0 and the next write indexed an empty list.

Two of these are the same root cause: this is a port from JavaScript, where reading past the end of an array yields `undefined` and falls into a null check. In C# the identical read throws.

## Files changed

- `src/XTerm.NET/Buffer/BufferReflow.cs` — pure reflow functions ported from `BufferReflow.ts`
- `src/XTerm.NET/Buffer/TerminalBuffer.cs` — `Resize` restructure, `ReflowLarger`/`ReflowSmaller`, `hasScrollback` flag
- `src/XTerm.NET/Buffer/BufferLine.cs` — `GetWidth`, `HasContent`, `ReplaceCells`; `GetTrimmedLength` wide-char width
- `src/XTerm.NET/Buffer/CircularList.cs` — `SetLength` for reflow batching
- `src/XTerm.NET/Terminal.cs` — alt buffer `hasScrollback: false`
- `src/XTerm.NET.Tests/Buffer/BufferReflowTests.cs` — pure-function tests
- `src/XTerm.NET.Tests/Buffer/BufferTests.cs` — reflow integration tests
- `src/XTerm.NET.Tests/Buffer/ReflowEmptyGroupTests.cs` — regression tests for the empty wrapped group
- `src/XTerm.NET.Tests/Buffer/ResizeEdgeCaseTests.cs` — regression tests for the six resize edge cases above

## Validation

```powershell
dotnet test src/XTerm.NET.slnx
```

Result on this branch:

```text
Passed: 728
Failed: 0
Comment on lines +49 to +50
Skipped: 0
```

# Docker progress rendering fixes

## Summary
Expand Down
112 changes: 112 additions & 0 deletions src/XTerm.NET.Tests/Buffer/BufferReflowTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using XTerm.Buffer;

namespace XTerm.Tests.Buffer;

public class BufferReflowTests
{
[Fact]
public void ReflowSmallerGetNewLineLengths_SmallLineWithWideCharacters()
{
var line = new BufferLine(4);
SetCell(line, 0, "汉", 2);
SetCell(line, 1, "", 0);
SetCell(line, 2, "语", 2);
SetCell(line, 3, "", 0);

Assert.Equal("汉语", line.TranslateToString(trimRight: true));
Assert.Equal(new[] { 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 4, 3));
Assert.Equal(new[] { 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 4, 2));
}

[Fact]
public void ReflowSmallerGetNewLineLengths_LargeLineWithWideCharacters()
{
var line = new BufferLine(12);
for (var i = 0; i < 12; i += 4)
{
SetCell(line, i, "汉", 2);
SetCell(line, i + 2, "语", 2);
}
for (var i = 1; i < 12; i += 2)
{
SetCell(line, i, "", 0);
}

Assert.Equal("汉语汉语汉语", line.TranslateToString());
Assert.Equal(new[] { 10, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 11));
Assert.Equal(new[] { 10, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 10));
Assert.Equal(new[] { 8, 4 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 9));
Assert.Equal(new[] { 8, 4 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 8));
Assert.Equal(new[] { 6, 6 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 7));
Assert.Equal(new[] { 6, 6 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 6));
Assert.Equal(new[] { 4, 4, 4 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 5));
Assert.Equal(new[] { 4, 4, 4 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 4));
Assert.Equal(new[] { 2, 2, 2, 2, 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 3));
Assert.Equal(new[] { 2, 2, 2, 2, 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 2));
}

[Fact]
public void ReflowSmallerGetNewLineLengths_MixedWideAndSingleCharacters()
{
var line = new BufferLine(6);
SetCell(line, 0, "a", 1);
SetCell(line, 1, "汉", 2);
SetCell(line, 2, "", 0);
SetCell(line, 3, "语", 2);
SetCell(line, 4, "", 0);
SetCell(line, 5, "b", 1);

Assert.Equal("a汉语b", line.TranslateToString());
Assert.Equal(new[] { 5, 1 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 6, 5));
Assert.Equal(new[] { 3, 3 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 6, 4));
Assert.Equal(new[] { 3, 3 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 6, 3));
Assert.Equal(new[] { 1, 2, 2, 1 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 6, 2));
}

[Fact]
public void ReflowSmallerGetNewLineLengths_WrappedLineWithWideAndSingleCharacters()
{
var line1 = new BufferLine(6);
SetCell(line1, 0, "a", 1);
SetCell(line1, 1, "汉", 2);
SetCell(line1, 2, "", 0);
SetCell(line1, 3, "语", 2);
SetCell(line1, 4, "", 0);
SetCell(line1, 5, "b", 1);

var line2 = new BufferLine(6) { IsWrapped = true };
SetCell(line2, 0, "a", 1);
SetCell(line2, 1, "汉", 2);
SetCell(line2, 2, "", 0);
SetCell(line2, 3, "语", 2);
SetCell(line2, 4, "", 0);
SetCell(line2, 5, "b", 1);

Assert.Equal(new[] { 5, 4, 3 }, BufferReflow.ReflowSmallerGetNewLineLengths([line1, line2], 6, 5));
Assert.Equal(new[] { 3, 4, 4, 1 }, BufferReflow.ReflowSmallerGetNewLineLengths([line1, line2], 6, 4));
Assert.Equal(new[] { 3, 3, 3, 3 }, BufferReflow.ReflowSmallerGetNewLineLengths([line1, line2], 6, 3));
Assert.Equal(new[] { 1, 2, 2, 2, 2, 2, 1 }, BufferReflow.ReflowSmallerGetNewLineLengths([line1, line2], 6, 2));
}

[Fact]
public void ReflowSmallerGetNewLineLengths_LinesEndingInNullSpace()
{
var line = new BufferLine(5);
SetCell(line, 0, "汉", 2);
SetCell(line, 1, "", 0);
SetCell(line, 2, "语", 2);
SetCell(line, 3, "", 0);
var empty = BufferCell.Empty;
line.SetCell(4, ref empty);

Assert.Equal("汉语", line.TranslateToString(trimRight: true));
Assert.Equal(new[] { 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 4, 3));
Assert.Equal(new[] { 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 4, 2));
}

private static void SetCell(BufferLine line, int col, string content, int width = 1)
{
var cell = content == "" ? BufferCell.Empty : new BufferCell(content, width, AttributeData.Default);
line.SetCell(col, ref cell);
}
}
Loading
Loading