Skip to content

Force the wrap before a wrapped row that starts with an empty cell - #6236

Open
daviwil wants to merge 1 commit into
xtermjs:masterfrom
daviwil:fix-serialize-wrapped-empty-cell
Open

daviwil wants to merge 1 commit into
xtermjs:masterfrom
daviwil:fix-serialize-wrapped-empty-cell

Conversation

@daviwil

@daviwil daviwil commented Oct 15, 2026

Copy link
Copy Markdown

Fixes #6230

_rowEnd decides whether the next row's wrap happens on its own, which needs a character to be printed. The check read

nextRowFirstChar.getChars() &&
  isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0

which parses as (chars && wide) ? a : b. When the wrapped row starts with an empty cell and the row above is full, it fell through to this._nullCellCount <= 0 and passed. The serializer then wrote the empty cell as CSI 1 C, which doesn't wrap, so the replay printed the rest of the row over the last cell of the row above.

The fix adds the parentheses the comment above it describes, so an empty first cell always takes the forced wrap path (-, CSI 1 D, CSI 1 X). Cases with a printed first cell are unchanged.

For the repro in the issue, the frame goes from abcdefghij\x1b[1CYZ\x1b[3D to abcdefghij-\x1b[1D\x1b[1X\x1b[1CYZ\x1b[3D, and the replay matches the source, wrap flag included.

Testing:

  • New integration test handle wrapped row starting with empty cell, using testNormalScreenEqual. It fails on master and passes with the fix.
  • addon-serialize integration suite on Chromium: 41 passed. I didn't run Firefox or WebKit locally.
  • npm run test-unit: 2403 passing. npm run lint-changes is clean.
  • Checked by hand against headless with a few nearby cases: two empty cells, an empty cell with a background, a wide character after the empty cells, a wide character at the edge, and a plain wrap. All round-trip. Master fails on the first and third of those.

This is independent of the fix for #6231 in the same function. The two branches merge cleanly in either order.

The check for whether a wrap happens on its own read as
`(chars && wide) ? a : b`, so a wrapped row starting with an empty cell
counted as a natural wrap. The serializer then wrote that cell as a
cursor move, which doesn't wrap, and the replay overwrote the end of
the row above. Now an empty first cell always forces the wrap.

Fixes xtermjs#6230
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SerializeAddon: an empty first cell of a wrapped row is written as a cursor move, so the replay overwrites the row above

1 participant