From d7ed109f9d65b86599cd35eae517407486137757 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Fri, 18 Sep 2020 15:21:59 +0800 Subject: [PATCH] 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')); }); });