From 3d69482c37497b50c2fece5cb9afe10009b2809c Mon Sep 17 00:00:00 2001 From: David Wilson Date: Thu, 15 Oct 2026 13:01:41 +0300 Subject: [PATCH] Skip the zero-cell moves in the forced wrap separator When the row before a forced wrap has no content, the moves around the erase were written as `CSI 0 C` and `CSI 0 D`. A count of 0 means 1, so the erase started one cell late and left the first dash on screen. Now the moves are left out when there's nothing to move past. Fixes #6231 --- addons/addon-serialize/src/SerializeAddon.ts | 10 ++++++++-- .../addon-serialize/test/SerializeAddon.test.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index e1728feb21..c8a1c72fd2 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -250,10 +250,16 @@ class StringSerializeHandler extends BaseSerializeHandler { if (this._nullCellCount > 0) { // do these because we filled the last several null slot, which we shouldn't + // a move of 0 means a move of 1, so skip the moves when the row has no content + const contentLength = currentLine.length - this._nullCellCount; rowSeparator += '\u001b[A'; - rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}C`; + if (contentLength > 0) { + rowSeparator += `\u001b[${contentLength}C`; + } rowSeparator += `\u001b[${this._nullCellCount}X`; - rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}D`; + if (contentLength > 0) { + rowSeparator += `\u001b[${contentLength}D`; + } rowSeparator += '\u001b[B'; } diff --git a/addons/addon-serialize/test/SerializeAddon.test.ts b/addons/addon-serialize/test/SerializeAddon.test.ts index f84452d58b..21a24e9122 100644 --- a/addons/addon-serialize/test/SerializeAddon.test.ts +++ b/addons/addon-serialize/test/SerializeAddon.test.ts @@ -520,6 +520,22 @@ test.describe('SerializeAddon', () => { await testNormalScreenEqual(ctx.page, segments.join('')); }); + test('handle empty row followed by wrapped row', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + const MOVE_TO = (row: number, col: number): string => `\u001b[${row};${col}H`; + + // The wrap is forced with a row of dashes. With no content in the row, the + // moves around the erase have a count of 0, which means 1. + const segments = [ + 'abcdefghijXYZ', + MOVE_TO(1, 1), + CLEAR_RIGHT(10), + MOVE_TO(2, 4) + ]; + + await testNormalScreenEqual(ctx.page, segments.join('')); + }); + test('handle invalid wrap after scroll', async () => { const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; const MOVE_UP = (l: number): string => `\u001b[${l}A`;