From edee1a106788e839651c1bc12269bd375d695d0d Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Sun, 13 Sep 2020 21:33:32 +0800 Subject: [PATCH 01/18] Handle alt screen --- .../src/SerializeAddon.ts | 28 +++++++++++++--- .../test/SerializeAddon.api.ts | 32 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index dad6aa4f..ba366c37 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -11,6 +11,11 @@ function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); } +interface ISerializeOptions { + withAlternate?: boolean; + +} + // TODO: Refine this template class later abstract class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } @@ -167,7 +172,7 @@ export class SerializeAddon implements ITerminalAddon { this._terminal = terminal; } - public serialize(rows?: number): string { + public serialize(rows?: number, options: ISerializeOptions = {}): string { // TODO: Add re-position cursor support // TODO: Add word wrap mode support // TODO: Add combinedData support @@ -175,12 +180,25 @@ export class SerializeAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } - const maxRows = this._terminal.buffer.active.length; - const handler = new StringSerializeHandler(this._terminal.buffer.active); + if (this._terminal.buffer.active.type === 'normal' || !(options?.withAlternate ?? false)) { + const maxRows = this._terminal.buffer.active.length; + const handler = new StringSerializeHandler(this._terminal.buffer.active); - rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows); + rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows); - return handler.serialize(maxRows - rows, maxRows); + return handler.serialize(maxRows - rows, maxRows); + } + + const maxNormalRows = this._terminal.buffer.normal.length; + const maxAltRows = this._terminal.buffer.alternate.length; + const normalHandler = new StringSerializeHandler(this._terminal.buffer.normal); + const altHandler = new StringSerializeHandler(this._terminal.buffer.alternate); + const normalRows = (rows === undefined) ? maxNormalRows : constrain(rows, 0, maxNormalRows); + const altRows = (rows === undefined) ? maxAltRows : constrain(rows, 0, maxAltRows); + + return normalHandler.serialize(maxNormalRows - normalRows, maxNormalRows) + + '\u001b[?1049h\u001b[H' + + altHandler.serialize(maxAltRows - altRows, maxAltRows); } public dispose(): void { } diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 5b615db1..32e816a1 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -299,6 +299,38 @@ describe('SerializeAddon', () => { await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n')); }); + + it('serialize with alt screen correctly', async () => { + const SMCUP = '\u001b[?1049h'; + const CUP = '\u001b[H'; + + const lines = [ + `1${SMCUP}${CUP}2` + ]; + const expected = [ + `1${SMCUP}${CUP}2` + ]; + + await writeSync(page, lines.join('\\r\\n')); + assert.equal(JSON.stringify(await page.evaluate(`window.term.buffer.active.type`)), '"alternate"'); + assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize(undefined, { withAlternate: true });`)), JSON.stringify(expected.join('\r\n'))); + }); + + it('serialize without alt screen correctly', async () => { + const SMCUP = '\u001b[?1049h'; + const RMCUP = '\u001b[?1049l'; + + const lines = [ + `1${SMCUP}2${RMCUP}` + ]; + const expected = [ + `1` + ]; + + await writeSync(page, lines.join('\\r\\n')); + assert.equal(JSON.stringify(await page.evaluate(`window.term.buffer.active === window.term.buffer.alt`)), 'false'); + assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize(undefined, { withAlternate: true });`)), JSON.stringify(expected.join('\r\n'))); + }); }); function newArray(initial: T | ((index: number) => T), count: number): T[] { From 881a4a0dbb838231d9ed44450e539bd426d44417 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Tue, 15 Sep 2020 17:58:56 +0800 Subject: [PATCH 02/18] Try implement cursor restortion --- .../src/SerializeAddon.ts | 110 ++++++++++++++---- demo/client.ts | 2 +- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index ba366c37..e03707e0 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -13,7 +13,7 @@ function constrain(value: number, low: number, high: number): number { interface ISerializeOptions { withAlternate?: boolean; - + withCursor?: boolean; } // TODO: Refine this template class later @@ -26,7 +26,7 @@ abstract class BaseSerializeHandler { const cell2 = this._buffer.getNullCell(); let oldCell = cell1; - this._beforeSerialize(endRow - startRow); + this._beforeSerialize(endRow - startRow, startRow, endRow); for (let row = startRow; row < endRow; row++) { const line = this._buffer.getLine(row); @@ -51,7 +51,7 @@ abstract class BaseSerializeHandler { protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _rowEnd(row: number): void { } - protected _beforeSerialize(rows: number): void { } + protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { } protected _afterSerialize(): void { } protected _serializeString(): string { return ''; } } @@ -82,12 +82,16 @@ class StringSerializeHandler extends BaseSerializeHandler { private _currentRow: string = ''; private _nullCellCount: number = 0; - constructor(buffer: IBuffer) { - super(buffer); + private _lastContentCellRow: number = 0; + private _lastContentCellCol: number = 0; + + constructor(private _buffer1: IBuffer,private _terminal: Terminal, private _option: ISerializeOptions = {}) { + super(_buffer1); } - protected _beforeSerialize(rows: number): void { + protected _beforeSerialize(rows: number, start: number, end: number): void { this._allRows = new Array(rows); + this._lastContentCellRow = start; } protected _rowEnd(row: number): void { @@ -147,6 +151,9 @@ class StringSerializeHandler extends BaseSerializeHandler { } else if (this._nullCellCount > 0) { this._currentRow += `\x1b[${this._nullCellCount}C`; this._nullCellCount = 0; + } else { + this._lastContentCellRow = row; + this._lastContentCellCol = col + cell.getWidth(); } this._currentRow += cell.getChars(); @@ -154,12 +161,71 @@ class StringSerializeHandler extends BaseSerializeHandler { protected _serializeString(): string { let rowEnd = this._allRows.length; + for (; rowEnd > 0; rowEnd--) { if (this._allRows[rowEnd - 1]) { break; } } - return this._allRows.slice(0, rowEnd).join('\r\n'); + + let content = this._allRows.slice(0, rowEnd).join('\r\n'); + + if (this._option.withCursor) { + const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY; + const realCursorCol = this._buffer1.cursorX; + + const hasScroll = this._buffer1.length > this._terminal.rows!; + const hasEmptyLine = hasScroll ? (this._buffer1.length - 1 > this._lastContentCellRow) : (realCursorRow > this._lastContentCellRow); + const cursorMoved = + hasScroll + ? hasEmptyLine + ? (realCursorCol !== 0 || realCursorRow !== this._buffer1.length - 1) + : (realCursorRow !== this._lastContentCellRow || realCursorCol !== this._lastContentCellCol) + : hasEmptyLine + // we don't need to check the row because empty row count are based on cursor + ? realCursorCol !== 0 + : (realCursorRow !== this._lastContentCellRow || realCursorCol !== this._lastContentCellCol); + + const moveRight = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}C`; + } else if (offset < 0) { + content += `\u001b[${-offset}D`; + } + }; + const moveDown = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}B`; + } else if (offset < 0) { + content += `\u001b[${-offset}A`; + } + }; + + // Fix empty lines + if (hasEmptyLine) { + if (hasScroll) { + content += '\r\n'.repeat(this._buffer1.length - 1 - this._lastContentCellRow); + } else { + content += '\r\n'.repeat(realCursorRow - this._lastContentCellRow); + } + } + + if (cursorMoved) { + if (hasEmptyLine) { + if (hasScroll) { + moveRight(realCursorCol); + moveDown(realCursorRow - (this._buffer1.length - 1)); + } else { + moveRight(realCursorCol); + } + } else { + moveDown(realCursorRow - this._lastContentCellRow); + moveRight(realCursorCol - this._lastContentCellCol); + } + } + } + + return content; } } @@ -172,6 +238,15 @@ export class SerializeAddon implements ITerminalAddon { this._terminal = terminal; } + private _getString(buffer: IBuffer, rows?: number, option?: ISerializeOptions): string { + const maxRows = buffer.length; + const handler = new StringSerializeHandler(buffer, this._terminal!, option); + const correctRows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows); + const result = handler.serialize(maxRows - correctRows, maxRows); + + return result; + } + public serialize(rows?: number, options: ISerializeOptions = {}): string { // TODO: Add re-position cursor support // TODO: Add word wrap mode support @@ -180,25 +255,16 @@ export class SerializeAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } - if (this._terminal.buffer.active.type === 'normal' || !(options?.withAlternate ?? false)) { - const maxRows = this._terminal.buffer.active.length; - const handler = new StringSerializeHandler(this._terminal.buffer.active); - - rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows); - - return handler.serialize(maxRows - rows, maxRows); + if (this._terminal.buffer.active.type === 'normal' || !(options.withAlternate ?? false)) { + return this._getString(this._terminal.buffer.active, rows, options); } - const maxNormalRows = this._terminal.buffer.normal.length; - const maxAltRows = this._terminal.buffer.alternate.length; - const normalHandler = new StringSerializeHandler(this._terminal.buffer.normal); - const altHandler = new StringSerializeHandler(this._terminal.buffer.alternate); - const normalRows = (rows === undefined) ? maxNormalRows : constrain(rows, 0, maxNormalRows); - const altRows = (rows === undefined) ? maxAltRows : constrain(rows, 0, maxAltRows); + const normalScreenContent = this._getString(this._terminal.buffer.normal, rows, options); + const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, rows, options); - return normalHandler.serialize(maxNormalRows - normalRows, maxNormalRows) + return normalScreenContent + '\u001b[?1049h\u001b[H' - + altHandler.serialize(maxAltRows - altRows, maxAltRows); + + alternativeScreenContent; } public dispose(): void { } diff --git a/demo/client.ts b/demo/client.ts index 6efa181a..509b73a9 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -413,7 +413,7 @@ function updateTerminalSize(): void { } function serializeButtonHandler(): void { - const output = addons.serialize.instance.serialize(); + const output = addons.serialize.instance.serialize(undefined, { withAlternate: true, withCursor: true }); const outputString = JSON.stringify(output); document.getElementById('serialize-output').innerText = outputString; From 00a8b80ef729c9ed89a44288425c592601be8df1 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Wed, 16 Sep 2020 14:28:08 +0800 Subject: [PATCH 03/18] Implement cursor adn alt screen. Fix background serialize --- .../src/SerializeAddon.ts | 168 +++++++++++++++--- .../test/SerializeAddon.api.ts | 18 ++ 2 files changed, 161 insertions(+), 25 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index e03707e0..3113777e 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -82,8 +82,16 @@ class StringSerializeHandler extends BaseSerializeHandler { private _currentRow: string = ''; private _nullCellCount: number = 0; - private _lastContentCellRow: number = 0; - private _lastContentCellCol: number = 0; + // this is a null cell for reference for checking whether background is empty or not + private _nullCell: IBufferCell = this._buffer1.getNullCell(); + + // we can see a full colored cell and a null cell that only have background the same style + // but the information isn't preserved by null cell itself + // so wee need to record it when required. + private _cursorStyle: IBufferCell = this._buffer1.getNullCell(); + + private _lastCursorRow: number = 0; + private _lastCursorCol: number = 0; constructor(private _buffer1: IBuffer,private _terminal: Terminal, private _option: ISerializeOptions = {}) { super(_buffer1); @@ -91,16 +99,35 @@ class StringSerializeHandler extends BaseSerializeHandler { protected _beforeSerialize(rows: number, start: number, end: number): void { this._allRows = new Array(rows); - this._lastContentCellRow = start; + this._lastCursorRow = start; } protected _rowEnd(row: number): void { + // if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing + if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._nullCell)) { + // use clear right to set background. + // use move right to move cursor. + this._currentRow += `\x1b[${this._nullCellCount}X`; + + // set the cursor back because we aren't there + this._lastCursorRow = row; + this._lastCursorCol = this._terminal.cols - this._nullCellCount; + + this._nullCellCount = 0; + + // perform a style reset before next line, + // because scroll when having background set will change the whole background of next line. + this._currentRow += `\x1b[m`; + // FIXME: we just get a new one because we can't reset it. + this._cursorStyle = this._buffer1.getNullCell(); + } + this._allRows[this._rowIndex++] = this._currentRow; this._currentRow = ''; this._nullCellCount = 0; } - protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + private _diffStyle (cell: IBufferCell, oldCell: IBufferCell): number[] { const sgrSeq: number[] = []; const fgChanged = !equalFg(cell, oldCell); const bgChanged = !equalBg(cell, oldCell); @@ -108,7 +135,9 @@ class StringSerializeHandler extends BaseSerializeHandler { if (fgChanged || bgChanged || flagsChanged) { if (cell.isAttributeDefault()) { - this._currentRow += '\x1b[0m'; + if (!oldCell.isAttributeDefault()) { + sgrSeq.push(0); + } } else { if (fgChanged) { const color = cell.getFgColor(); @@ -140,23 +169,76 @@ class StringSerializeHandler extends BaseSerializeHandler { } } - if (sgrSeq.length) { + return sgrSeq; + } + + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + // a width 0 cell don't need to be count because it is just a placeholder after a CJK character; + const isPlaceHolderCell = cell.getWidth() === 0; + + if (isPlaceHolderCell) { + return; + } + + // this cell don't have content + const isEmptyCell = cell.getChars() === ''; + + // this cell don't have content and style + const isNullCell = cell.getWidth() === 1 && cell.getChars() === '' && cell.isAttributeDefault(); + + const sgrSeq = this._diffStyle(cell, this._cursorStyle); + + // the empty cell style is only assumed to be changed when background changed, because foreground is always 0. + const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0; + + /** + * handles style change + */ + if (styleChanged) { + // before update the style, we need to fill empty cell back + if (this._nullCellCount > 0) { + // use clear right to set background. + // use move right to move cursor. + if (equalBg(this._cursorStyle, this._nullCell)) { + this._currentRow += `\x1b[${this._nullCellCount}C`; + } else { + this._currentRow += `\x1b[${this._nullCellCount}X`; + this._currentRow += `\x1b[${this._nullCellCount}C`; + } + this._nullCellCount = 0; + } + this._currentRow += `\x1b[${sgrSeq.join(';')}m`; + + // update the last cursor style + this._buffer1.getLine(row)?.getCell(col, this._cursorStyle); } - // Count number of null cells encountered after the last non-null cell and move the cursor - // if a non-null cell is found (eg. \t or cursor move) - if (cell.getChars() === '') { + /** + * handles actual content + */ + if (isEmptyCell) { this._nullCellCount += cell.getWidth(); - } else if (this._nullCellCount > 0) { - this._currentRow += `\x1b[${this._nullCellCount}C`; - this._nullCellCount = 0; } else { - this._lastContentCellRow = row; - this._lastContentCellCol = col + cell.getWidth(); + if (this._nullCellCount > 0) { + // we can just assume we have same style with previous one here + // because style change is handled by previous stage + // use move right when background is empty, use clear right when there is background. + if (equalBg(this._cursorStyle, this._nullCell)) { + this._currentRow += `\x1b[${this._nullCellCount}C`; + } else { + this._currentRow += `\x1b[${this._nullCellCount}X`; + this._currentRow += `\x1b[${this._nullCellCount}C`; + } + this._nullCellCount = 0; + } + this._currentRow += cell.getChars(); } - this._currentRow += cell.getChars(); + if (!isNullCell) { + this._lastCursorRow = row; + this._lastCursorCol = col + cell.getWidth(); + } } protected _serializeString(): string { @@ -170,21 +252,21 @@ class StringSerializeHandler extends BaseSerializeHandler { let content = this._allRows.slice(0, rowEnd).join('\r\n'); - if (this._option.withCursor) { + if (this._option.withCursor ?? true) { const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY; const realCursorCol = this._buffer1.cursorX; const hasScroll = this._buffer1.length > this._terminal.rows!; - const hasEmptyLine = hasScroll ? (this._buffer1.length - 1 > this._lastContentCellRow) : (realCursorRow > this._lastContentCellRow); + const hasEmptyLine = hasScroll ? (this._buffer1.length - 1 > this._lastCursorRow) : (realCursorRow > this._lastCursorRow); const cursorMoved = hasScroll ? hasEmptyLine ? (realCursorCol !== 0 || realCursorRow !== this._buffer1.length - 1) - : (realCursorRow !== this._lastContentCellRow || realCursorCol !== this._lastContentCellCol) + : (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol) : hasEmptyLine // we don't need to check the row because empty row count are based on cursor ? realCursorCol !== 0 - : (realCursorRow !== this._lastContentCellRow || realCursorCol !== this._lastContentCellCol); + : (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); const moveRight = (offset: number): void => { if (offset > 0) { @@ -204,9 +286,9 @@ class StringSerializeHandler extends BaseSerializeHandler { // Fix empty lines if (hasEmptyLine) { if (hasScroll) { - content += '\r\n'.repeat(this._buffer1.length - 1 - this._lastContentCellRow); + content += '\r\n'.repeat(this._buffer1.length - 1 - this._lastCursorRow); } else { - content += '\r\n'.repeat(realCursorRow - this._lastContentCellRow); + content += '\r\n'.repeat(realCursorRow - this._lastCursorRow); } } @@ -219,8 +301,8 @@ class StringSerializeHandler extends BaseSerializeHandler { moveRight(realCursorCol); } } else { - moveDown(realCursorRow - this._lastContentCellRow); - moveRight(realCursorCol - this._lastContentCellCol); + moveDown(realCursorRow - this._lastCursorRow); + moveRight(realCursorCol - this._lastCursorCol); } } } @@ -247,15 +329,51 @@ export class SerializeAddon implements ITerminalAddon { return result; } + public inspectBuffer(buffer: IBuffer): { x: number, y: number, data: any[][] } { + const lines: any[] = []; + const cell = buffer.getNullCell(); + + for (let i = 0; i < buffer.length; i++) { + const line = []; + const bufferLine = buffer.getLine(i)!; + for (let j = 0; j < bufferLine.length; j++) { + const cellData: any = {}; + bufferLine.getCell(j, cell)!; + cellData.getBgColor = cell.getBgColor(); + cellData.getBgColorMode = cell.getBgColorMode(); + cellData.getChars = cell.getChars(); + cellData.getCode = cell.getCode(); + cellData.getFgColor = cell.getFgColor(); + cellData.getFgColorMode = cell.getFgColorMode(); + cellData.getWidth = cell.getWidth(); + cellData.isAttributeDefault = cell.isAttributeDefault(); + cellData.isBlink = cell.isBlink(); + cellData.isBold = cell.isBold(); + cellData.isDim = cell.isDim(); + cellData.isInverse = cell.isInverse(); + cellData.isInvisible = cell.isInvisible(); + + line.push(cellData); + } + + lines.push(line); + } + + return { + x: buffer.cursorX, + y: buffer.cursorY, + data: lines + }; + } + public serialize(rows?: number, options: ISerializeOptions = {}): string { - // TODO: Add re-position cursor support // TODO: Add word wrap mode support // TODO: Add combinedData support if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - if (this._terminal.buffer.active.type === 'normal' || !(options.withAlternate ?? false)) { + if (this._terminal.buffer.active.type === 'normal' || !(options.withAlternate ?? true)) { return this._getString(this._terminal.buffer.active, rows, options); } diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 32e816a1..a63e386b 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -331,6 +331,24 @@ describe('SerializeAddon', () => { assert.equal(JSON.stringify(await page.evaluate(`window.term.buffer.active === window.term.buffer.alt`)), 'false'); assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize(undefined, { withAlternate: true });`)), JSON.stringify(expected.join('\r\n'))); }); + + it('serialize with background', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + + const lines = [ + `1\u001b[44m${CLEAR_RIGHT(5)}`, + `2${CLEAR_RIGHT(9)}` + ]; + + await writeSync(page, lines.join('\\r\\n')); + const originalBuffer = await page.evaluate(`serializeAddon.inspectBuffer(term.buffer.normal);`); + const result = await page.evaluate(`serializeAddon.serialize(undefined, { withAlternate: true, withCursor: true });`); + + await writeSync(page, '\' +' + JSON.stringify('\x1bc' + result) + '+ \''); + const newBuffer = await page.evaluate(`serializeAddon.inspectBuffer(term.buffer.normal);`); + + assert.deepEqual(originalBuffer, newBuffer); + }); }); function newArray(initial: T | ((index: number) => T), count: number): T[] { From 8518b354a71c6ef3fb1b2e7c5fed51ff04be401a Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Wed, 16 Sep 2020 14:45:14 +0800 Subject: [PATCH 04/18] Fix test that breaks by the option change --- .../src/SerializeAddon.ts | 14 ++++++++------ .../test/SerializeAddon.api.ts | 19 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 3113777e..2363e326 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -320,10 +320,11 @@ export class SerializeAddon implements ITerminalAddon { this._terminal = terminal; } - private _getString(buffer: IBuffer, rows?: number, option?: ISerializeOptions): string { + private _getString(buffer: IBuffer, scrollback?: number, option?: ISerializeOptions): string { const maxRows = buffer.length; const handler = new StringSerializeHandler(buffer, this._terminal!, option); - const correctRows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows); + + const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + this!._terminal!.rows, 0, maxRows); const result = handler.serialize(maxRows - correctRows, maxRows); return result; @@ -366,7 +367,7 @@ export class SerializeAddon implements ITerminalAddon { }; } - public serialize(rows?: number, options: ISerializeOptions = {}): string { + public serialize(scrollback?: number, options: ISerializeOptions = {}): string { // TODO: Add word wrap mode support // TODO: Add combinedData support if (!this._terminal) { @@ -374,11 +375,12 @@ export class SerializeAddon implements ITerminalAddon { } if (this._terminal.buffer.active.type === 'normal' || !(options.withAlternate ?? true)) { - return this._getString(this._terminal.buffer.active, rows, options); + return this._getString(this._terminal.buffer.active, scrollback, options); } - const normalScreenContent = this._getString(this._terminal.buffer.normal, rows, options); - const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, rows, options); + const normalScreenContent = this._getString(this._terminal.buffer.normal, scrollback, options); + // alt screen don't have scrollback + const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, undefined, options); return normalScreenContent + '\u001b[?1049h\u001b[H' diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index a63e386b..0401953d 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -39,7 +39,7 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize();`), ''); }); - it('trim last empty lines', async function(): Promise { + it('preserve last empty lines', async function(): Promise { const cols = 10; const lines = [ '', @@ -55,7 +55,7 @@ describe('SerializeAddon', () => { '' ]; await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.slice(0, 8).join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('digits content', async function(): Promise { @@ -67,21 +67,22 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); - it('serialize half rows of content', async function(): Promise { - const rows = 10; - const halfRows = rows >> 1; + it('serialize with half of scrollback', async function(): Promise { + const rows = 20; + const scrollback = rows - 10; + const halfScrollback = scrollback / 2; const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize(${halfScrollback});`), lines.slice(halfScrollback, rows).join('\r\n')); }); - it('serialize 0 rows of content', async function(): Promise { - const rows = 10; + it('serialize 0 rows of scrollback', async function(): Promise { + const rows = 20; const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); + assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), lines.slice(rows - 10, rows).join('\r\n')); }); it('serialize all rows of content with color16', async function(): Promise { From 10282e4cb1d0d1d0a26cefa679afeb0cda82479e Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Wed, 16 Sep 2020 14:54:15 +0800 Subject: [PATCH 05/18] Move the test util to static method --- .../src/SerializeAddon.ts | 43 ++++++++++--------- .../test/SerializeAddon.api.ts | 5 ++- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 2363e326..32cab90c 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -330,7 +330,28 @@ export class SerializeAddon implements ITerminalAddon { return result; } - public inspectBuffer(buffer: IBuffer): { x: number, y: number, data: any[][] } { + public serialize(scrollback?: number, options: ISerializeOptions = {}): string { + // TODO: Add word wrap mode support + // TODO: Add combinedData support + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } + + if (this._terminal.buffer.active.type === 'normal' || !(options.withAlternate ?? true)) { + return this._getString(this._terminal.buffer.active, scrollback, options); + } + + const normalScreenContent = this._getString(this._terminal.buffer.normal, scrollback, options); + // alt screen don't have scrollback + const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, undefined, options); + + return normalScreenContent + + '\u001b[?1049h\u001b[H' + + alternativeScreenContent; + } + + // this is a util used only for test + private static _inspectBuffer(buffer: IBuffer): { x: number, y: number, data: any[][] } { const lines: any[] = []; const cell = buffer.getNullCell(); @@ -367,25 +388,5 @@ export class SerializeAddon implements ITerminalAddon { }; } - public serialize(scrollback?: number, options: ISerializeOptions = {}): string { - // TODO: Add word wrap mode support - // TODO: Add combinedData support - if (!this._terminal) { - throw new Error('Cannot use addon until it has been loaded'); - } - - if (this._terminal.buffer.active.type === 'normal' || !(options.withAlternate ?? true)) { - return this._getString(this._terminal.buffer.active, scrollback, options); - } - - const normalScreenContent = this._getString(this._terminal.buffer.normal, scrollback, options); - // alt screen don't have scrollback - const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, undefined, options); - - return normalScreenContent - + '\u001b[?1049h\u001b[H' - + alternativeScreenContent; - } - public dispose(): void { } } diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 0401953d..281a3154 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -342,11 +342,12 @@ describe('SerializeAddon', () => { ]; await writeSync(page, lines.join('\\r\\n')); - const originalBuffer = await page.evaluate(`serializeAddon.inspectBuffer(term.buffer.normal);`); + const originalBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + const result = await page.evaluate(`serializeAddon.serialize(undefined, { withAlternate: true, withCursor: true });`); await writeSync(page, '\' +' + JSON.stringify('\x1bc' + result) + '+ \''); - const newBuffer = await page.evaluate(`serializeAddon.inspectBuffer(term.buffer.normal);`); + const newBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); assert.deepEqual(originalBuffer, newBuffer); }); From 6c0163009051652ac8348ddcc03067e8da713d86 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Wed, 16 Sep 2020 15:13:30 +0800 Subject: [PATCH 06/18] Remove the unused options --- .../src/SerializeAddon.ts | 117 +++++++++--------- .../test/SerializeAddon.api.ts | 6 +- demo/client.ts | 2 +- 3 files changed, 60 insertions(+), 65 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 32cab90c..f4dcf35b 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -11,11 +11,6 @@ function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); } -interface ISerializeOptions { - withAlternate?: boolean; - withCursor?: boolean; -} - // TODO: Refine this template class later abstract class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } @@ -93,7 +88,7 @@ class StringSerializeHandler extends BaseSerializeHandler { private _lastCursorRow: number = 0; private _lastCursorCol: number = 0; - constructor(private _buffer1: IBuffer,private _terminal: Terminal, private _option: ISerializeOptions = {}) { + constructor(private _buffer1: IBuffer,private _terminal: Terminal) { super(_buffer1); } @@ -252,61 +247,61 @@ class StringSerializeHandler extends BaseSerializeHandler { let content = this._allRows.slice(0, rowEnd).join('\r\n'); - if (this._option.withCursor ?? true) { - const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY; - const realCursorCol = this._buffer1.cursorX; + // restore the cursor + const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY; + const realCursorCol = this._buffer1.cursorX; - const hasScroll = this._buffer1.length > this._terminal.rows!; - const hasEmptyLine = hasScroll ? (this._buffer1.length - 1 > this._lastCursorRow) : (realCursorRow > this._lastCursorRow); - const cursorMoved = - hasScroll - ? hasEmptyLine - ? (realCursorCol !== 0 || realCursorRow !== this._buffer1.length - 1) - : (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol) - : hasEmptyLine - // we don't need to check the row because empty row count are based on cursor - ? realCursorCol !== 0 - : (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); + const hasScroll = this._buffer1.length > this._terminal.rows!; + const hasEmptyLine = hasScroll ? (this._buffer1.length - 1 > this._lastCursorRow) : (realCursorRow > this._lastCursorRow); + const cursorMoved = + hasScroll + ? hasEmptyLine + ? (realCursorCol !== 0 || realCursorRow !== this._buffer1.length - 1) + : (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol) + : hasEmptyLine + // we don't need to check the row because empty row count are based on cursor + ? realCursorCol !== 0 + : (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); - const moveRight = (offset: number): void => { - if (offset > 0) { - content += `\u001b[${offset}C`; - } else if (offset < 0) { - content += `\u001b[${-offset}D`; - } - }; - const moveDown = (offset: number): void => { - if (offset > 0) { - content += `\u001b[${offset}B`; - } else if (offset < 0) { - content += `\u001b[${-offset}A`; - } - }; - - // Fix empty lines - if (hasEmptyLine) { - if (hasScroll) { - content += '\r\n'.repeat(this._buffer1.length - 1 - this._lastCursorRow); - } else { - content += '\r\n'.repeat(realCursorRow - this._lastCursorRow); - } + const moveRight = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}C`; + } else if (offset < 0) { + content += `\u001b[${-offset}D`; } + }; + const moveDown = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}B`; + } else if (offset < 0) { + content += `\u001b[${-offset}A`; + } + }; - if (cursorMoved) { - if (hasEmptyLine) { - if (hasScroll) { - moveRight(realCursorCol); - moveDown(realCursorRow - (this._buffer1.length - 1)); - } else { - moveRight(realCursorCol); - } - } else { - moveDown(realCursorRow - this._lastCursorRow); - moveRight(realCursorCol - this._lastCursorCol); - } + // Fix empty lines + if (hasEmptyLine) { + if (hasScroll) { + content += '\r\n'.repeat(this._buffer1.length - 1 - this._lastCursorRow); + } else { + content += '\r\n'.repeat(realCursorRow - this._lastCursorRow); } } + if (cursorMoved) { + if (hasEmptyLine) { + if (hasScroll) { + moveRight(realCursorCol); + moveDown(realCursorRow - (this._buffer1.length - 1)); + } else { + moveRight(realCursorCol); + } + } else { + moveDown(realCursorRow - this._lastCursorRow); + moveRight(realCursorCol - this._lastCursorCol); + } + } + + return content; } } @@ -320,9 +315,9 @@ export class SerializeAddon implements ITerminalAddon { this._terminal = terminal; } - private _getString(buffer: IBuffer, scrollback?: number, option?: ISerializeOptions): string { + private _getString(buffer: IBuffer, scrollback?: number): string { const maxRows = buffer.length; - const handler = new StringSerializeHandler(buffer, this._terminal!, option); + const handler = new StringSerializeHandler(buffer, this._terminal!); const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + this!._terminal!.rows, 0, maxRows); const result = handler.serialize(maxRows - correctRows, maxRows); @@ -330,20 +325,20 @@ export class SerializeAddon implements ITerminalAddon { return result; } - public serialize(scrollback?: number, options: ISerializeOptions = {}): string { + public serialize(scrollback?: number): string { // TODO: Add word wrap mode support // TODO: Add combinedData support if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - if (this._terminal.buffer.active.type === 'normal' || !(options.withAlternate ?? true)) { - return this._getString(this._terminal.buffer.active, scrollback, options); + if (this._terminal.buffer.active.type === 'normal') { + return this._getString(this._terminal.buffer.active, scrollback); } - const normalScreenContent = this._getString(this._terminal.buffer.normal, scrollback, options); + const normalScreenContent = this._getString(this._terminal.buffer.normal, scrollback); // alt screen don't have scrollback - const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, undefined, options); + const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, undefined); return normalScreenContent + '\u001b[?1049h\u001b[H' diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 281a3154..2e570120 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -314,7 +314,7 @@ describe('SerializeAddon', () => { await writeSync(page, lines.join('\\r\\n')); assert.equal(JSON.stringify(await page.evaluate(`window.term.buffer.active.type`)), '"alternate"'); - assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize(undefined, { withAlternate: true });`)), JSON.stringify(expected.join('\r\n'))); + assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n'))); }); it('serialize without alt screen correctly', async () => { @@ -330,7 +330,7 @@ describe('SerializeAddon', () => { await writeSync(page, lines.join('\\r\\n')); assert.equal(JSON.stringify(await page.evaluate(`window.term.buffer.active === window.term.buffer.alt`)), 'false'); - assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize(undefined, { withAlternate: true });`)), JSON.stringify(expected.join('\r\n'))); + assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n'))); }); it('serialize with background', async () => { @@ -344,7 +344,7 @@ describe('SerializeAddon', () => { await writeSync(page, lines.join('\\r\\n')); const originalBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); - const result = await page.evaluate(`serializeAddon.serialize(undefined, { withAlternate: true, withCursor: true });`); + const result = await page.evaluate(`serializeAddon.serialize();`); await writeSync(page, '\' +' + JSON.stringify('\x1bc' + result) + '+ \''); const newBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); diff --git a/demo/client.ts b/demo/client.ts index 509b73a9..6efa181a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -413,7 +413,7 @@ function updateTerminalSize(): void { } function serializeButtonHandler(): void { - const output = addons.serialize.instance.serialize(undefined, { withAlternate: true, withCursor: true }); + const output = addons.serialize.instance.serialize(); const outputString = JSON.stringify(output); document.getElementById('serialize-output').innerText = outputString; From b89227d365f6e6e6062115c62ec8ba4491f926b3 Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Wed, 16 Sep 2020 22:47:23 +0800 Subject: [PATCH 07/18] Workaround firefox bug that yield -0 on bit op --- .../xterm-addon-serialize/test/SerializeAddon.api.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 2e570120..4d90f3c5 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -14,6 +14,8 @@ let page: Page; const width = 800; const height = 600; +const writeRawSync = (page: any, str: string): Promise => writeSync(page, '\' +' + JSON.stringify(str) + '+ \''); + describe('SerializeAddon', () => { before(async function(): Promise { const browserType = getBrowserType(); @@ -344,12 +346,14 @@ describe('SerializeAddon', () => { await writeSync(page, lines.join('\\r\\n')); const originalBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); - const result = await page.evaluate(`serializeAddon.serialize();`); - - await writeSync(page, '\' +' + JSON.stringify('\x1bc' + result) + '+ \''); + const result = await page.evaluate(`serializeAddon.serialize();`) as string; + await page.evaluate(`term.reset();`); + await writeRawSync(page, result); const newBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); - assert.deepEqual(originalBuffer, newBuffer); + // chai decides -0 and 0 are different number... + // and firefox have a bug that output -0 for unknown reason + assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); }); }); From 6904dc50092d979f47108d1fd89f2bfbf7fcd4e6 Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Fri, 18 Sep 2020 00:36:10 +0800 Subject: [PATCH 08/18] Handle BCE + line scroll Using an additional field for store the current background and compare against it to generate proper color sequence --- .../xterm-addon-serialize/src/MyBufferCell.ts | 96 +++++++++++++++++++ .../src/SerializeAddon.ts | 51 +++++----- .../test/SerializeAddon.api.ts | 27 ++++++ 3 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 addons/xterm-addon-serialize/src/MyBufferCell.ts diff --git a/addons/xterm-addon-serialize/src/MyBufferCell.ts b/addons/xterm-addon-serialize/src/MyBufferCell.ts new file mode 100644 index 00000000..56efcb3c --- /dev/null +++ b/addons/xterm-addon-serialize/src/MyBufferCell.ts @@ -0,0 +1,96 @@ +import { IBufferCell } from 'xterm'; + +/** + * This is a dummy buffer cell to hold data from real buffer cell + */ +export class MyBufferCell implements IBufferCell { + constructor (private _cell: IBufferCell) {} + private _width: number = this._cell.getWidth(); + private _chars: string = this._cell.getChars(); + private _code: number = this._cell.getCode(); + private _fgColorMode: number = this._cell.getFgColorMode(); + private _bgColorMode: number = this._cell.getBgColorMode(); + private _fgColor: number = this._cell.getFgColor(); + private _bgColor: number = this._cell.getBgColor(); + private _bold: number = this._cell.isBold(); + private _italic: number = this._cell.isItalic(); + private _dim: number = this._cell.isDim(); + private _underline: number = this._cell.isUnderline(); + private _blink: number = this._cell.isBlink(); + private _inverse: number = this._cell.isInverse(); + private _invisible: number = this._cell.isInvisible(); + private _fgRGB: boolean = this._cell.isFgRGB(); + private _bgRGB: boolean = this._cell.isBgRGB(); + private _fgPalette: boolean = this._cell.isFgPalette(); + private _bgPallette: boolean = this._cell.isBgPalette(); + private _fgDefault: boolean = this._cell.isFgDefault(); + private _bgDefault: boolean = this._cell.isBgDefault(); + private _attributeDefault: boolean = this._cell.isAttributeDefault(); + public getWidth(): number { + return this._width; + } + public getChars(): string { + return this._chars; + } + public getCode(): number { + return this._code; + } + public getFgColorMode(): number { + return this._fgColorMode; + } + public getBgColorMode(): number { + return this._bgColorMode; + } + public getFgColor(): number { + return this._fgColor; + } + public getBgColor(): number { + return this._bgColor; + } + public isBold(): number { + return this._bold; + } + public isItalic(): number { + return this._italic; + } + public isDim(): number { + return this._dim; + } + public isUnderline(): number { + return this._underline; + } + public isBlink(): number { + return this._blink; + } + public isInverse(): number { + return this._inverse; + } + public isInvisible(): number { + return this._invisible; + } + public isFgRGB(): boolean { + return this._fgRGB; + } + public isBgRGB(): boolean { + return this._bgRGB; + } + public isFgPalette(): boolean { + return this._fgPalette; + } + public isBgPalette(): boolean { + return this._bgPallette; + } + public isFgDefault(): boolean { + return this._fgDefault; + } + public isBgDefault(): boolean { + return this._bgDefault; + } + public isAttributeDefault(): boolean { + return this._attributeDefault; + } + + public static from(cell: IBufferCell): MyBufferCell { + return new MyBufferCell(cell); + } +} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index f4dcf35b..9518fcbf 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -6,6 +6,7 @@ */ import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; +import { MyBufferCell } from './MyBufferCell'; function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); @@ -36,7 +37,7 @@ abstract class BaseSerializeHandler { oldCell = c; } } - this._rowEnd(row); + this._rowEnd(row, row === endRow - 1); } this._afterSerialize(); @@ -45,7 +46,7 @@ abstract class BaseSerializeHandler { } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _rowEnd(row: number): void { } + protected _rowEnd(row: number, isLastRow: boolean): void { } protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { } protected _afterSerialize(): void { } protected _serializeString(): string { return ''; } @@ -71,20 +72,23 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { && cell1.isDim() === cell2.isDim(); } + + class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); private _currentRow: string = ''; private _nullCellCount: number = 0; - // this is a null cell for reference for checking whether background is empty or not - private _nullCell: IBufferCell = this._buffer1.getNullCell(); - // we can see a full colored cell and a null cell that only have background the same style // but the information isn't preserved by null cell itself // so wee need to record it when required. private _cursorStyle: IBufferCell = this._buffer1.getNullCell(); + // this is a null cell for reference for checking whether background is empty or not + private _backgroundCell: MyBufferCell = MyBufferCell.from(this._cursorStyle); + + private _firstRow: number = 0; private _lastCursorRow: number = 0; private _lastCursorCol: number = 0; @@ -95,26 +99,21 @@ class StringSerializeHandler extends BaseSerializeHandler { protected _beforeSerialize(rows: number, start: number, end: number): void { this._allRows = new Array(rows); this._lastCursorRow = start; + this._firstRow = start; } - protected _rowEnd(row: number): void { + protected _rowEnd(row: number, isLastRow: boolean): void { // if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing - if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._nullCell)) { + if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) { // use clear right to set background. // use move right to move cursor. this._currentRow += `\x1b[${this._nullCellCount}X`; + } - // set the cursor back because we aren't there - this._lastCursorRow = row; - this._lastCursorCol = this._terminal.cols - this._nullCellCount; - - this._nullCellCount = 0; - - // perform a style reset before next line, - // because scroll when having background set will change the whole background of next line. - this._currentRow += `\x1b[m`; - // FIXME: we just get a new one because we can't reset it. - this._cursorStyle = this._buffer1.getNullCell(); + if (!isLastRow) { + if (row - this._firstRow >= this._terminal.rows) { + this._backgroundCell = MyBufferCell.from(this._cursorStyle); + } } this._allRows[this._rowIndex++] = this._currentRow; @@ -178,9 +177,6 @@ class StringSerializeHandler extends BaseSerializeHandler { // this cell don't have content const isEmptyCell = cell.getChars() === ''; - // this cell don't have content and style - const isNullCell = cell.getWidth() === 1 && cell.getChars() === '' && cell.isAttributeDefault(); - const sgrSeq = this._diffStyle(cell, this._cursorStyle); // the empty cell style is only assumed to be changed when background changed, because foreground is always 0. @@ -194,7 +190,7 @@ class StringSerializeHandler extends BaseSerializeHandler { if (this._nullCellCount > 0) { // use clear right to set background. // use move right to move cursor. - if (equalBg(this._cursorStyle, this._nullCell)) { + if (equalBg(this._cursorStyle, this._backgroundCell)) { this._currentRow += `\x1b[${this._nullCellCount}C`; } else { this._currentRow += `\x1b[${this._nullCellCount}X`; @@ -203,6 +199,9 @@ class StringSerializeHandler extends BaseSerializeHandler { this._nullCellCount = 0; } + this._lastCursorRow = row; + this._lastCursorCol = col; + this._currentRow += `\x1b[${sgrSeq.join(';')}m`; // update the last cursor style @@ -219,7 +218,7 @@ class StringSerializeHandler extends BaseSerializeHandler { // we can just assume we have same style with previous one here // because style change is handled by previous stage // use move right when background is empty, use clear right when there is background. - if (equalBg(this._cursorStyle, this._nullCell)) { + if (equalBg(this._cursorStyle, this._backgroundCell)) { this._currentRow += `\x1b[${this._nullCellCount}C`; } else { this._currentRow += `\x1b[${this._nullCellCount}X`; @@ -227,10 +226,10 @@ class StringSerializeHandler extends BaseSerializeHandler { } this._nullCellCount = 0; } - this._currentRow += cell.getChars(); - } - if (!isNullCell) { + this._currentRow += cell.getChars(); + + // update cursor this._lastCursorRow = row; this._lastCursorCol = col + cell.getWidth(); } diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 4d90f3c5..afbe3b39 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -355,6 +355,33 @@ describe('SerializeAddon', () => { // and firefox have a bug that output -0 for unknown reason assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); }); + + it('cause the BCE on scroll', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + + const padLines = newArray( + (index: number) => digitsString(10, index), + 10 + ); + + const lines = [ + ...padLines, + `\u001b[44m${CLEAR_RIGHT(5)}1111111111111111` + ]; + + await writeSync(page, lines.join('\\r\\n')); + const originalBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + + const result = await page.evaluate(`serializeAddon.serialize();`) as string; + + await page.evaluate(`term.reset();`); + await writeRawSync(page, result); + const newBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + + // chai decides -0 and 0 are different number... + // and firefox have a bug that output -0 for unknown reason + assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); + }); }); function newArray(initial: T | ((index: number) => T), count: number): T[] { From 84dfe8fcccb283656216e03933cebe2623d236b5 Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Fri, 18 Sep 2020 01:20:41 +0800 Subject: [PATCH 09/18] CLearup comment --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 9518fcbf..a05250cf 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -106,7 +106,6 @@ class StringSerializeHandler extends BaseSerializeHandler { // if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) { // use clear right to set background. - // use move right to move cursor. this._currentRow += `\x1b[${this._nullCellCount}X`; } @@ -189,13 +188,11 @@ class StringSerializeHandler extends BaseSerializeHandler { // before update the style, we need to fill empty cell back if (this._nullCellCount > 0) { // use clear right to set background. - // use move right to move cursor. - if (equalBg(this._cursorStyle, this._backgroundCell)) { - this._currentRow += `\x1b[${this._nullCellCount}C`; - } else { + if (!equalBg(this._cursorStyle, this._backgroundCell)) { this._currentRow += `\x1b[${this._nullCellCount}X`; - this._currentRow += `\x1b[${this._nullCellCount}C`; } + // use move right to move cursor. + this._currentRow += `\x1b[${this._nullCellCount}C`; this._nullCellCount = 0; } From d7ed109f9d65b86599cd35eae517407486137757 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Fri, 18 Sep 2020 15:21:59 +0800 Subject: [PATCH 10/18] Support the isWrapped handling --- .../src/SerializeAddon.ts | 165 +++++++++++------- .../test/SerializeAddon.api.ts | 104 +++++++---- 2 files changed, 174 insertions(+), 95 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index a05250cf..025f1833 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -77,6 +77,7 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); + private _allRowSeparators: string[] = new Array(); private _currentRow: string = ''; private _nullCellCount: number = 0; @@ -91,6 +92,8 @@ class StringSerializeHandler extends BaseSerializeHandler { private _firstRow: number = 0; private _lastCursorRow: number = 0; private _lastCursorCol: number = 0; + private _lastContentCursorRow: number = 0; + private _lastContentCursorCol: number = 0; constructor(private _buffer1: IBuffer,private _terminal: Terminal) { super(_buffer1); @@ -98,6 +101,7 @@ class StringSerializeHandler extends BaseSerializeHandler { protected _beforeSerialize(rows: number, start: number, end: number): void { this._allRows = new Array(rows); + this._lastContentCursorRow = start; this._lastCursorRow = start; this._firstRow = start; } @@ -109,13 +113,82 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow += `\x1b[${this._nullCellCount}X`; } + let rowSeparator = ''; + + // handle row separator if (!isLastRow) { + // Enable BCE if (row - this._firstRow >= this._terminal.rows) { this._backgroundCell = MyBufferCell.from(this._cursorStyle); } + + // Fetch current line + const currentLine = this._buffer1.getLine(row)!; + // Fetch next line + const nextLine = this._buffer1.getLine(row + 1)!; + + if (!nextLine.isWrapped) { + // just insert the line break + rowSeparator = '\r\n'; + // we sended the enter + this._lastCursorRow = row + 1; + this._lastCursorCol = 0; + } else { + rowSeparator = ''; + const thisRowLastChar = currentLine.getCell(currentLine.length - 1)!; + const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2)!; + const nextRowFirstChar = nextLine.getCell(0)!; + const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1; + + // validate whether this line wrap is ever possible + let isValid = false; + + if ( + nextRowFirstChar.getChars() && + isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0 + ) { + if ( + (thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) && + equalBg(thisRowLastChar, nextRowFirstChar) + ) { + isValid = true; + } + + if ( + isNextRowFirstCharDoubleWidth && + (thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) && + equalBg(thisRowLastChar, nextRowFirstChar) && + equalBg(thisRowLastSecondChar, nextRowFirstChar) + ) { + isValid = true; + } + } + + if (!isValid) { + // force the wrap with magic + // insert enough character to force the wrap + rowSeparator = '-'.repeat(this._nullCellCount + 1); + // move back and erase next line head + rowSeparator += '\x1b[1D\x1b[1X'; + + // do these because we filled the last several null slot, which we shouldn't + if (this._nullCellCount > 0) { + rowSeparator += '\x1b[A'; + rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}C`; + rowSeparator += `\x1b[${this._nullCellCount}X`; + rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}D`; + rowSeparator += '\x1b[B'; + } + + // force commit the cursor position + this._lastCursorRow = row + 1; + this._lastCursorCol = 0; + } + } } - this._allRows[this._rowIndex++] = this._currentRow; + this._allRows[this._rowIndex] = this._currentRow; + this._allRowSeparators[this._rowIndex++] = rowSeparator; this._currentRow = ''; this._nullCellCount = 0; } @@ -196,8 +269,8 @@ class StringSerializeHandler extends BaseSerializeHandler { this._nullCellCount = 0; } - this._lastCursorRow = row; - this._lastCursorCol = col; + this._lastContentCursorRow = this._lastCursorRow = row; + this._lastContentCursorCol = this._lastCursorCol = col; this._currentRow += `\x1b[${sgrSeq.join(';')}m`; @@ -227,37 +300,36 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow += cell.getChars(); // update cursor - this._lastCursorRow = row; - this._lastCursorCol = col + cell.getWidth(); + this._lastContentCursorRow = this._lastCursorRow = row; + this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth(); } } protected _serializeString(): string { let rowEnd = this._allRows.length; - for (; rowEnd > 0; rowEnd--) { - if (this._allRows[rowEnd - 1]) { - break; - } + // the fixup is only required for data without scrollback + // because it will always be placed at last line otherwise + if (this._buffer1.length - this._firstRow <= this._terminal.rows) { + rowEnd = this._lastContentCursorRow + 1 - this._firstRow; + this._lastCursorCol = this._lastContentCursorCol; + this._lastCursorRow = this._lastContentCursorRow; } - let content = this._allRows.slice(0, rowEnd).join('\r\n'); + let content = ''; + + for (let i = 0; i < rowEnd; i++) { + content += this._allRows[i]; + if (i + 1 < rowEnd) { + content += this._allRowSeparators[i]; + } + } // restore the cursor const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY; const realCursorCol = this._buffer1.cursorX; - const hasScroll = this._buffer1.length > this._terminal.rows!; - const hasEmptyLine = hasScroll ? (this._buffer1.length - 1 > this._lastCursorRow) : (realCursorRow > this._lastCursorRow); - const cursorMoved = - hasScroll - ? hasEmptyLine - ? (realCursorCol !== 0 || realCursorRow !== this._buffer1.length - 1) - : (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol) - : hasEmptyLine - // we don't need to check the row because empty row count are based on cursor - ? realCursorCol !== 0 - : (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); + const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); const moveRight = (offset: number): void => { if (offset > 0) { @@ -274,27 +346,9 @@ class StringSerializeHandler extends BaseSerializeHandler { } }; - // Fix empty lines - if (hasEmptyLine) { - if (hasScroll) { - content += '\r\n'.repeat(this._buffer1.length - 1 - this._lastCursorRow); - } else { - content += '\r\n'.repeat(realCursorRow - this._lastCursorRow); - } - } - if (cursorMoved) { - if (hasEmptyLine) { - if (hasScroll) { - moveRight(realCursorCol); - moveDown(realCursorRow - (this._buffer1.length - 1)); - } else { - moveRight(realCursorCol); - } - } else { - moveDown(realCursorRow - this._lastCursorRow); - moveRight(realCursorCol - this._lastCursorCol); - } + moveDown(realCursorRow - this._lastCursorRow); + moveRight(realCursorCol - this._lastCursorCol); } @@ -322,7 +376,6 @@ export class SerializeAddon implements ITerminalAddon { } public serialize(scrollback?: number): string { - // TODO: Add word wrap mode support // TODO: Add combinedData support if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); @@ -344,32 +397,14 @@ export class SerializeAddon implements ITerminalAddon { // this is a util used only for test private static _inspectBuffer(buffer: IBuffer): { x: number, y: number, data: any[][] } { const lines: any[] = []; - const cell = buffer.getNullCell(); for (let i = 0; i < buffer.length; i++) { - const line = []; - const bufferLine = buffer.getLine(i)!; - for (let j = 0; j < bufferLine.length; j++) { - const cellData: any = {}; - bufferLine.getCell(j, cell)!; - cellData.getBgColor = cell.getBgColor(); - cellData.getBgColorMode = cell.getBgColorMode(); - cellData.getChars = cell.getChars(); - cellData.getCode = cell.getCode(); - cellData.getFgColor = cell.getFgColor(); - cellData.getFgColorMode = cell.getFgColorMode(); - cellData.getWidth = cell.getWidth(); - cellData.isAttributeDefault = cell.isAttributeDefault(); - cellData.isBlink = cell.isBlink(); - cellData.isBold = cell.isBold(); - cellData.isDim = cell.isDim(); - cellData.isInverse = cell.isInverse(); - cellData.isInvisible = cell.isInvisible(); + /** + * Do this intentionally to get content of underlining source + */ + const bufferLine = (buffer.getLine(i)! as any)._line; - line.push(cellData); - } - - lines.push(line); + lines.push(JSON.stringify(bufferLine)); } return { diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index afbe3b39..4bf6d1b5 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -16,6 +16,20 @@ const height = 600; const writeRawSync = (page: any, str: string): Promise => writeSync(page, '\' +' + JSON.stringify(str) + '+ \''); +const testNormalScreenEqual = async (page: any, str: string): Promise => { + await writeRawSync(page, str); + const originalBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + + const result = await page.evaluate(`serializeAddon.serialize();`) as string; + await page.evaluate(`term.reset();`); + await writeRawSync(page, result); + const newBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + + // chai decides -0 and 0 are different number... + // and firefox have a bug that output -0 for unknown reason + assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); +}; + describe('SerializeAddon', () => { before(async function(): Promise { const browserType = getBrowserType(); @@ -35,12 +49,54 @@ describe('SerializeAddon', () => { after(async () => await browser.close()); beforeEach(async () => await page.evaluate(`window.term.reset()`)); + it('produce different output when we call test util with different text', async function(): Promise { + await writeRawSync(page, '12345'); + const buffer1 = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + + await page.evaluate(`term.reset();`); + await writeRawSync(page, '67890'); + const buffer2 = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + + assert.throw(() => { + assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2)); + }); + }); + + it('produce different output when we call test util with different line wrap', async function(): Promise { + await writeRawSync(page, '1234567890\r\n12345'); + const buffer3 = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + + await page.evaluate(`term.reset();`); + await writeRawSync(page, '1234567890n12345'); + const buffer4 = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + + assert.throw(() => { + assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4)); + }); + }); + it('empty content', async function(): Promise { const rows = 10; const cols = 10; assert.equal(await page.evaluate(`serializeAddon.serialize();`), ''); }); + it('unwrap wrapped line', async function(): Promise { + const lines = ['123456789123456789']; + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('does not unwrap non-wrapped line', async function(): Promise { + const lines = [ + '123456789', + '123456789' + ]; + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('preserve last empty lines', async function(): Promise { const cols = 10; const lines = [ @@ -281,15 +337,8 @@ describe('SerializeAddon', () => { '中文12', '1中文中文中' // this line is going to be wrapped at last character because it has line length of 11 (1+2*5) ]; - const expected = [ - '中文中文', - '12中文', - '中文12', - '1中文中文', - '中' - ]; await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize CJK Mixed with tab correctly', async () => { @@ -315,7 +364,7 @@ describe('SerializeAddon', () => { ]; await writeSync(page, lines.join('\\r\\n')); - assert.equal(JSON.stringify(await page.evaluate(`window.term.buffer.active.type`)), '"alternate"'); + assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate'); assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n'))); }); @@ -331,7 +380,7 @@ describe('SerializeAddon', () => { ]; await writeSync(page, lines.join('\\r\\n')); - assert.equal(JSON.stringify(await page.evaluate(`window.term.buffer.active === window.term.buffer.alt`)), 'false'); + assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal'); assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n'))); }); @@ -343,17 +392,7 @@ describe('SerializeAddon', () => { `2${CLEAR_RIGHT(9)}` ]; - await writeSync(page, lines.join('\\r\\n')); - const originalBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); - - const result = await page.evaluate(`serializeAddon.serialize();`) as string; - await page.evaluate(`term.reset();`); - await writeRawSync(page, result); - const newBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); - - // chai decides -0 and 0 are different number... - // and firefox have a bug that output -0 for unknown reason - assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); + await testNormalScreenEqual(page, lines.join('\r\n')); }); it('cause the BCE on scroll', async () => { @@ -369,18 +408,23 @@ describe('SerializeAddon', () => { `\u001b[44m${CLEAR_RIGHT(5)}1111111111111111` ]; - await writeSync(page, lines.join('\\r\\n')); - const originalBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + await testNormalScreenEqual(page, lines.join('\r\n')); + }); + it('handle invalid wrap', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + const MOVE_UP = (l: number): string => `\u001b[${l}A`; + const MOVE_DOWN = (l: number): string => `\u001b[${l}B`; - const result = await page.evaluate(`serializeAddon.serialize();`) as string; + const padLines = newArray( + (index: number) => digitsString(10, index), + 10 + ); - await page.evaluate(`term.reset();`); - await writeRawSync(page, result); - const newBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + const lines = [ + `\u001b[44m${CLEAR_RIGHT(5)}123456789012345${MOVE_UP(1)}${CLEAR_RIGHT(5)}${MOVE_DOWN(1)}` + ]; - // chai decides -0 and 0 are different number... - // and firefox have a bug that output -0 for unknown reason - assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); + await testNormalScreenEqual(page, lines.join('\r\n')); }); }); From aee1629c258939d9034f066bba1a91b99df8dc6b Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Fri, 18 Sep 2020 21:02:56 +0800 Subject: [PATCH 11/18] Reuse the cell object to redice gc --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 025f1833..dd34f780 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -106,6 +106,9 @@ class StringSerializeHandler extends BaseSerializeHandler { this._firstRow = start; } + private _thisRowLastChar: IBufferCell = this._buffer1.getNullCell(); + private _thisRowLastSecondChar: IBufferCell = this._buffer1.getNullCell(); + private _nextRowFirstChar: IBufferCell = this._buffer1.getNullCell(); protected _rowEnd(row: number, isLastRow: boolean): void { // if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) { @@ -135,9 +138,9 @@ class StringSerializeHandler extends BaseSerializeHandler { this._lastCursorCol = 0; } else { rowSeparator = ''; - const thisRowLastChar = currentLine.getCell(currentLine.length - 1)!; - const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2)!; - const nextRowFirstChar = nextLine.getCell(0)!; + const thisRowLastChar = currentLine.getCell(currentLine.length - 1, this._thisRowLastChar)!; + const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2, this._thisRowLastSecondChar)!; + const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!; const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1; // validate whether this line wrap is ever possible From 47be9a2df4b7238c303357c658068d55a8a5ea68 Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Fri, 18 Sep 2020 22:02:30 +0800 Subject: [PATCH 12/18] Fix another error about impossible wrap --- .../src/SerializeAddon.ts | 5 ++ .../test/SerializeAddon.api.ts | 50 +++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index dd34f780..ec8c6d0d 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -181,6 +181,11 @@ class StringSerializeHandler extends BaseSerializeHandler { rowSeparator += `\x1b[${this._nullCellCount}X`; rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}D`; rowSeparator += '\x1b[B'; + + // This is content even it is invisible + // without this, wrap will be missing + this._lastContentCursorRow = row + 1; + this._lastContentCursorCol = 0; } // force commit the cursor position diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 4bf6d1b5..5dc29d0d 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -335,7 +335,12 @@ describe('SerializeAddon', () => { '中文中文', '12中文', '中文12', - '1中文中文中' // this line is going to be wrapped at last character because it has line length of 11 (1+2*5) + // This line is going to be wrapped at last character + // because it has line length of 11 (1+2*5). + // We concat it back without the null cell currently. + // But this may be incorrect. + // see also #3097 + '1中文中文中' ]; await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); @@ -410,21 +415,58 @@ describe('SerializeAddon', () => { await testNormalScreenEqual(page, lines.join('\r\n')); }); - it('handle invalid wrap', async () => { + + it('handle invalid wrap before scroll', async () => { const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; const MOVE_UP = (l: number): string => `\u001b[${l}A`; const MOVE_DOWN = (l: number): string => `\u001b[${l}B`; + const MOVE_LEFT = (l: number): string => `\u001b[${l}D`; + + // A line wrap happened after current line. + // But there is no content. + // so wrap shouldn't even be able to happen. + const segments = [ + `123456789012345`, + MOVE_UP(1), + CLEAR_RIGHT(5), + MOVE_DOWN(1), + MOVE_LEFT(5), + CLEAR_RIGHT(5), + MOVE_UP(1), + '1' + ]; + + await testNormalScreenEqual(page, segments.join('')); + }); + + it('handle invalid wrap after scroll', async () => { + const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; + const MOVE_UP = (l: number): string => `\u001b[${l}A`; + const MOVE_DOWN = (l: number): string => `\u001b[${l}B`; + const MOVE_LEFT = (l: number): string => `\u001b[${l}D`; const padLines = newArray( (index: number) => digitsString(10, index), 10 ); + // A line wrap happened after current line. + // But there is no content. + // so wrap shouldn't even be able to happen. const lines = [ - `\u001b[44m${CLEAR_RIGHT(5)}123456789012345${MOVE_UP(1)}${CLEAR_RIGHT(5)}${MOVE_DOWN(1)}` + padLines.join('\r\n'), + '\r\n', + `123456789012345`, + MOVE_UP(1), + CLEAR_RIGHT(5), + MOVE_DOWN(1), + MOVE_LEFT(5), + CLEAR_RIGHT(5), + MOVE_UP(1), + '1' ]; - await testNormalScreenEqual(page, lines.join('\r\n')); + await testNormalScreenEqual(page, lines.join('')); }); }); From 354f3e03a2bb3aa364bef6346971b50dcd63b7ea Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Sat, 23 Jan 2021 10:19:03 +0800 Subject: [PATCH 13/18] Fix: inconsistent styles Co-authored-by: Daniel Imms --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index ec8c6d0d..9641dc20 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -95,7 +95,7 @@ class StringSerializeHandler extends BaseSerializeHandler { private _lastContentCursorRow: number = 0; private _lastContentCursorCol: number = 0; - constructor(private _buffer1: IBuffer,private _terminal: Terminal) { + constructor(private _buffer1: IBuffer, private _terminal: Terminal) { super(_buffer1); } @@ -201,7 +201,7 @@ class StringSerializeHandler extends BaseSerializeHandler { this._nullCellCount = 0; } - private _diffStyle (cell: IBufferCell, oldCell: IBufferCell): number[] { + private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] { const sgrSeq: number[] = []; const fgChanged = !equalFg(cell, oldCell); const bgChanged = !equalBg(cell, oldCell); @@ -337,7 +337,7 @@ class StringSerializeHandler extends BaseSerializeHandler { const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY; const realCursorCol = this._buffer1.cursorX; - const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); + const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); const moveRight = (offset: number): void => { if (offset > 0) { From cd501f9dcc445737160b982876146de0c67cb1ba Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Sat, 23 Jan 2021 10:20:03 +0800 Subject: [PATCH 14/18] Update addons/xterm-addon-serialize/src/MyBufferCell.ts Co-authored-by: Daniel Imms --- addons/xterm-addon-serialize/src/MyBufferCell.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-serialize/src/MyBufferCell.ts b/addons/xterm-addon-serialize/src/MyBufferCell.ts index 56efcb3c..d635a424 100644 --- a/addons/xterm-addon-serialize/src/MyBufferCell.ts +++ b/addons/xterm-addon-serialize/src/MyBufferCell.ts @@ -4,7 +4,7 @@ import { IBufferCell } from 'xterm'; * This is a dummy buffer cell to hold data from real buffer cell */ export class MyBufferCell implements IBufferCell { - constructor (private _cell: IBufferCell) {} + constructor(private _cell: IBufferCell) {} private _width: number = this._cell.getWidth(); private _chars: string = this._cell.getChars(); private _code: number = this._cell.getCode(); From a2480bae51b86a368cea3784b4bcb07e9e139caa Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Sat, 23 Jan 2021 11:57:08 +0800 Subject: [PATCH 15/18] Remove dummy buffer cell in favor of just remember the location --- .../xterm-addon-serialize/src/MyBufferCell.ts | 96 ------------------- .../src/SerializeAddon.ts | 18 +++- 2 files changed, 14 insertions(+), 100 deletions(-) delete mode 100644 addons/xterm-addon-serialize/src/MyBufferCell.ts diff --git a/addons/xterm-addon-serialize/src/MyBufferCell.ts b/addons/xterm-addon-serialize/src/MyBufferCell.ts deleted file mode 100644 index d635a424..00000000 --- a/addons/xterm-addon-serialize/src/MyBufferCell.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { IBufferCell } from 'xterm'; - -/** - * This is a dummy buffer cell to hold data from real buffer cell - */ -export class MyBufferCell implements IBufferCell { - constructor(private _cell: IBufferCell) {} - private _width: number = this._cell.getWidth(); - private _chars: string = this._cell.getChars(); - private _code: number = this._cell.getCode(); - private _fgColorMode: number = this._cell.getFgColorMode(); - private _bgColorMode: number = this._cell.getBgColorMode(); - private _fgColor: number = this._cell.getFgColor(); - private _bgColor: number = this._cell.getBgColor(); - private _bold: number = this._cell.isBold(); - private _italic: number = this._cell.isItalic(); - private _dim: number = this._cell.isDim(); - private _underline: number = this._cell.isUnderline(); - private _blink: number = this._cell.isBlink(); - private _inverse: number = this._cell.isInverse(); - private _invisible: number = this._cell.isInvisible(); - private _fgRGB: boolean = this._cell.isFgRGB(); - private _bgRGB: boolean = this._cell.isBgRGB(); - private _fgPalette: boolean = this._cell.isFgPalette(); - private _bgPallette: boolean = this._cell.isBgPalette(); - private _fgDefault: boolean = this._cell.isFgDefault(); - private _bgDefault: boolean = this._cell.isBgDefault(); - private _attributeDefault: boolean = this._cell.isAttributeDefault(); - public getWidth(): number { - return this._width; - } - public getChars(): string { - return this._chars; - } - public getCode(): number { - return this._code; - } - public getFgColorMode(): number { - return this._fgColorMode; - } - public getBgColorMode(): number { - return this._bgColorMode; - } - public getFgColor(): number { - return this._fgColor; - } - public getBgColor(): number { - return this._bgColor; - } - public isBold(): number { - return this._bold; - } - public isItalic(): number { - return this._italic; - } - public isDim(): number { - return this._dim; - } - public isUnderline(): number { - return this._underline; - } - public isBlink(): number { - return this._blink; - } - public isInverse(): number { - return this._inverse; - } - public isInvisible(): number { - return this._invisible; - } - public isFgRGB(): boolean { - return this._fgRGB; - } - public isBgRGB(): boolean { - return this._bgRGB; - } - public isFgPalette(): boolean { - return this._fgPalette; - } - public isBgPalette(): boolean { - return this._bgPallette; - } - public isFgDefault(): boolean { - return this._fgDefault; - } - public isBgDefault(): boolean { - return this._bgDefault; - } - public isAttributeDefault(): boolean { - return this._attributeDefault; - } - - public static from(cell: IBufferCell): MyBufferCell { - return new MyBufferCell(cell); - } -} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 9641dc20..60fe9624 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -6,7 +6,6 @@ */ import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; -import { MyBufferCell } from './MyBufferCell'; function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); @@ -86,8 +85,14 @@ class StringSerializeHandler extends BaseSerializeHandler { // so wee need to record it when required. private _cursorStyle: IBufferCell = this._buffer1.getNullCell(); + // where exact the cursor styles comes from + // because we can't copy the cell directly + // so we remember where the content comes from instead + private _cursorStyleRow: number = 0; + private _cursorStyleCol: number = 0; + // this is a null cell for reference for checking whether background is empty or not - private _backgroundCell: MyBufferCell = MyBufferCell.from(this._cursorStyle); + private _backgroundCell: IBufferCell = this._buffer1.getNullCell(); private _firstRow: number = 0; private _lastCursorRow: number = 0; @@ -122,7 +127,7 @@ class StringSerializeHandler extends BaseSerializeHandler { if (!isLastRow) { // Enable BCE if (row - this._firstRow >= this._terminal.rows) { - this._backgroundCell = MyBufferCell.from(this._cursorStyle); + this._buffer1.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell); } // Fetch current line @@ -283,7 +288,12 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow += `\x1b[${sgrSeq.join(';')}m`; // update the last cursor style - this._buffer1.getLine(row)?.getCell(col, this._cursorStyle); + const line = this._buffer1.getLine(row); + if (line !== undefined) { + line.getCell(col, this._cursorStyle); + this._cursorStyleRow = row; + this._cursorStyleCol = col; + } } /** From 079702282cb048a23d5d861f1a40336b1ace8e40 Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Sat, 23 Jan 2021 12:26:30 +0800 Subject: [PATCH 16/18] SerializeAddon: move test util into seperate file --- .../src/SerializeAddon.ts | 20 -------------- .../test/SerializeAddon.api.ts | 12 ++++----- .../test/SerializeAddonTestUtil.ts | 27 +++++++++++++++++++ demo/client.ts | 3 +++ 4 files changed, 36 insertions(+), 26 deletions(-) create mode 100644 addons/xterm-addon-serialize/test/SerializeAddonTestUtil.ts diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 60fe9624..25fa9ad2 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -412,25 +412,5 @@ export class SerializeAddon implements ITerminalAddon { + alternativeScreenContent; } - // this is a util used only for test - private static _inspectBuffer(buffer: IBuffer): { x: number, y: number, data: any[][] } { - const lines: any[] = []; - - for (let i = 0; i < buffer.length; i++) { - /** - * Do this intentionally to get content of underlining source - */ - const bufferLine = (buffer.getLine(i)! as any)._line; - - lines.push(JSON.stringify(bufferLine)); - } - - return { - x: buffer.cursorX, - y: buffer.cursorY, - data: lines - }; - } - public dispose(): void { } } diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 5dc29d0d..339ac162 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -18,12 +18,12 @@ const writeRawSync = (page: any, str: string): Promise => writeSync(page, const testNormalScreenEqual = async (page: any, str: string): Promise => { await writeRawSync(page, str); - const originalBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + const originalBuffer = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); const result = await page.evaluate(`serializeAddon.serialize();`) as string; await page.evaluate(`term.reset();`); await writeRawSync(page, result); - const newBuffer = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + const newBuffer = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); // chai decides -0 and 0 are different number... // and firefox have a bug that output -0 for unknown reason @@ -51,11 +51,11 @@ describe('SerializeAddon', () => { it('produce different output when we call test util with different text', async function(): Promise { await writeRawSync(page, '12345'); - const buffer1 = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + const buffer1 = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); await page.evaluate(`term.reset();`); await writeRawSync(page, '67890'); - const buffer2 = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + const buffer2 = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); assert.throw(() => { assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2)); @@ -64,11 +64,11 @@ describe('SerializeAddon', () => { it('produce different output when we call test util with different line wrap', async function(): Promise { await writeRawSync(page, '1234567890\r\n12345'); - const buffer3 = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + const buffer3 = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); await page.evaluate(`term.reset();`); await writeRawSync(page, '1234567890n12345'); - const buffer4 = await page.evaluate(`SerializeAddon._inspectBuffer(term.buffer.normal);`); + const buffer4 = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); assert.throw(() => { assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4)); diff --git a/addons/xterm-addon-serialize/test/SerializeAddonTestUtil.ts b/addons/xterm-addon-serialize/test/SerializeAddonTestUtil.ts new file mode 100644 index 00000000..23ac2237 --- /dev/null +++ b/addons/xterm-addon-serialize/test/SerializeAddonTestUtil.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBuffer } from 'xterm'; +export class SerializeAddonTestUtil { + // this is a util used only for test + public static inspectBuffer(buffer: IBuffer): { x: number, y: number, data: any[][] } { + const lines: any[] = []; + + for (let i = 0; i < buffer.length; i++) { + /** + * Do this intentionally to get content of underlining source + */ + const bufferLine = (buffer.getLine(i)! as any)._line; + + lines.push(JSON.stringify(bufferLine)); + } + + return { + x: buffer.cursorX, + y: buffer.cursorY, + data: lines + }; + } +} diff --git a/demo/client.ts b/demo/client.ts index 93b7c26c..27d1c297 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -13,6 +13,7 @@ import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon'; import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon'; +import { SerializeAddonTestUtil } from '../addons/xterm-addon-serialize/out-test/SerializeAddonTestUtil'; import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Addon'; @@ -38,6 +39,7 @@ export interface IWindowWithTerminal extends Window { FitAddon?: typeof FitAddon; SearchAddon?: typeof SearchAddon; SerializeAddon?: typeof SerializeAddon; + SerializeAddonTestUtil?: typeof SerializeAddonTestUtil; WebLinksAddon?: typeof WebLinksAddon; WebglAddon?: typeof WebglAddon; Unicode11Addon?: typeof Unicode11Addon; @@ -132,6 +134,7 @@ if (document.location.pathname === '/test') { window.FitAddon = FitAddon; window.SearchAddon = SearchAddon; window.SerializeAddon = SerializeAddon; + window.SerializeAddonTestUtil = SerializeAddonTestUtil; window.Unicode11Addon = Unicode11Addon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; From 54bb0ee18999df013d12aff9ccbb52aa943395ba Mon Sep 17 00:00:00 2001 From: Mmis1000 Date: Mon, 25 Jan 2021 10:52:48 +0800 Subject: [PATCH 17/18] SerializeAddon: comment the intent of several check --- .../src/SerializeAddon.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 25fa9ad2..0caaaa03 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -149,22 +149,32 @@ class StringSerializeHandler extends BaseSerializeHandler { const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1; // validate whether this line wrap is ever possible + // which mean whether cursor can placed at a overflow position (x === row) naturally let isValid = false; if ( + // you must output character to cause overflow, control sequence can't do this nextRowFirstChar.getChars() && isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0 ) { if ( + // the last character can't be null, + // you can't use control sequence to move cursor to (x === row) (thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) && + // change background of the first wrapped cell also affects BCE + // so we mark it as invalid to simply the process to determine line separator equalBg(thisRowLastChar, nextRowFirstChar) ) { isValid = true; } if ( + // the second to last character can't be null if the next line starts with CJK, + // you can't use control sequence to move cursor to (x === row) isNextRowFirstCharDoubleWidth && (thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) && + // change background of the first wrapped cell also affects BCE + // so we mark it as invalid to simply the process to determine line separator equalBg(thisRowLastChar, nextRowFirstChar) && equalBg(thisRowLastSecondChar, nextRowFirstChar) ) { @@ -179,20 +189,20 @@ class StringSerializeHandler extends BaseSerializeHandler { // move back and erase next line head rowSeparator += '\x1b[1D\x1b[1X'; - // do these because we filled the last several null slot, which we shouldn't if (this._nullCellCount > 0) { + // do these because we filled the last several null slot, which we shouldn't rowSeparator += '\x1b[A'; rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}C`; rowSeparator += `\x1b[${this._nullCellCount}X`; rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}D`; rowSeparator += '\x1b[B'; - - // This is content even it is invisible - // without this, wrap will be missing - this._lastContentCursorRow = row + 1; - this._lastContentCursorCol = 0; } + // This is content and need the be serialized even it is invisible. + // without this, wrap will be missing from outputs. + this._lastContentCursorRow = row + 1; + this._lastContentCursorCol = 0; + // force commit the cursor position this._lastCursorRow = row + 1; this._lastCursorCol = 0; From 46c93f1b63d61a841492648d40745bcd4c3f5c18 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 1 Feb 2021 09:34:38 -0800 Subject: [PATCH 18/18] Move util function into page.evaluate --- .../test/SerializeAddon.api.ts | 25 ++++++++++++----- .../test/SerializeAddonTestUtil.ts | 27 ------------------- demo/client.ts | 3 --- 3 files changed, 19 insertions(+), 36 deletions(-) delete mode 100644 addons/xterm-addon-serialize/test/SerializeAddonTestUtil.ts diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 339ac162..47af9d91 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -18,12 +18,12 @@ const writeRawSync = (page: any, str: string): Promise => writeSync(page, const testNormalScreenEqual = async (page: any, str: string): Promise => { await writeRawSync(page, str); - const originalBuffer = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); + const originalBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`); const result = await page.evaluate(`serializeAddon.serialize();`) as string; await page.evaluate(`term.reset();`); await writeRawSync(page, result); - const newBuffer = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); + const newBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`); // chai decides -0 and 0 are different number... // and firefox have a bug that output -0 for unknown reason @@ -43,6 +43,19 @@ describe('SerializeAddon', () => { await page.evaluate(` window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); + window.inspectBuffer = (buffer) => { + const lines = []; + for (let i = 0; i < buffer.length; i++) { + // Do this intentionally to get content of underlining source + const bufferLine = buffer.getLine(i)._line; + lines.push(JSON.stringify(bufferLine)); + } + return { + x: buffer.cursorX, + y: buffer.cursorY, + data: lines + }; + } `); }); @@ -51,11 +64,11 @@ describe('SerializeAddon', () => { it('produce different output when we call test util with different text', async function(): Promise { await writeRawSync(page, '12345'); - const buffer1 = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); + const buffer1 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); await page.evaluate(`term.reset();`); await writeRawSync(page, '67890'); - const buffer2 = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); + const buffer2 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); assert.throw(() => { assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2)); @@ -64,11 +77,11 @@ describe('SerializeAddon', () => { it('produce different output when we call test util with different line wrap', async function(): Promise { await writeRawSync(page, '1234567890\r\n12345'); - const buffer3 = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); + const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); await page.evaluate(`term.reset();`); await writeRawSync(page, '1234567890n12345'); - const buffer4 = await page.evaluate(`SerializeAddonTestUtil.inspectBuffer(term.buffer.normal);`); + const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); assert.throw(() => { assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4)); diff --git a/addons/xterm-addon-serialize/test/SerializeAddonTestUtil.ts b/addons/xterm-addon-serialize/test/SerializeAddonTestUtil.ts deleted file mode 100644 index 23ac2237..00000000 --- a/addons/xterm-addon-serialize/test/SerializeAddonTestUtil.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2021 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IBuffer } from 'xterm'; -export class SerializeAddonTestUtil { - // this is a util used only for test - public static inspectBuffer(buffer: IBuffer): { x: number, y: number, data: any[][] } { - const lines: any[] = []; - - for (let i = 0; i < buffer.length; i++) { - /** - * Do this intentionally to get content of underlining source - */ - const bufferLine = (buffer.getLine(i)! as any)._line; - - lines.push(JSON.stringify(bufferLine)); - } - - return { - x: buffer.cursorX, - y: buffer.cursorY, - data: lines - }; - } -} diff --git a/demo/client.ts b/demo/client.ts index 27d1c297..93b7c26c 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -13,7 +13,6 @@ import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon'; import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon'; -import { SerializeAddonTestUtil } from '../addons/xterm-addon-serialize/out-test/SerializeAddonTestUtil'; import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Addon'; @@ -39,7 +38,6 @@ export interface IWindowWithTerminal extends Window { FitAddon?: typeof FitAddon; SearchAddon?: typeof SearchAddon; SerializeAddon?: typeof SerializeAddon; - SerializeAddonTestUtil?: typeof SerializeAddonTestUtil; WebLinksAddon?: typeof WebLinksAddon; WebglAddon?: typeof WebglAddon; Unicode11Addon?: typeof Unicode11Addon; @@ -134,7 +132,6 @@ if (document.location.pathname === '/test') { window.FitAddon = FitAddon; window.SearchAddon = SearchAddon; window.SerializeAddon = SerializeAddon; - window.SerializeAddonTestUtil = SerializeAddonTestUtil; window.Unicode11Addon = Unicode11Addon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon;