From 8a50729a98b3c9ea4daf249e35d96ab709e6b859 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 21:58:54 -0800 Subject: [PATCH 01/50] Reflow wider --- src/Buffer.ts | 77 +++++++++++++++++++++++++++++++++++++++++++++++ src/BufferLine.ts | 10 ++++++ 2 files changed, 87 insertions(+) diff --git a/src/Buffer.ts b/src/Buffer.ts index 74750a8b..6b247afe 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -233,6 +233,83 @@ export class Buffer implements IBuffer { } this.scrollBottom = newRows - 1; + + if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { + this._reflow(newCols, newRows); + } + } + + private _reflow(newCols: number, newRows: number): void { + if (this._terminal.cols === newCols) { + return; + } + + // Iterate through rows, ignore the last one as it cannot be wrapped + for (let y = 0; y < this.lines.length - 1; y++) { + // Check if this row is wrapped + let i = y; + let nextLine = this.lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + continue; + } + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; + while (nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = this.lines.get(++i) as BufferLine; + } + + if (newCols > this._terminal.cols) { + let destLineIndex = 0; + let destCol = this._terminal.cols; + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcRemainingCells = this._terminal.cols - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy); + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === this._terminal.cols) { + srcLineIndex++; + srcCol = 0; + } + } + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + // Remove rows and adjust cursor + if (countToRemove > 0) { + this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); + while (countToRemove-- > 0) { + if (this.ybase === 0) { + this.y--; + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + } + } else { + + } + } } /** diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 3f93af62..aafc9051 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -304,6 +304,16 @@ export class BufferLine implements IBufferLine { return 0; } + public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number): void { + console.log(' copyCellsFrom', srcCol, destCol, length); + const srcData = src._data; + for (let cell = 0; cell < length; cell++) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } + } + } + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { if (trimRight) { endCol = Math.min(endCol, this.getTrimmedLength()); From 314f98f2a63cd100f1084df46aab0bdbe5c82624 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 23:35:52 -0800 Subject: [PATCH 02/50] Mostly working for reflowing to smaller --- src/Buffer.ts | 231 +++++++++++++++++++++++++++++++++------------- src/BufferLine.ts | 17 +++- 2 files changed, 179 insertions(+), 69 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 6b247afe..fc8e008e 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -235,83 +235,187 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { - this._reflow(newCols, newRows); + this._reflow(newCols); } } - private _reflow(newCols: number, newRows: number): void { + private _reflow(newCols: number): void { if (this._terminal.cols === newCols) { return; } // Iterate through rows, ignore the last one as it cannot be wrapped for (let y = 0; y < this.lines.length - 1; y++) { - // Check if this row is wrapped - let i = y; - let nextLine = this.lines.get(++i) as BufferLine; - if (!nextLine.isWrapped) { - continue; - } - - // Check how many lines it's wrapped for - const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (nextLine.isWrapped) { - wrappedLines.push(nextLine); - nextLine = this.lines.get(++i) as BufferLine; - } - if (newCols > this._terminal.cols) { - let destLineIndex = 0; - let destCol = this._terminal.cols; - let srcLineIndex = 1; - let srcCol = 0; - while (srcLineIndex < wrappedLines.length) { - const srcRemainingCells = this._terminal.cols - srcCol; - const destRemainingCells = newCols - destCol; - const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy); - destCol += cellsToCopy; - if (destCol === newCols) { - destLineIndex++; - destCol = 0; - } - srcCol += cellsToCopy; - if (srcCol === this._terminal.cols) { - srcLineIndex++; - srcCol = 0; - } - } - - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - - // Remove rows and adjust cursor - if (countToRemove > 0) { - this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); - while (countToRemove-- > 0) { - if (this.ybase === 0) { - this.y--; - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; - } - } - } + y += this._reflowLarger(y, newCols); } else { - + y += this._reflowSmaller(y, newCols); } } } + private _reflowLarger(y: number, newCols: number): number { + // Check if this row is wrapped + let i = y; + let nextLine = this.lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + return 0; + } + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; + while (nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = this.lines.get(++i) as BufferLine; + } + + // Copy buffer data to new locations + let destLineIndex = 0; + let destCol = this._terminal.cols; + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcRemainingCells = this._terminal.cols - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === this._terminal.cols) { + srcLineIndex++; + srcCol = 0; + } + } + + // Clear out remaining cells or fragments could remain + // TODO: @jerch can this be a const? + const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData); + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + // Remove rows and adjust cursor + if (countToRemove > 0) { + this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); + let removing = countToRemove; + while (removing-- > 0) { + if (this.ybase === 0) { + this.y--; + // Add an extra row at the bottom of the viewport + this.lines.push(new this._bufferLineConstructor(newCols, fillCharData)); + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + } + // TODO: Handle list trimming + + return wrappedLines.length - countToRemove - 1; + } + + private _reflowSmaller(y: number, newCols: number): number { + // Check whether this line is a problem + const line = this.lines.get(y) as BufferLine; + if (line.getTrimmedLength() <= newCols) { + return 0; + } + + // TODO: How is the cursor x handled if it's wrapped? Do something special when the cursor is this line? + + + // Gather wrapped lines if it's wrapped + let lineIndex = y; + let nextLine = this.lines.get(++lineIndex) as BufferLine; + const wrappedLines: BufferLine[] = [line]; + while (nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = this.lines.get(++lineIndex) as BufferLine; + } + + // Determine how many lines need to be inserted at the end, based on the trimmed length of + // the last wrapped line + if (wrappedLines[wrappedLines.length - 1].getTrimmedLength() === undefined) { + debugger; + } + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); + const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; + const linesNeeded = Math.ceil(cellsNeeded / newCols); + const linesToAdd = linesNeeded - wrappedLines.length; + + // Add the new lines + const newLines: BufferLine[] = []; + for (let i = 0; i < linesToAdd; i++) { + // TODO: Remove any! + const newLine = this.getBlankLine((this._terminal as any).eraseAttr(), true) as BufferLine; + newLines.push(newLine); + } + this.lines.splice(y + wrappedLines.length, 0, ...newLines); + wrappedLines.push(...newLines); + + // Copy buffer data to new locations, this needs to happen backwards to do in-place + let destLineIndex = Math.floor(cellsNeeded / newCols); + let destCol = cellsNeeded % newCols; + if (destCol === 0) { + destLineIndex--; + destCol = newCols; + } + let srcLineIndex = wrappedLines.length - linesToAdd - 1; + let srcCol = lastLineLength; + while (srcLineIndex >= 0) { // Don't need to copy any from the first line + const cellsToCopy = Math.min(srcCol, destCol); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); + destCol -= cellsToCopy; + if (destCol === 0) { + destLineIndex--; + destCol = newCols; + } + srcCol -= cellsToCopy; + if (srcCol === 0) { + srcLineIndex--; + srcCol = this._terminal.cols; + } + } + + // Adjust viewport as needed + let viewportAdjustments = linesToAdd; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + if (this.y < this._terminal.rows) { + this.y++; + this.lines.pop(); + } else { + this.ybase++; + this.ydisp++; + } + } else { + if (this.ybase === this.ydisp) { + this.ybase++; + this.ydisp++; + } + } + } + + // TODO: Adjust viewport if needed (remove rows on end if ybase === 0? etc. + // TODO: Handle list trimming + + return wrappedLines.length - 1; + } + /** * Translates a string index back to a BufferIndex. * To get the correct buffer position the string must start at `startCol` 0 @@ -339,10 +443,9 @@ export class Buffer implements IBuffer { } lineIndex++; } - return [lineIndex, 0]; } - /** + /** // TODO: Handle list trimming * Translates a buffer line to a string, with optional start and end columns. * Wide characters will count as two columns in the resulting string. This * function is useful for getting the actual text underneath the raw selection diff --git a/src/BufferLine.ts b/src/BufferLine.ts index aafc9051..ea44de87 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -304,12 +304,19 @@ export class BufferLine implements IBufferLine { return 0; } - public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number): void { - console.log(' copyCellsFrom', srcCol, destCol, length); + public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void { const srcData = src._data; - for (let cell = 0; cell < length; cell++) { - for (let i = 0; i < CELL_SIZE; i++) { - this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + if (applyInReverse) { + for (let cell = length - 1; cell >= 0; cell--) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } + } + } else { + for (let cell = 0; cell < length; cell++) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } } } } From fa47036982cb8aecfb10d9c86427198b5fc91982 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 23:49:59 -0800 Subject: [PATCH 03/50] Fix row removal in reflowLarger --- src/Buffer.ts | 13 ++----------- src/BufferLine.ts | 6 +++--- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index fc8e008e..961032d9 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -292,21 +292,12 @@ export class Buffer implements IBuffer { } // Clear out remaining cells or fragments could remain - // TODO: @jerch can this be a const? + // TODO: @jerch can fillCharData be a const? const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData); - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - // Remove rows and adjust cursor + const countToRemove = wrappedLines.length - destLineIndex - 1; if (countToRemove > 0) { this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); let removing = countToRemove; diff --git a/src/BufferLine.ts b/src/BufferLine.ts index ea44de87..fd28af84 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -228,8 +228,8 @@ export class BufferLine implements IBufferLine { } } - public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { - if (cols === this.length || (!shrink && cols < this.length)) { + public resize(cols: number, fillCharData: CharData): void { + if (cols === this.length) { return; } if (cols > this.length) { @@ -245,7 +245,7 @@ export class BufferLine implements IBufferLine { for (let i = this.length; i < cols; ++i) { this.set(i, fillCharData); } - } else if (shrink) { + } else { if (cols) { const data = new Uint32Array(cols * CELL_SIZE); data.set(this._data.subarray(0, cols * CELL_SIZE)); From 8bc04c2fa73206f2e4ba3404297ebe187578d96c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 23:58:57 -0800 Subject: [PATCH 04/50] Tidy up --- src/Buffer.ts | 14 +++----------- src/Types.ts | 1 + 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 961032d9..5c1d8e45 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -300,8 +300,8 @@ export class Buffer implements IBuffer { const countToRemove = wrappedLines.length - destLineIndex - 1; if (countToRemove > 0) { this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); - let removing = countToRemove; - while (removing-- > 0) { + let viewportAdjustments = countToRemove; + while (viewportAdjustments-- > 0) { if (this.ybase === 0) { this.y--; // Add an extra row at the bottom of the viewport @@ -314,7 +314,6 @@ export class Buffer implements IBuffer { } } } - // TODO: Handle list trimming return wrappedLines.length - countToRemove - 1; } @@ -326,9 +325,6 @@ export class Buffer implements IBuffer { return 0; } - // TODO: How is the cursor x handled if it's wrapped? Do something special when the cursor is this line? - - // Gather wrapped lines if it's wrapped let lineIndex = y; let nextLine = this.lines.get(++lineIndex) as BufferLine; @@ -340,9 +336,6 @@ export class Buffer implements IBuffer { // Determine how many lines need to be inserted at the end, based on the trimmed length of // the last wrapped line - if (wrappedLines[wrappedLines.length - 1].getTrimmedLength() === undefined) { - debugger; - } const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; const linesNeeded = Math.ceil(cellsNeeded / newCols); @@ -351,8 +344,7 @@ export class Buffer implements IBuffer { // Add the new lines const newLines: BufferLine[] = []; for (let i = 0; i < linesToAdd; i++) { - // TODO: Remove any! - const newLine = this.getBlankLine((this._terminal as any).eraseAttr(), true) as BufferLine; + const newLine = this.getBlankLine(this._terminal.eraseAttr(), true) as BufferLine; newLines.push(newLine); } this.lines.splice(y + wrappedLines.length, 0, ...newLines); diff --git a/src/Types.ts b/src/Types.ts index 8ebb28d3..b1fea903 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -230,6 +230,7 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce cancel(ev: Event, force?: boolean): boolean | void; log(text: string): void; showCursor(): void; + eraseAttr(): number; } export interface IBufferAccessor { From 3311ed509aaca3372062b1b59457d487b6b59bd4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 00:40:30 -0800 Subject: [PATCH 05/50] Do shrink in reverse, fix up row remove count again --- src/Buffer.ts | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 5c1d8e45..dcb05049 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -245,11 +245,14 @@ export class Buffer implements IBuffer { } // Iterate through rows, ignore the last one as it cannot be wrapped - for (let y = 0; y < this.lines.length - 1; y++) { - if (newCols > this._terminal.cols) { + if (newCols > this._terminal.cols) { + for (let y = 0; y < this.lines.length - 1; y++) { y += this._reflowLarger(y, newCols); - } else { - y += this._reflowSmaller(y, newCols); + } + } else { + // Go backwards as many lines may be trimmed and this will avoid considering them + for (let y = this.lines.length - 1; y >= 0; y--) { + y -= this._reflowSmaller(y, newCols); } } } @@ -296,8 +299,16 @@ export class Buffer implements IBuffer { const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData); - // Remove rows and adjust cursor - const countToRemove = wrappedLines.length - destLineIndex - 1; + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + if (countToRemove > 0) { this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); let viewportAdjustments = countToRemove; @@ -320,18 +331,22 @@ export class Buffer implements IBuffer { private _reflowSmaller(y: number, newCols: number): number { // Check whether this line is a problem - const line = this.lines.get(y) as BufferLine; - if (line.getTrimmedLength() <= newCols) { + let nextLine = this.lines.get(y) as BufferLine; + if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) { return 0; } - // Gather wrapped lines if it's wrapped - let lineIndex = y; - let nextLine = this.lines.get(++lineIndex) as BufferLine; - const wrappedLines: BufferLine[] = [line]; - while (nextLine.isWrapped) { - wrappedLines.push(nextLine); - nextLine = this.lines.get(++lineIndex) as BufferLine; + // Gather wrapped lines and adjust y to be the starting line + const wrappedLines: BufferLine[] = [nextLine]; + if (nextLine.isWrapped) { + while (true) { + nextLine = this.lines.get(--y) as BufferLine; + // TODO: unshift is expensive + wrappedLines.unshift(nextLine); + if (!nextLine.isWrapped || y === 0) { + break; + } + } } // Determine how many lines need to be inserted at the end, based on the trimmed length of @@ -396,7 +411,7 @@ export class Buffer implements IBuffer { // TODO: Adjust viewport if needed (remove rows on end if ybase === 0? etc. // TODO: Handle list trimming - return wrappedLines.length - 1; + return wrappedLines.length - 1 - linesToAdd; } /** From ae29dbb133dc18348a70ab7af81a3f099d886782 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 01:26:49 -0800 Subject: [PATCH 06/50] Fix scrollbar when wrapping beyond single viewport of data --- src/Buffer.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index dcb05049..ef91055d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -355,6 +355,9 @@ export class Buffer implements IBuffer { const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; const linesNeeded = Math.ceil(cellsNeeded / newCols); const linesToAdd = linesNeeded - wrappedLines.length; + const trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + console.log('linesToAdd', linesToAdd); + console.log('trimmedLines', trimmedLines); // Add the new lines const newLines: BufferLine[] = []; @@ -374,7 +377,7 @@ export class Buffer implements IBuffer { } let srcLineIndex = wrappedLines.length - linesToAdd - 1; let srcCol = lastLineLength; - while (srcLineIndex >= 0) { // Don't need to copy any from the first line + while (srcLineIndex >= 0) { const cellsToCopy = Math.min(srcCol, destCol); wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); destCol -= cellsToCopy; @@ -393,11 +396,12 @@ export class Buffer implements IBuffer { let viewportAdjustments = linesToAdd; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - if (this.y < this._terminal.rows) { + if (this.y < this._terminal.rows - 1) { this.y++; this.lines.pop(); } else { this.ybase++; + // TODO: Use this? if (this._terminal._userScrolling) { this.ydisp++; } } else { @@ -411,7 +415,7 @@ export class Buffer implements IBuffer { // TODO: Adjust viewport if needed (remove rows on end if ybase === 0? etc. // TODO: Handle list trimming - return wrappedLines.length - 1 - linesToAdd; + return wrappedLines.length - 1 - linesToAdd + trimmedLines; } /** From 7684f93773fbde3bd9c16e9d56230c17ab312fc0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 01:32:19 -0800 Subject: [PATCH 07/50] Fix ydisp/ybase after trimming buffer --- src/Buffer.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index ef91055d..acba0c32 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -356,8 +356,6 @@ export class Buffer implements IBuffer { const linesNeeded = Math.ceil(cellsNeeded / newCols); const linesToAdd = linesNeeded - wrappedLines.length; const trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); - console.log('linesToAdd', linesToAdd); - console.log('trimmedLines', trimmedLines); // Add the new lines const newLines: BufferLine[] = []; @@ -393,7 +391,7 @@ export class Buffer implements IBuffer { } // Adjust viewport as needed - let viewportAdjustments = linesToAdd; + let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { if (this.y < this._terminal.rows - 1) { @@ -412,9 +410,6 @@ export class Buffer implements IBuffer { } } - // TODO: Adjust viewport if needed (remove rows on end if ybase === 0? etc. - // TODO: Handle list trimming - return wrappedLines.length - 1 - linesToAdd + trimmedLines; } From 358898daf9ca63e3da8ff649bad00c2b6a9129fa Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 11:02:08 -0800 Subject: [PATCH 08/50] Fix some tests --- src/Buffer.ts | 3 ++- src/BufferLine.test.ts | 51 +++------------------------------------- src/ui/TestUtils.test.ts | 3 +++ 3 files changed, 8 insertions(+), 49 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index acba0c32..143e2b2b 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -440,9 +440,10 @@ export class Buffer implements IBuffer { } lineIndex++; } + return [lineIndex, 0]; } - /** // TODO: Handle list trimming + /** * Translates a buffer line to a string, with optional start and end columns. * Wide characters will count as two columns in the resulting string. This * function is useful for getting the actual text underneath the raw selection diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index fbf8b051..c652ff4e 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -141,64 +141,19 @@ describe('BufferLine', function(): void { }); it('enlarge(true)', function(): void { const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true); + line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink(true) - should apply new size', function(): void { const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], true); + line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]); chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); - it('shrink(false) - should not apply new size', function(): void { - const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('shrink(false) + shrink(false) - should not apply new size', function(): void { - const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); - chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('shrink(false) + enlarge(false) to smaller than before', function(): void { - const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(15, [1, 'a', 0, 'a'.charCodeAt(0)]); - chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('shrink(false) + enlarge(false) to bigger than before', function(): void { - const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(25, [1, 'a', 0, 'a'.charCodeAt(0)]); - chai.expect(line.toArray()).eql(Array(25).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('shrink(false) + resize shrink=true should enforce shrinking', function(): void { - const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); - it('enlarge from 0 length', function(): void { - const line = new TestBufferLine(0, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); it('shrink to 0 length', function(): void { const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], true); + line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]); chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); - it('shrink(false) to 0 and enlarge to different sizes', function(): void { - const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], false); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], false); - chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], true); - chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)])); - }); }); describe('getTrimLength', function(): void { it('empty line', function(): void { diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 10033a33..3b59ee5d 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -19,6 +19,9 @@ export class TestTerminal extends Terminal { } export class MockTerminal implements ITerminal { + eraseAttr(): number { + throw new Error('Method not implemented.'); + } markers: IMarker[]; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); From a33e8a6af79b9f2aa9838f300b5e8366ddd010ba Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 12:57:10 -0800 Subject: [PATCH 09/50] Properly shrink rows to cols every time --- src/Buffer.ts | 8 +++++++- src/Types.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 143e2b2b..41a76ccc 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -162,7 +162,7 @@ export class Buffer implements IBuffer { // The following adjustments should only happen if the buffer has been // initialized/filled. if (this.lines.length > 0) { - // Deal with columns increasing (we don't do anything when columns reduce) + // Deal with columns increasing (reducing needs to happen after reflow) if (this._terminal.cols < newCols) { const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr? for (let i = 0; i < this.lines.length; i++) { @@ -236,6 +236,12 @@ export class Buffer implements IBuffer { if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { this._reflow(newCols); + + if (this._terminal.cols > newCols) { + for (let i = 0; i < this.lines.length; i++) { + this.lines.get(i).resize(newCols, null); + } + } } } diff --git a/src/Types.ts b/src/Types.ts index b1fea903..d7715f81 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -521,7 +521,7 @@ export interface IBufferLine { insertCells(pos: number, n: number, ch: CharData): void; deleteCells(pos: number, n: number, fill: CharData): void; replaceCells(start: number, end: number, fill: CharData): void; - resize(cols: number, fill: CharData, shrink?: boolean): void; + resize(cols: number, fill: CharData): void; fill(fillCharData: CharData): void; copyFrom(line: IBufferLine): void; clone(): IBufferLine; From 2a0da173f200b006f292b0592d2d47334e78bbc9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 14:55:15 -0800 Subject: [PATCH 10/50] Add a bunch of reflow tests --- src/Buffer.test.ts | 115 +++++++++++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 18 +++++-- 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0546dfe8..189b3ad6 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -233,6 +233,121 @@ describe('Buffer', () => { } }); }); + + describe('reflow', () => { + beforeEach(() => { + terminal.eraseAttr = () => DEFAULT_ATTR; + // Needed until the setting is removed + terminal.options.experimentalBufferLineImpl = 'TypedArray'; + }); + it('should not wrap empty lines', () => { + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + buffer.resize(INIT_COLS - 5, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + }); + it('should shrink row length', () => { + buffer.fillViewportRows(); + buffer.resize(5, 10); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).length, 5); + assert.equal(buffer.lines.get(1).length, 5); + assert.equal(buffer.lines.get(2).length, 5); + assert.equal(buffer.lines.get(3).length, 5); + assert.equal(buffer.lines.get(4).length, 5); + assert.equal(buffer.lines.get(5).length, 5); + assert.equal(buffer.lines.get(6).length, 5); + assert.equal(buffer.lines.get(7).length, 5); + assert.equal(buffer.lines.get(8).length, 5); + assert.equal(buffer.lines.get(9).length, 5); + }); + it('should wrap and unwrap lines', () => { + buffer.fillViewportRows(); + buffer.resize(5, 10); + terminal.cols = 5; + const firstLine = buffer.lines.get(0); + for (let i = 0; i < 5; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + firstLine.set(i, [null, char, 1, code]); + } + assert.equal(buffer.lines.get(0).length, 5); + assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); + buffer.resize(1, 10); + terminal.cols = 1; + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'a'); + assert.equal(buffer.lines.get(1).translateToString(), 'b'); + assert.equal(buffer.lines.get(2).translateToString(), 'c'); + assert.equal(buffer.lines.get(3).translateToString(), 'd'); + assert.equal(buffer.lines.get(4).translateToString(), 'e'); + assert.equal(buffer.lines.get(5).translateToString(), ' '); + assert.equal(buffer.lines.get(6).translateToString(), ' '); + assert.equal(buffer.lines.get(7).translateToString(), ' '); + assert.equal(buffer.lines.get(8).translateToString(), ' '); + assert.equal(buffer.lines.get(9).translateToString(), ' '); + buffer.resize(5, 10); + terminal.cols = 5; + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); + assert.equal(buffer.lines.get(1).translateToString(), ' '); + assert.equal(buffer.lines.get(2).translateToString(), ' '); + assert.equal(buffer.lines.get(3).translateToString(), ' '); + assert.equal(buffer.lines.get(4).translateToString(), ' '); + assert.equal(buffer.lines.get(5).translateToString(), ' '); + assert.equal(buffer.lines.get(6).translateToString(), ' '); + assert.equal(buffer.lines.get(7).translateToString(), ' '); + assert.equal(buffer.lines.get(8).translateToString(), ' '); + assert.equal(buffer.lines.get(9).translateToString(), ' '); + }); + it('should discard parts of wrapped lines that go out of the scrollback', () => { + buffer.fillViewportRows(); + terminal.options.scrollback = 1; + buffer.resize(10, 5); + terminal.cols = 10; + terminal.rows = 5; + const lastLine = buffer.lines.get(4); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + lastLine.set(i, [null, char, 1, code]); + } + assert.equal(buffer.lines.length, 5); + buffer.y = 4; + buffer.resize(2, 5); + terminal.cols = 2; + assert.equal(buffer.y, 4); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 6); + assert.equal(buffer.lines.get(0).translateToString(), ' '); + assert.equal(buffer.lines.get(1).translateToString(), 'ab'); + assert.equal(buffer.lines.get(2).translateToString(), 'cd'); + assert.equal(buffer.lines.get(3).translateToString(), 'ef'); + assert.equal(buffer.lines.get(4).translateToString(), 'gh'); + assert.equal(buffer.lines.get(5).translateToString(), 'ij'); + buffer.resize(1, 5); + terminal.cols = 1; + assert.equal(buffer.y, 4); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 6); + assert.equal(buffer.lines.get(0).translateToString(), 'e'); + assert.equal(buffer.lines.get(1).translateToString(), 'f'); + assert.equal(buffer.lines.get(2).translateToString(), 'g'); + assert.equal(buffer.lines.get(3).translateToString(), 'h'); + assert.equal(buffer.lines.get(4).translateToString(), 'i'); + assert.equal(buffer.lines.get(5).translateToString(), 'j'); + buffer.resize(10, 5); + terminal.cols = 10; + assert.equal(buffer.y, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 5); + assert.equal(buffer.lines.get(0).translateToString(), 'efghij '); + assert.equal(buffer.lines.get(1).translateToString(), ' '); + assert.equal(buffer.lines.get(2).translateToString(), ' '); + assert.equal(buffer.lines.get(3).translateToString(), ' '); + assert.equal(buffer.lines.get(4).translateToString(), ' '); + }); + }); }); describe('buffer marked to have no scrollback', () => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 41a76ccc..5f6791d8 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -164,9 +164,9 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (reducing needs to happen after reflow) if (this._terminal.cols < newCols) { - const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr? + const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, ch); + this.lines.get(i).resize(newCols, fillCharData); } } @@ -237,9 +237,11 @@ export class Buffer implements IBuffer { if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { this._reflow(newCols); + // Trim the end of the line off if cols shrunk if (this._terminal.cols > newCols) { + const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, null); + this.lines.get(i).resize(newCols, fillCharData); } } } @@ -273,7 +275,7 @@ export class Buffer implements IBuffer { // Check how many lines it's wrapped for const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (nextLine.isWrapped) { + while (nextLine.isWrapped && i < this.lines.length) { wrappedLines.push(nextLine); nextLine = this.lines.get(++i) as BufferLine; } @@ -361,7 +363,13 @@ export class Buffer implements IBuffer { const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; const linesNeeded = Math.ceil(cellsNeeded / newCols); const linesToAdd = linesNeeded - wrappedLines.length; - const trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + let trimmedLines: number; + if (this.ybase === 0 && this.y !== this.lines.length - 1) { + // If the top section of the buffer is not yet filled + trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd); + } else { + trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + } // Add the new lines const newLines: BufferLine[] = []; From 72369e060e602152709956dc6deae6aa2295d606 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 14:55:58 -0800 Subject: [PATCH 11/50] Only enable reflow on the normal buffer --- src/Buffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 5f6791d8..0b660321 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -234,7 +234,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { + if (this.hasScrollback && this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { this._reflow(newCols); // Trim the end of the line off if cols shrunk From 4a9f10d062c2f92b08dbbc862e7a5279ed5f2a3e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 19:22:05 -0800 Subject: [PATCH 12/50] Remove some of Buffer's dependency on Terminal --- src/Buffer.test.ts | 9 ++------- src/Buffer.ts | 4 ++-- src/Types.ts | 1 - 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 189b3ad6..cee0fbad 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -108,12 +108,12 @@ describe('Buffer', () => { describe('resize', () => { describe('column size is reduced', () => { - it('should not trim the data in the buffer', () => { + it('should trim the data in the buffer', () => { buffer.fillViewportRows(); buffer.resize(INIT_COLS / 2, INIT_ROWS); assert.equal(buffer.lines.length, INIT_ROWS); for (let i = 0; i < INIT_ROWS; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS); + assert.equal(buffer.lines.get(i).length, INIT_COLS / 2); } }); }); @@ -235,11 +235,6 @@ describe('Buffer', () => { }); describe('reflow', () => { - beforeEach(() => { - terminal.eraseAttr = () => DEFAULT_ATTR; - // Needed until the setting is removed - terminal.options.experimentalBufferLineImpl = 'TypedArray'; - }); it('should not wrap empty lines', () => { buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); diff --git a/src/Buffer.ts b/src/Buffer.ts index 0b660321..feb3a1ee 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -234,7 +234,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this.hasScrollback && this._terminal.options.experimentalBufferLineImpl === 'TypedArray') { + if (this.hasScrollback && this._bufferLineConstructor === BufferLine) { this._reflow(newCols); // Trim the end of the line off if cols shrunk @@ -374,7 +374,7 @@ export class Buffer implements IBuffer { // Add the new lines const newLines: BufferLine[] = []; for (let i = 0; i < linesToAdd; i++) { - const newLine = this.getBlankLine(this._terminal.eraseAttr(), true) as BufferLine; + const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine; newLines.push(newLine); } this.lines.splice(y + wrappedLines.length, 0, ...newLines); diff --git a/src/Types.ts b/src/Types.ts index d7715f81..d48362b8 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -230,7 +230,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce cancel(ev: Event, force?: boolean): boolean | void; log(text: string): void; showCursor(): void; - eraseAttr(): number; } export interface IBufferAccessor { From dde96187fa7b6502b357c1a614d0b2cacd6d79e5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 28 Dec 2018 19:27:38 -0800 Subject: [PATCH 13/50] Keep track of cols/rows inside Buffer --- src/Buffer.test.ts | 8 ------- src/Buffer.ts | 56 +++++++++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index cee0fbad..bece8c22 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -259,7 +259,6 @@ describe('Buffer', () => { it('should wrap and unwrap lines', () => { buffer.fillViewportRows(); buffer.resize(5, 10); - terminal.cols = 5; const firstLine = buffer.lines.get(0); for (let i = 0; i < 5; i++) { const code = 'a'.charCodeAt(0) + i; @@ -269,7 +268,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(0).length, 5); assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); buffer.resize(1, 10); - terminal.cols = 1; assert.equal(buffer.lines.length, 10); assert.equal(buffer.lines.get(0).translateToString(), 'a'); assert.equal(buffer.lines.get(1).translateToString(), 'b'); @@ -282,7 +280,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(8).translateToString(), ' '); assert.equal(buffer.lines.get(9).translateToString(), ' '); buffer.resize(5, 10); - terminal.cols = 5; assert.equal(buffer.lines.length, 10); assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); assert.equal(buffer.lines.get(1).translateToString(), ' '); @@ -299,8 +296,6 @@ describe('Buffer', () => { buffer.fillViewportRows(); terminal.options.scrollback = 1; buffer.resize(10, 5); - terminal.cols = 10; - terminal.rows = 5; const lastLine = buffer.lines.get(4); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; @@ -310,7 +305,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.length, 5); buffer.y = 4; buffer.resize(2, 5); - terminal.cols = 2; assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); @@ -321,7 +315,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(4).translateToString(), 'gh'); assert.equal(buffer.lines.get(5).translateToString(), 'ij'); buffer.resize(1, 5); - terminal.cols = 1; assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); @@ -332,7 +325,6 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(4).translateToString(), 'i'); assert.equal(buffer.lines.get(5).translateToString(), 'j'); buffer.resize(10, 5); - terminal.cols = 10; assert.equal(buffer.y, 0); assert.equal(buffer.ybase, 0); assert.equal(buffer.lines.length, 5); diff --git a/src/Buffer.ts b/src/Buffer.ts index feb3a1ee..ba335309 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -46,6 +46,8 @@ export class Buffer implements IBuffer { public savedCurAttr: number; public markers: Marker[] = []; private _bufferLineConstructor: IBufferLineConstructor; + private _cols: number; + private _rows: number; /** * Create a new Buffer. @@ -57,6 +59,8 @@ export class Buffer implements IBuffer { private _terminal: ITerminal, private _hasScrollback: boolean ) { + this._cols = this._terminal.cols; + this._rows = this._terminal.rows; this.clear(); } @@ -88,17 +92,17 @@ export class Buffer implements IBuffer { public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - return new this._bufferLineConstructor(this._terminal.cols, fillCharData, isWrapped); + return new this._bufferLineConstructor(this._cols, fillCharData, isWrapped); } public get hasScrollback(): boolean { - return this._hasScrollback && this.lines.maxLength > this._terminal.rows; + return this._hasScrollback && this.lines.maxLength > this._rows; } public get isCursorInViewport(): boolean { const absoluteY = this.ybase + this.y; const relativeY = absoluteY - this.ydisp; - return (relativeY >= 0 && relativeY < this._terminal.rows); + return (relativeY >= 0 && relativeY < this._rows); } /** @@ -124,7 +128,7 @@ export class Buffer implements IBuffer { if (fillAttr === undefined) { fillAttr = DEFAULT_ATTR; } - let i = this._terminal.rows; + let i = this._rows; while (i--) { this.lines.push(this.getBlankLine(fillAttr)); } @@ -140,9 +144,9 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this.lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.lines = new CircularList(this._getCorrectBufferLength(this._rows)); this.scrollTop = 0; - this.scrollBottom = this._terminal.rows - 1; + this.scrollBottom = this._rows - 1; this.setupTabStops(); } @@ -163,7 +167,7 @@ export class Buffer implements IBuffer { // initialized/filled. if (this.lines.length > 0) { // Deal with columns increasing (reducing needs to happen after reflow) - if (this._terminal.cols < newCols) { + if (this._cols < newCols) { const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { this.lines.get(i).resize(newCols, fillCharData); @@ -172,8 +176,8 @@ export class Buffer implements IBuffer { // Resize rows in both directions as needed let addToY = 0; - if (this._terminal.rows < newRows) { - for (let y = this._terminal.rows; y < newRows; y++) { + if (this._rows < newRows) { + for (let y = this._rows; y < newRows; y++) { if (this.lines.length < newRows + this.ybase) { if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { // There is room above the buffer and there are no empty elements below the line, @@ -192,8 +196,8 @@ export class Buffer implements IBuffer { } } } - } else { // (this._terminal.rows >= newRows) - for (let y = this._terminal.rows; y > newRows; y--) { + } else { // (this._rows >= newRows) + for (let y = this._rows; y > newRows; y--) { if (this.lines.length > newRows + this.ybase) { if (this.lines.length > this.ybase + this.y + 1) { // The line is a blank line below the cursor, remove it @@ -238,22 +242,25 @@ export class Buffer implements IBuffer { this._reflow(newCols); // Trim the end of the line off if cols shrunk - if (this._terminal.cols > newCols) { + if (this._cols > newCols) { const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { this.lines.get(i).resize(newCols, fillCharData); } } } + + this._cols = newCols; + this._rows = newRows; } private _reflow(newCols: number): void { - if (this._terminal.cols === newCols) { + if (this._cols === newCols) { return; } // Iterate through rows, ignore the last one as it cannot be wrapped - if (newCols > this._terminal.cols) { + if (newCols > this._cols) { for (let y = 0; y < this.lines.length - 1; y++) { y += this._reflowLarger(y, newCols); } @@ -282,11 +289,11 @@ export class Buffer implements IBuffer { // Copy buffer data to new locations let destLineIndex = 0; - let destCol = this._terminal.cols; + let destCol = this._cols; let srcLineIndex = 1; let srcCol = 0; while (srcLineIndex < wrappedLines.length) { - const srcRemainingCells = this._terminal.cols - srcCol; + const srcRemainingCells = this._cols - srcCol; const destRemainingCells = newCols - destCol; const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); @@ -296,7 +303,7 @@ export class Buffer implements IBuffer { destCol = 0; } srcCol += cellsToCopy; - if (srcCol === this._terminal.cols) { + if (srcCol === this._cols) { srcLineIndex++; srcCol = 0; } @@ -360,7 +367,7 @@ export class Buffer implements IBuffer { // Determine how many lines need to be inserted at the end, based on the trimmed length of // the last wrapped line const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - const cellsNeeded = (wrappedLines.length - 1) * this._terminal.cols + lastLineLength; + const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; const linesNeeded = Math.ceil(cellsNeeded / newCols); const linesToAdd = linesNeeded - wrappedLines.length; let trimmedLines: number; @@ -400,7 +407,7 @@ export class Buffer implements IBuffer { srcCol -= cellsToCopy; if (srcCol === 0) { srcLineIndex--; - srcCol = this._terminal.cols; + srcCol = this._cols; } } @@ -408,12 +415,11 @@ export class Buffer implements IBuffer { let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - if (this.y < this._terminal.rows - 1) { + if (this.y < this._rows - 1) { this.y++; this.lines.pop(); } else { this.ybase++; - // TODO: Use this? if (this._terminal._userScrolling) { this.ydisp++; } } else { @@ -503,7 +509,7 @@ export class Buffer implements IBuffer { i = 0; } - for (; i < this._terminal.cols; i += this._terminal.options.tabStopWidth) { + for (; i < this._cols; i += this._terminal.options.tabStopWidth) { this.tabs[i] = true; } } @@ -517,7 +523,7 @@ export class Buffer implements IBuffer { x = this.x; } while (!this.tabs[--x] && x > 0); - return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x; + return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; } /** @@ -528,8 +534,8 @@ export class Buffer implements IBuffer { if (x === null || x === undefined) { x = this.x; } - while (!this.tabs[++x] && x < this._terminal.cols); - return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x; + while (!this.tabs[++x] && x < this._cols); + return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; } public addMarker(y: number): Marker { From 478742a22e7c3c8aacfa0ea81757a7f6d9121504 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 30 Dec 2018 12:14:48 -0800 Subject: [PATCH 14/50] Make reflow small crazy fast This messy but this drops 100000 scrollback reflow from 87 cols to 40 cols go from ~18 seconds to < 1 second, 10000 takes around 70ms --- src/Buffer.ts | 221 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 149 insertions(+), 72 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index ba335309..acb462ab 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -265,10 +265,7 @@ export class Buffer implements IBuffer { y += this._reflowLarger(y, newCols); } } else { - // Go backwards as many lines may be trimmed and this will avoid considering them - for (let y = this.lines.length - 1; y >= 0; y--) { - y -= this._reflowSmaller(y, newCols); - } + this._reflowSmaller(newCols); } } @@ -344,93 +341,173 @@ export class Buffer implements IBuffer { return wrappedLines.length - countToRemove - 1; } - private _reflowSmaller(y: number, newCols: number): number { - // Check whether this line is a problem - let nextLine = this.lines.get(y) as BufferLine; - if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) { - return 0; - } + private _reflowSmaller(newCols: number): void { + // Gather all BufferLines that need to be inserted into the Buffer here so that they can be + // batched up and only committed once + const toInsert = []; + let countToInsert = 0; + // Go backwards as many lines may be trimmed and this will avoid considering them + for (let y = this.lines.length - 1; y >= 0; y--) { + // Check whether this line is a problem + let nextLine = this.lines.get(y) as BufferLine; + if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) { + continue; + } - // Gather wrapped lines and adjust y to be the starting line - const wrappedLines: BufferLine[] = [nextLine]; - if (nextLine.isWrapped) { - while (true) { + // Gather wrapped lines and adjust y to be the starting line + const wrappedLines: BufferLine[] = [nextLine]; + while (nextLine.isWrapped && y > 0) { nextLine = this.lines.get(--y) as BufferLine; // TODO: unshift is expensive wrappedLines.unshift(nextLine); - if (!nextLine.isWrapped || y === 0) { - break; - } } - } - // Determine how many lines need to be inserted at the end, based on the trimmed length of - // the last wrapped line - const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; - const linesNeeded = Math.ceil(cellsNeeded / newCols); - const linesToAdd = linesNeeded - wrappedLines.length; - let trimmedLines: number; - if (this.ybase === 0 && this.y !== this.lines.length - 1) { - // If the top section of the buffer is not yet filled - trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd); - } else { - trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); - } + // Determine how many lines need to be inserted at the end, based on the trimmed length of + // the last wrapped line + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); + const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; + const linesNeeded = Math.ceil(cellsNeeded / newCols); + const linesToAdd = linesNeeded - wrappedLines.length; + let trimmedLines: number; + if (this.ybase === 0 && this.y !== this.lines.length - 1) { + // If the top section of the buffer is not yet filled + trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd); + } else { + trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); + } - // Add the new lines - const newLines: BufferLine[] = []; - for (let i = 0; i < linesToAdd; i++) { - const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine; - newLines.push(newLine); - } - this.lines.splice(y + wrappedLines.length, 0, ...newLines); - wrappedLines.push(...newLines); + // Add the new lines + const newLines: BufferLine[] = []; + for (let i = 0; i < linesToAdd; i++) { + const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine; + newLines.push(newLine); + } + if (newLines.length > 0) { + toInsert.push({ + // countToInsert here gets the actual index, taking into account other inserted items. + // using this we can iterate through the list forwards + start: y + wrappedLines.length + countToInsert, + newLines + }); + countToInsert += newLines.length; + } + // this.lines.splice(y + wrappedLines.length, 0, ...newLines); + wrappedLines.push(...newLines); - // Copy buffer data to new locations, this needs to happen backwards to do in-place - let destLineIndex = Math.floor(cellsNeeded / newCols); - let destCol = cellsNeeded % newCols; - if (destCol === 0) { - destLineIndex--; - destCol = newCols; - } - let srcLineIndex = wrappedLines.length - linesToAdd - 1; - let srcCol = lastLineLength; - while (srcLineIndex >= 0) { - const cellsToCopy = Math.min(srcCol, destCol); - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); - destCol -= cellsToCopy; + // Copy buffer data to new locations, this needs to happen backwards to do in-place + let destLineIndex = Math.floor(cellsNeeded / newCols); + let destCol = cellsNeeded % newCols; if (destCol === 0) { destLineIndex--; destCol = newCols; } - srcCol -= cellsToCopy; - if (srcCol === 0) { - srcLineIndex--; - srcCol = this._cols; + let srcLineIndex = wrappedLines.length - linesToAdd - 1; + let srcCol = lastLineLength; + while (srcLineIndex >= 0) { + const cellsToCopy = Math.min(srcCol, destCol); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); + destCol -= cellsToCopy; + if (destCol === 0) { + destLineIndex--; + destCol = newCols; + } + srcCol -= cellsToCopy; + if (srcCol === 0) { + srcLineIndex--; + srcCol = this._cols; + } } - } - // Adjust viewport as needed - let viewportAdjustments = linesToAdd - trimmedLines; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - if (this.y < this._rows - 1) { - this.y++; - this.lines.pop(); + // Adjust viewport as needed + let viewportAdjustments = linesToAdd - trimmedLines; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + if (this.y < this._rows - 1) { + this.y++; + this.lines.pop(); + } else { + this.ybase++; + this.ydisp++; + } } else { - this.ybase++; - this.ydisp++; - } - } else { - if (this.ybase === this.ydisp) { - this.ybase++; - this.ydisp++; + if (this.ybase === this.ydisp) { + this.ybase++; + this.ydisp++; + } } } + + // y -= wrappedLines.length - 1 /*- linesToAdd*/ /*+ trimmedLines */; } - return wrappedLines.length - 1 - linesToAdd + trimmedLines; + // Record original lines so they don't get overridden when we rearrange the list + const originalLines: BufferLine[] = []; + for (let i = 0; i < this.lines.length; i++) { + originalLines.push(this.lines.get(i) as BufferLine); + } + // if (toInsert.length) { + // let insertIndex = toInsert.length - 1; + // let nextToInsert = toInsert[insertIndex]; + // let originalLineIndex = 0; + // for (let i = 0; i < Math.min(this.lines.maxLength - 1, this.lines.length + countToInsert); i++) { + // if (nextToInsert && nextToInsert.start === i) { + // this.lines.set(i, nextToInsert.newLines.shift()); + // if (nextToInsert.newLines.length === 0) { + // nextToInsert = toInsert[--insertIndex]; + // } + // } else { + // this.lines.set(i, originalLines[originalLineIndex++]); + // } + // } + // } + + if (toInsert.length > 0) { + let nextToInsertIndex = 0; + let nextToInsert = toInsert[nextToInsertIndex]; + let originalLineIndex = originalLines.length - 1; + const originalLinesLength = this.lines.length; + this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert); + // let countToBeInserted = countToInsert; + let countInsertedSoFar = 0; + for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { + if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) { + for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { + this.lines.set(i--, nextToInsert.newLines[nextI]); + } + i++; // Don't skip for the first row + // this.lines.set(i, nextToInsert.newLines.pop()); + countInsertedSoFar += nextToInsert.newLines.length; + // countToBeInserted--; + // if (nextToInsert.newLines.length === 0) { + nextToInsert = toInsert[++nextToInsertIndex]; + // } + + + + // this.lines.set(i, nextToInsert.newLines.pop()); + // countInsertedSoFar++; + // // countToBeInserted--; + // if (nextToInsert.newLines.length === 0) { + // nextToInsert = toInsert[++nextToInsertIndex]; + // } + } else { + this.lines.set(i, originalLines[originalLineIndex--]); + } + } + // TODO: Throw trim event + } + + // let offset = 0; + // const listener = (countToTrim: number) => { + // offset -= countToTrim; + // }; + // this.lines.on('trim', listener); + // toInsert.forEach(value => { + // console.log('Insert at ', value.start + offset, value.newLines); + // this.lines.splice(value.start + offset, 0, ...value.newLines); + // // offset -= value.start; + // }); + // this.lines.off('trim', listener); } /** From c9f4a650c6c24c350e526d8bb8ed063fb0d8f8d3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 07:26:08 -0800 Subject: [PATCH 15/50] Clean up comments and todos --- src/Buffer.ts | 72 ++++++++++----------------------------------------- 1 file changed, 14 insertions(+), 58 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index acb462ab..1dfc3ef6 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -25,6 +25,8 @@ export const WHITESPACE_CELL_CHAR = ' '; export const WHITESPACE_CELL_WIDTH = 1; export const WHITESPACE_CELL_CODE = 32; +const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + /** * This class represents a terminal buffer (an internal state of the terminal), where the * following information is stored (in high-level): @@ -168,9 +170,8 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (reducing needs to happen after reflow) if (this._cols < newCols) { - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, fillCharData); + this.lines.get(i).resize(newCols, FILL_CHAR_DATA); } } @@ -191,8 +192,7 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - this.lines.push(new this._bufferLineConstructor(newCols, fillCharData)); + this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); } } } @@ -243,9 +243,8 @@ export class Buffer implements IBuffer { // Trim the end of the line off if cols shrunk if (this._cols > newCols) { - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, fillCharData); + this.lines.get(i).resize(newCols, FILL_CHAR_DATA); } } } @@ -306,10 +305,8 @@ export class Buffer implements IBuffer { } } - // Clear out remaining cells or fragments could remain - // TODO: @jerch can fillCharData be a const? - const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, fillCharData); + // Clear out remaining cells or fragments could remain; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); // Work backwards and remove any rows at the end that only contain null cells let countToRemove = 0; @@ -328,7 +325,7 @@ export class Buffer implements IBuffer { if (this.ybase === 0) { this.y--; // Add an extra row at the bottom of the viewport - this.lines.push(new this._bufferLineConstructor(newCols, fillCharData)); + this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); } else { if (this.ydisp === this.ybase) { this.ydisp--; @@ -358,7 +355,6 @@ export class Buffer implements IBuffer { const wrappedLines: BufferLine[] = [nextLine]; while (nextLine.isWrapped && y > 0) { nextLine = this.lines.get(--y) as BufferLine; - // TODO: unshift is expensive wrappedLines.unshift(nextLine); } @@ -391,7 +387,6 @@ export class Buffer implements IBuffer { }); countToInsert += newLines.length; } - // this.lines.splice(y + wrappedLines.length, 0, ...newLines); wrappedLines.push(...newLines); // Copy buffer data to new locations, this needs to happen backwards to do in-place @@ -436,8 +431,6 @@ export class Buffer implements IBuffer { } } } - - // y -= wrappedLines.length - 1 /*- linesToAdd*/ /*+ trimmedLines */; } // Record original lines so they don't get overridden when we rearrange the list @@ -445,69 +438,32 @@ export class Buffer implements IBuffer { for (let i = 0; i < this.lines.length; i++) { originalLines.push(this.lines.get(i) as BufferLine); } - // if (toInsert.length) { - // let insertIndex = toInsert.length - 1; - // let nextToInsert = toInsert[insertIndex]; - // let originalLineIndex = 0; - // for (let i = 0; i < Math.min(this.lines.maxLength - 1, this.lines.length + countToInsert); i++) { - // if (nextToInsert && nextToInsert.start === i) { - // this.lines.set(i, nextToInsert.newLines.shift()); - // if (nextToInsert.newLines.length === 0) { - // nextToInsert = toInsert[--insertIndex]; - // } - // } else { - // this.lines.set(i, originalLines[originalLineIndex++]); - // } - // } - // } + // Rearrange lines in the buffer if there are any insertions, this is done at the end rather + // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many + // costly calls to CircularList.splice. if (toInsert.length > 0) { let nextToInsertIndex = 0; let nextToInsert = toInsert[nextToInsertIndex]; let originalLineIndex = originalLines.length - 1; const originalLinesLength = this.lines.length; this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert); - // let countToBeInserted = countToInsert; let countInsertedSoFar = 0; for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) { + // Insert extra lines here, adjusting i as needed for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { this.lines.set(i--, nextToInsert.newLines[nextI]); } i++; // Don't skip for the first row - // this.lines.set(i, nextToInsert.newLines.pop()); countInsertedSoFar += nextToInsert.newLines.length; - // countToBeInserted--; - // if (nextToInsert.newLines.length === 0) { - nextToInsert = toInsert[++nextToInsertIndex]; - // } - - - - // this.lines.set(i, nextToInsert.newLines.pop()); - // countInsertedSoFar++; - // // countToBeInserted--; - // if (nextToInsert.newLines.length === 0) { - // nextToInsert = toInsert[++nextToInsertIndex]; - // } + nextToInsert = toInsert[++nextToInsertIndex]; } else { this.lines.set(i, originalLines[originalLineIndex--]); } } // TODO: Throw trim event - } - - // let offset = 0; - // const listener = (countToTrim: number) => { - // offset -= countToTrim; - // }; - // this.lines.on('trim', listener); - // toInsert.forEach(value => { - // console.log('Insert at ', value.start + offset, value.newLines); - // this.lines.splice(value.start + offset, 0, ...value.newLines); - // // offset -= value.start; - // }); - // this.lines.off('trim', listener); + } } /** From b7081abfceb41b82dda4921e2f96b59cde26f0f4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 07:54:16 -0800 Subject: [PATCH 16/50] Move loop into reflowLarger (adjust indent) --- src/Buffer.ts | 130 +++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 1dfc3ef6..65e42de0 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -260,82 +260,82 @@ export class Buffer implements IBuffer { // Iterate through rows, ignore the last one as it cannot be wrapped if (newCols > this._cols) { - for (let y = 0; y < this.lines.length - 1; y++) { - y += this._reflowLarger(y, newCols); - } + this._reflowLarger(newCols); } else { this._reflowSmaller(newCols); } } - private _reflowLarger(y: number, newCols: number): number { - // Check if this row is wrapped - let i = y; - let nextLine = this.lines.get(++i) as BufferLine; - if (!nextLine.isWrapped) { - return 0; - } - - // Check how many lines it's wrapped for - const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (nextLine.isWrapped && i < this.lines.length) { - wrappedLines.push(nextLine); - nextLine = this.lines.get(++i) as BufferLine; - } - - // Copy buffer data to new locations - let destLineIndex = 0; - let destCol = this._cols; - let srcLineIndex = 1; - let srcCol = 0; - while (srcLineIndex < wrappedLines.length) { - const srcRemainingCells = this._cols - srcCol; - const destRemainingCells = newCols - destCol; - const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); - destCol += cellsToCopy; - if (destCol === newCols) { - destLineIndex++; - destCol = 0; + private _reflowLarger(newCols: number): void { + for (let y = 0; y < this.lines.length - 1; y++) { + // Check if this row is wrapped + let i = y; + let nextLine = this.lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + continue; } - srcCol += cellsToCopy; - if (srcCol === this._cols) { - srcLineIndex++; - srcCol = 0; + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; + while (nextLine.isWrapped && i < this.lines.length) { + wrappedLines.push(nextLine); + nextLine = this.lines.get(++i) as BufferLine; } - } - // Clear out remaining cells or fragments could remain; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); - - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - - if (countToRemove > 0) { - this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); - let viewportAdjustments = countToRemove; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; + // Copy buffer data to new locations + let destLineIndex = 0; + let destCol = this._cols; + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcRemainingCells = this._cols - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === this._cols) { + srcLineIndex++; + srcCol = 0; } } - } - return wrappedLines.length - countToRemove - 1; + // Clear out remaining cells or fragments could remain; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + if (countToRemove > 0) { + this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); + let viewportAdjustments = countToRemove; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + this.y--; + // Add an extra row at the bottom of the viewport + this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + } + + y += wrappedLines.length - countToRemove - 1; + } } private _reflowSmaller(newCols: number): void { From 135e31f2ca653932d5b85c78780326b5a4776179 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 08:58:04 -0800 Subject: [PATCH 17/50] Speed up reflow larger by batching removals --- src/Buffer.ts | 82 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 22 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 65e42de0..fc74f679 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -267,6 +267,9 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number): void { + // Gather all BufferLines that need to be removed from the Buffer here so that they can be + // batched up and only committed once + const toRemove: number[] = []; for (let y = 0; y < this.lines.length - 1; y++) { // Check if this row is wrapped let i = y; @@ -318,24 +321,59 @@ export class Buffer implements IBuffer { } if (countToRemove > 0) { - this.lines.splice(y + wrappedLines.length - countToRemove, countToRemove); - let viewportAdjustments = countToRemove; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; - } - } + toRemove.push(y + wrappedLines.length - countToRemove); // index + toRemove.push(countToRemove); } y += wrappedLines.length - countToRemove - 1; } + + if (toRemove.length > 0) { + // First iterate through the list and get the actual indexes to use for rows + const newLayout: number[] = []; + + let nextToRemoveIndex = 0; + let nextToRemoveStart = toRemove[nextToRemoveIndex]; + let countRemovedSoFar = 0; + for (let i = 0; i < this.lines.length; i++) { + if (nextToRemoveStart === i) { + const countToRemove = toRemove[++nextToRemoveIndex]; + i += countToRemove - 1; + countRemovedSoFar += countToRemove; + nextToRemoveStart = toRemove[++nextToRemoveIndex]; + } else { + newLayout.push(i); + } + } + + // TODO: THis and the next loop could be improved, only gather the new layout lines, not the original lines + // Record original lines so they don't get overridden when we rearrange the list + const originalLines: BufferLine[] = []; + for (let i = 0; i < this.lines.length; i++) { + originalLines.push(this.lines.get(i) as BufferLine); + } + + // Rearrange the list + for (let i = 0; i < newLayout.length; i++) { + this.lines.set(i, originalLines[newLayout[i]]); + } + this.lines.length = newLayout.length; + + // Adjust viewport based on number of items removed + let viewportAdjustments = countRemovedSoFar; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + this.y--; + // Add an extra row at the bottom of the viewport + this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; + } + this.ybase--; + } + } + } } private _reflowSmaller(newCols: number): void { @@ -433,20 +471,20 @@ export class Buffer implements IBuffer { } } - // Record original lines so they don't get overridden when we rearrange the list - const originalLines: BufferLine[] = []; - for (let i = 0; i < this.lines.length; i++) { - originalLines.push(this.lines.get(i) as BufferLine); - } - // Rearrange lines in the buffer if there are any insertions, this is done at the end rather // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many // costly calls to CircularList.splice. if (toInsert.length > 0) { + // Record original lines so they don't get overridden when we rearrange the list + const originalLines: BufferLine[] = []; + for (let i = 0; i < this.lines.length; i++) { + originalLines.push(this.lines.get(i) as BufferLine); + } + const originalLinesLength = this.lines.length; + + let originalLineIndex = originalLinesLength - 1; let nextToInsertIndex = 0; let nextToInsert = toInsert[nextToInsertIndex]; - let originalLineIndex = originalLines.length - 1; - const originalLinesLength = this.lines.length; this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert); let countInsertedSoFar = 0; for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { From 1612cec3e093861f6f46da4101876d32c635affa Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 09:54:19 -0800 Subject: [PATCH 18/50] Fix reflow larger bug, add regression test --- src/Buffer.test.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 15 +++++++-------- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index bece8c22..faed1808 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -334,6 +334,49 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(3).translateToString(), ' '); assert.equal(buffer.lines.get(4).translateToString(), ' '); }); + it('should remove the correct amount of rows when reflowing larger', () => { + // This is a regression test to ensure that successive wrapped lines that are getting + // 3+ lines removed on a reflow actually remove the right lines + buffer.fillViewportRows(); + buffer.resize(10, 10); + const firstLine = buffer.lines.get(0); + const secondLine = buffer.lines.get(1); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + firstLine.set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + secondLine.set(i, [null, char, 1, code]); + } + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + for (let i = 2; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + buffer.resize(2, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), '01'); + assert.equal(buffer.lines.get(6).translateToString(), '23'); + assert.equal(buffer.lines.get(7).translateToString(), '45'); + assert.equal(buffer.lines.get(8).translateToString(), '67'); + assert.equal(buffer.lines.get(9).translateToString(), '89'); + buffer.resize(10, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + for (let i = 2; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + }); }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index fc74f679..27662708 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -280,7 +280,7 @@ export class Buffer implements IBuffer { // Check how many lines it's wrapped for const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (nextLine.isWrapped && i < this.lines.length) { + while (i < this.lines.length && nextLine.isWrapped) { wrappedLines.push(nextLine); nextLine = this.lines.get(++i) as BufferLine; } @@ -325,7 +325,7 @@ export class Buffer implements IBuffer { toRemove.push(countToRemove); } - y += wrappedLines.length - countToRemove - 1; + y += wrappedLines.length - 1; } if (toRemove.length > 0) { @@ -346,16 +346,15 @@ export class Buffer implements IBuffer { } } - // TODO: THis and the next loop could be improved, only gather the new layout lines, not the original lines // Record original lines so they don't get overridden when we rearrange the list - const originalLines: BufferLine[] = []; - for (let i = 0; i < this.lines.length; i++) { - originalLines.push(this.lines.get(i) as BufferLine); + const newLayoutLines: BufferLine[] = []; + for (let i = 0; i < newLayout.length; i++) { + newLayoutLines.push(this.lines.get(newLayout[i]) as BufferLine); } // Rearrange the list - for (let i = 0; i < newLayout.length; i++) { - this.lines.set(i, originalLines[newLayout[i]]); + for (let i = 0; i < newLayoutLines.length; i++) { + this.lines.set(i, newLayoutLines[i]); } this.lines.length = newLayout.length; From db488ebcc9699ef97c21bd6695f0983006947dfd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 10:18:31 -0800 Subject: [PATCH 19/50] Reflow combined chars --- src/Buffer.test.ts | 15 +++++++++++++++ src/BufferLine.ts | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index faed1808..4c348ec6 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -377,6 +377,21 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(i).translateToString(), ' '); } }); + it('should transfer combined char data over to reflowed lines', () => { + buffer.fillViewportRows(); + buffer.resize(4, 2); + const firstLine = buffer.lines.get(0); + firstLine.set(0, [ null, 'a', 1, 'a'.charCodeAt(0) ]); + firstLine.set(1, [ null, 'b', 1, 'b'.charCodeAt(0) ]); + firstLine.set(2, [ null, 'c', 1, 'c'.charCodeAt(0) ]); + firstLine.set(3, [ null, '😁', 1, '😁'.charCodeAt(0) ]); + assert.equal(buffer.lines.length, 2); + assert.equal(buffer.lines.get(0).translateToString(), 'abc😁'); + assert.equal(buffer.lines.get(1).translateToString(), ' '); + buffer.resize(2, 2); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'c😁'); + }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index fd28af84..619fa9ca 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -319,6 +319,15 @@ export class BufferLine implements IBufferLine { } } } + + // Move any combined data over as needed + const srcCombinedKeys = Object.keys(src._combined); + for (let i = 0; i < srcCombinedKeys.length; i++) { + const key = parseInt(srcCombinedKeys[i], 10); + if (key >= srcCol) { + this._combined[key - srcCol + destCol] = src._combined[key]; + } + } } public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { From 40e8618cf5810e61b8d42e6d0a1d37aab31516f3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 10:26:10 -0800 Subject: [PATCH 20/50] Discard cut off combined data when resizing BufferLines --- src/BufferLine.test.ts | 13 +++++++++++++ src/BufferLine.ts | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index c652ff4e..f5e95fc8 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -9,6 +9,10 @@ import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from '. class TestBufferLine extends BufferLine { + public get combined(): {[index: number]: string} { + return this._combined; + } + public toArray(): CharData[] { const result = []; for (let i = 0; i < this.length; ++i) { @@ -154,6 +158,15 @@ describe('BufferLine', function(): void { line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]); chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); + it('should remove combining data', () => { + const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); + line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]); + chai.expect(line.translateToString()).eql('aaaaaaaaa😁'); + chai.expect(Object.keys(line.combined).length).eql(1); + line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]); + chai.expect(line.translateToString()).eql('aaaaa'); + chai.expect(Object.keys(line.combined).length).eql(0); + }); }); describe('getTrimLength', function(): void { it('empty line', function(): void { diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 619fa9ca..1c391471 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -250,8 +250,17 @@ export class BufferLine implements IBufferLine { const data = new Uint32Array(cols * CELL_SIZE); data.set(this._data.subarray(0, cols * CELL_SIZE)); this._data = data; + // Remove any cut off combined data + const keys = Object.keys(this._combined); + for (let i = 0; i < keys.length; i++) { + const key = parseInt(keys[i], 10); + if (key >= cols) { + delete this._combined[key]; + } + } } else { this._data = null; + this._combined = {}; } } this.length = cols; From 840970eac7e1d157f68dd3e1d98a073921b31a07 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 13:15:23 -0800 Subject: [PATCH 21/50] Update markers after a reflow --- src/Buffer.test.ts | 118 +++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 54 +++++++++++++++-- src/common/CircularList.ts | 20 +++++-- src/common/EventEmitter.ts | 13 ++++ 4 files changed, 195 insertions(+), 10 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 4c348ec6..7abf6f72 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -392,6 +392,124 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(0).translateToString(), 'ab'); assert.equal(buffer.lines.get(1).translateToString(), 'c😁'); }); + it('should adjust markers when reflowing', () => { + buffer.fillViewportRows(); + buffer.resize(10, 15); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(0).set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(1).set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = 'k'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(2).set(i, [null, char, 1, code]); + } + // Buffer: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0); + assert.equal(secondMarker.line, 1); + assert.equal(thirdMarker.line, 2); + buffer.resize(2, 15); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), '01'); + assert.equal(buffer.lines.get(6).translateToString(), '23'); + assert.equal(buffer.lines.get(7).translateToString(), '45'); + assert.equal(buffer.lines.get(8).translateToString(), '67'); + assert.equal(buffer.lines.get(9).translateToString(), '89'); + assert.equal(buffer.lines.get(10).translateToString(), 'kl'); + assert.equal(buffer.lines.get(11).translateToString(), 'mn'); + assert.equal(buffer.lines.get(12).translateToString(), 'op'); + assert.equal(buffer.lines.get(13).translateToString(), 'qr'); + assert.equal(buffer.lines.get(14).translateToString(), 'st'); + assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); + assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped'); + assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped'); + buffer.resize(10, 15); + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); + assert.equal(secondMarker.line, 1, 'second marker should be restored to it\'s original line'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored to it\'s original line'); + assert.equal(firstMarker.isDisposed, false); + assert.equal(secondMarker.isDisposed, false); + assert.equal(thirdMarker.isDisposed, false); + }); + it('should dispose markers whose rows are trimmed during a reflow', () => { + buffer.fillViewportRows(); + terminal.options.scrollback = 1; + buffer.resize(10, 10); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(0).set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(1).set(i, [null, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = 'k'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(2).set(i, [null, char, 1, code]); + } + // Buffer: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + buffer.y = 2; + assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0); + assert.equal(secondMarker.line, 1); + assert.equal(thirdMarker.line, 2); + buffer.resize(2, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'ij'); + assert.equal(buffer.lines.get(1).translateToString(), '01'); + assert.equal(buffer.lines.get(2).translateToString(), '23'); + assert.equal(buffer.lines.get(3).translateToString(), '45'); + assert.equal(buffer.lines.get(4).translateToString(), '67'); + assert.equal(buffer.lines.get(5).translateToString(), '89'); + assert.equal(buffer.lines.get(6).translateToString(), 'kl'); + assert.equal(buffer.lines.get(7).translateToString(), 'mn'); + assert.equal(buffer.lines.get(8).translateToString(), 'op'); + assert.equal(buffer.lines.get(9).translateToString(), 'qr'); + assert.equal(buffer.lines.get(10).translateToString(), 'st'); + assert.equal(secondMarker.line, 1, 'second marker should remain the same as it was shifted 4 and trimmed 4'); + assert.equal(thirdMarker.line, 6, 'third marker should be shifted since the first and second lines wrapped'); + assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); + assert.equal(secondMarker.isDisposed, false); + assert.equal(thirdMarker.isDisposed, false); + buffer.resize(10, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'ij '); + assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); + assert.equal(secondMarker.line, 1, 'second marker should be restored'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored'); + }); }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 27662708..5b9af742 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CircularList } from './common/CircularList'; +import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; @@ -238,7 +238,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this.hasScrollback && this._bufferLineConstructor === BufferLine) { + if (this._hasScrollback && this._bufferLineConstructor === BufferLine) { this._reflow(newCols); // Trim the end of the line off if cols shrunk @@ -338,6 +338,13 @@ export class Buffer implements IBuffer { for (let i = 0; i < this.lines.length; i++) { if (nextToRemoveStart === i) { const countToRemove = toRemove[++nextToRemoveIndex]; + + // Tell markers that there was a deletion + this.lines.emit('delete', { + index: i - countRemovedSoFar, + amount: countToRemove + } as IDeleteEvent); + i += countToRemove - 1; countRemovedSoFar += countToRemove; nextToRemoveStart = toRemove[++nextToRemoveIndex]; @@ -474,6 +481,10 @@ export class Buffer implements IBuffer { // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many // costly calls to CircularList.splice. if (toInsert.length > 0) { + // Record buffer insert events and then play them back backwards so that the indexes are + // correct + const insertEvents: IInsertEvent[] = []; + // Record original lines so they don't get overridden when we rearrange the list const originalLines: BufferLine[] = []; for (let i = 0; i < this.lines.length; i++) { @@ -492,14 +503,32 @@ export class Buffer implements IBuffer { for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { this.lines.set(i--, nextToInsert.newLines[nextI]); } - i++; // Don't skip for the first row + i++; + + // Create insert events for later + insertEvents.push({ + index: originalLineIndex + 1, + amount: nextToInsert.newLines.length + } as IInsertEvent); + countInsertedSoFar += nextToInsert.newLines.length; nextToInsert = toInsert[++nextToInsertIndex]; } else { this.lines.set(i, originalLines[originalLineIndex--]); } } - // TODO: Throw trim event + + // Update markers + let insertCountEmitted = 0; + for (let i = insertEvents.length - 1; i >= 0; i--) { + insertEvents[i].index += insertCountEmitted; + this.lines.emit('insert', insertEvents[i]); + insertCountEmitted += insertEvents[i].amount; + } + const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength); + if (amountToTrim > 0) { + this.lines.emitMayRemoveListeners('trim', amountToTrim); + } } } @@ -618,12 +647,27 @@ export class Buffer implements IBuffer { marker.dispose(); } })); + marker.register(this.lines.addDisposableListener('insert', (event: IInsertEvent) => { + if (marker.line >= event.index) { + marker.line += event.amount; + } + })); + marker.register(this.lines.addDisposableListener('delete', (event: IDeleteEvent) => { + // Delete the marker if it's within the range + if (marker.line >= event.index && marker.line < event.index + event.amount) { + marker.dispose(); + } + + // Shift the marker if it's after the deleted range + if (marker.line > event.index) { + marker.line -= event.amount; + } + })); marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker))); return marker; } private _removeMarker(marker: Marker): void { - // TODO: This could probably be optimized by relying on sort order and trimming the array using .length this.markers.splice(this.markers.indexOf(marker), 1); } diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 9faf534a..af4d8af5 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -6,6 +6,16 @@ import { EventEmitter } from './EventEmitter'; import { ICircularList } from './Types'; +export interface IInsertEvent { + index: number; + amount: number; +} + +export interface IDeleteEvent { + index: number; + amount: number; +} + /** * Represents a circular list; a list with a maximum size that wraps around when push is called, * overriding values at the start of the list. @@ -91,7 +101,7 @@ export class CircularList extends EventEmitter implements ICircularList { this._array[this._getCyclicIndex(this._length)] = value; if (this._length === this._maxLength) { this._startIndex = ++this._startIndex % this._maxLength; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); } else { this._length++; } @@ -107,7 +117,7 @@ export class CircularList extends EventEmitter implements ICircularList { throw new Error('Can only recycle when the buffer is full'); } this._startIndex = ++this._startIndex % this._maxLength; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); return this._array[this._getCyclicIndex(this._length - 1)]!; } @@ -158,7 +168,7 @@ export class CircularList extends EventEmitter implements ICircularList { const countToTrim = (this._length + items.length) - this._maxLength; this._startIndex += countToTrim; this._length = this._maxLength; - this.emit('trim', countToTrim); + this.emitMayRemoveListeners('trim', countToTrim); } else { this._length += items.length; } @@ -175,7 +185,7 @@ export class CircularList extends EventEmitter implements ICircularList { } this._startIndex += count; this._length -= count; - this.emit('trim', count); + this.emitMayRemoveListeners('trim', count); } public shiftElements(start: number, count: number, offset: number): void { @@ -199,7 +209,7 @@ export class CircularList extends EventEmitter implements ICircularList { while (this._length > this._maxLength) { this._length--; this._startIndex++; - this.emit('trim', 1); + this.emitMayRemoveListeners('trim', 1); } } } else { diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index f9c0c001..c6b09901 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -75,6 +75,19 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab } } + public emitMayRemoveListeners(type: string, ...args: any[]): void { + if (!this._events[type]) { + return; + } + const obj = this._events[type]; + let length = obj.length; + for (let i = 0; i < obj.length; i++) { + obj[i].apply(this, args); + i -= length - obj.length; + length = obj.length; + } + } + public listeners(type: string): XtermListener[] { return this._events[type] || []; } From 6559931f34239fb0dd1c9e26b4b1cedc1c6c5294 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 14:28:52 -0800 Subject: [PATCH 22/50] Add lots of tests --- src/Buffer.test.ts | 393 +++++++++++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 2 +- 2 files changed, 394 insertions(+), 1 deletion(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 7abf6f72..81d4484b 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -510,6 +510,399 @@ describe('Buffer', () => { assert.equal(secondMarker.line, 1, 'second marker should be restored'); assert.equal(thirdMarker.line, 2, 'third marker should be restored'); }); + + describe('reflowLarger cases', () => { + beforeEach(() => { + // Setup buffer state: + // 'ab' + // 'cd' (wrapped) + // 'ef' + // 'gh' (wrapped) + // 'ij' + // 'kl' (wrapped) + // ' ' + // ' ' + // ' ' + // ' ' + buffer.fillViewportRows(); + buffer.resize(2, 10); + buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1).isWrapped = true; + buffer.lines.get(2).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]); + buffer.lines.get(2).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]); + buffer.lines.get(3).set(0, [null, 'g', 1, 'g'.charCodeAt(0)]); + buffer.lines.get(3).set(1, [null, 'h', 1, 'h'.charCodeAt(0)]); + buffer.lines.get(3).isWrapped = true; + buffer.lines.get(4).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]); + buffer.lines.get(4).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]); + buffer.lines.get(5).set(0, [null, 'k', 1, 'k'.charCodeAt(0)]); + buffer.lines.get(5).set(1, [null, 'l', 1, 'l'.charCodeAt(0)]); + buffer.lines.get(5).isWrapped = true; + }); + describe('viewport not yet filled', () => { + it('should move the cursor up and add empty lines', () => { + buffer.y = 6; + buffer.resize(4, 10); + assert.equal(buffer.y, 3); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(1).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(2).translateToString(), 'ijkl'); + for (let i = 3; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('viewport filled, scrollback remaining', () => { + beforeEach(() => { + buffer.y = 9; + }); + describe('ybase === 0', () => { + it('should move the cursor up and add empty lines', () => { + buffer.resize(4, 10); + assert.equal(buffer.y, 6); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(1).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(2).translateToString(), 'ijkl'); + for (let i = 3; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('ybase !== 0', () => { + beforeEach(() => { + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should adjust the viewport and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should keep ydisp at the same value', () => { + buffer.ydisp = 5; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + describe('viewport filled, no scrollback remaining', () => { + // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported + describe('ybase !== 0', () => { + beforeEach(() => { + terminal.options.scrollback = 10; + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + } + buffer.y = 9; + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should trim lines and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should trim lines and not change ydisp', () => { + buffer.ydisp = 5; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + }); + describe('reflowSmaller cases', () => { + beforeEach(() => { + // Setup buffer state: + // 'abcd' + // 'efgh' (wrapped) + // 'ijkl' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(0).set(2, [null, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(0).set(3, [null, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]); + buffer.lines.get(1).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]); + buffer.lines.get(1).set(2, [null, 'g', 1, 'g'.charCodeAt(0)]); + buffer.lines.get(1).set(3, [null, 'h', 1, 'h'.charCodeAt(0)]); + buffer.lines.get(2).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]); + buffer.lines.get(2).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]); + buffer.lines.get(2).set(2, [null, 'k', 1, 'k'.charCodeAt(0)]); + buffer.lines.get(2).set(3, [null, 'l', 1, 'l'.charCodeAt(0)]); + }); + describe('viewport not yet filled', () => { + it('should move the cursor down', () => { + buffer.y = 3; + buffer.resize(2, 10); + assert.equal(buffer.y, 6); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), 'kl'); + for (let i = 6; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [1, 3, 5]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('viewport filled, scrollback remaining', () => { + beforeEach(() => { + buffer.y = 9; + }); + describe('ybase === 0', () => { + it('should trim the top', () => { + buffer.resize(2, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 3); + assert.equal(buffer.ybase, 3); + assert.equal(buffer.lines.length, 13); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), 'kl'); + for (let i = 6; i < 13; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [1, 3, 5]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('ybase !== 0', () => { + beforeEach(() => { + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should adjust the viewport and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 13); + assert.equal(buffer.ybase, 13); + assert.equal(buffer.lines.length, 23); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'ab'); + assert.equal(buffer.lines.get(11).translateToString(), 'cd'); + assert.equal(buffer.lines.get(12).translateToString(), 'ef'); + assert.equal(buffer.lines.get(13).translateToString(), 'gh'); + assert.equal(buffer.lines.get(14).translateToString(), 'ij'); + assert.equal(buffer.lines.get(15).translateToString(), 'kl'); + for (let i = 16; i < 23; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [11, 13, 15]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should keep ydisp at the same value', () => { + buffer.ydisp = 5; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 13); + assert.equal(buffer.lines.length, 23); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(10).translateToString(), 'ab'); + assert.equal(buffer.lines.get(11).translateToString(), 'cd'); + assert.equal(buffer.lines.get(12).translateToString(), 'ef'); + assert.equal(buffer.lines.get(13).translateToString(), 'gh'); + assert.equal(buffer.lines.get(14).translateToString(), 'ij'); + assert.equal(buffer.lines.get(15).translateToString(), 'kl'); + for (let i = 16; i < 23; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [11, 13, 15]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + describe('viewport filled, no scrollback remaining', () => { + // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported + describe('ybase !== 0', () => { + beforeEach(() => { + terminal.options.scrollback = 10; + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should trim lines and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 10); + assert.equal(buffer.ybase, 10); + assert.equal(buffer.lines.length, 20); + for (let i = 0; i < 7; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(7).translateToString(), 'ab'); + assert.equal(buffer.lines.get(8).translateToString(), 'cd'); + assert.equal(buffer.lines.get(9).translateToString(), 'ef'); + assert.equal(buffer.lines.get(10).translateToString(), 'gh'); + assert.equal(buffer.lines.get(11).translateToString(), 'ij'); + assert.equal(buffer.lines.get(12).translateToString(), 'kl'); + for (let i = 13; i < 20; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [8, 10, 12]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should trim lines and not change ydisp', () => { + buffer.ydisp = 5; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 10); + assert.equal(buffer.lines.length, 20); + for (let i = 0; i < 7; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + assert.equal(buffer.lines.get(7).translateToString(), 'ab'); + assert.equal(buffer.lines.get(8).translateToString(), 'cd'); + assert.equal(buffer.lines.get(9).translateToString(), 'ef'); + assert.equal(buffer.lines.get(10).translateToString(), 'gh'); + assert.equal(buffer.lines.get(11).translateToString(), 'ij'); + assert.equal(buffer.lines.get(12).translateToString(), 'kl'); + for (let i = 13; i < 20; i++) { + assert.equal(buffer.lines.get(i).translateToString(), ' '); + } + const wrappedLines = [8, 10, 12]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + }); }); }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 5b9af742..861471b5 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -470,9 +470,9 @@ export class Buffer implements IBuffer { } } else { if (this.ybase === this.ydisp) { - this.ybase++; this.ydisp++; } + this.ybase++; } } } From 2ce67b89bcb62ccf95d34ceb285d02e2f1ed4a6c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 14:43:50 -0800 Subject: [PATCH 23/50] Remove unneeded MockTerminal member --- src/ui/TestUtils.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 3b59ee5d..10033a33 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -19,9 +19,6 @@ export class TestTerminal extends Terminal { } export class MockTerminal implements ITerminal { - eraseAttr(): number { - throw new Error('Method not implemented.'); - } markers: IMarker[]; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); From 338558d86c6a5a018347664bf7bdead3b89c3e63 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 31 Dec 2018 20:00:16 -0800 Subject: [PATCH 24/50] Make Terminal API properties readonly Fixes #1867 --- src/Buffer.test.ts | 4 ++-- src/BufferSet.test.ts | 4 ++-- src/Linkifier.test.ts | 14 +++++++------- src/SelectionManager.test.ts | 4 ++-- src/SelectionModel.test.ts | 4 ++-- src/Types.ts | 2 +- typings/xterm.d.ts | 18 +++++++++++------- 7 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0546dfe8..9d1d50c8 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -19,8 +19,8 @@ describe('Buffer', () => { beforeEach(() => { terminal = new MockTerminal(); - terminal.cols = INIT_COLS; - terminal.rows = INIT_ROWS; + (terminal as any).cols = INIT_COLS; + (terminal as any).rows = INIT_ROWS; terminal.options.scrollback = 1000; buffer = new Buffer(terminal, true); }); diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 26f9cd42..576a8ca4 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -15,8 +15,8 @@ describe('BufferSet', () => { beforeEach(() => { terminal = new MockTerminal(); - terminal.cols = 80; - terminal.rows = 24; + (terminal as any).cols = 80; + (terminal as any).rows = 24; terminal.options.scrollback = 1000; bufferSet = new BufferSet(terminal); }); diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0ba1294a..07dbf1b3 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -41,8 +41,8 @@ describe('Linkifier', () => { beforeEach(() => { terminal = new MockTerminal(); - terminal.cols = 100; - terminal.rows = 10; + (terminal as any).cols = 100; + (terminal as any).rows = 10; terminal.buffer = new MockBuffer(); (terminal.buffer).setLines(new CircularList(20)); terminal.buffer.ydisp = 0; @@ -65,7 +65,7 @@ describe('Linkifier', () => { function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { addRow(rowText); linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - terminal.rows = terminal.buffer.lines.length - 1; + (terminal as any).rows = terminal.buffer.lines.length - 1; linkifier.linkifyRows(); // Allow linkify to happen setTimeout(() => { @@ -142,19 +142,19 @@ describe('Linkifier', () => { }); describe('multi-line links', () => { it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => { - terminal.cols = 4; + (terminal as any).cols = 4; assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done); }); it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => { - terminal.cols = 4; + (terminal as any).cols = 4; assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done); }); it('should match links that start and end on line 2/2 of a wrapped line', done => { - terminal.cols = 4; + (terminal as any).cols = 4; assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done); }); it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => { - terminal.cols = 4; + (terminal as any).cols = 4; assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done); }); }); diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 2f74ccda..21bc6754 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -45,8 +45,8 @@ describe('SelectionManager', () => { beforeEach(() => { terminal = new TestMockTerminal(); - terminal.cols = 80; - terminal.rows = 2; + (terminal as any).cols = 80; + (terminal as any).rows = 2; terminal.options.scrollback = 100; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index 8d4b30bb..d49f41d0 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -23,8 +23,8 @@ describe('SelectionManager', () => { beforeEach(() => { terminal = new MockTerminal(); - terminal.cols = 80; - terminal.rows = 2; + (terminal as any).cols = 80; + (terminal as any).rows = 2; terminal.options.scrollback = 10; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; diff --git a/src/Types.ts b/src/Types.ts index 8ebb28d3..e6f36ac4 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -237,7 +237,7 @@ export interface IBufferAccessor { } export interface IElementAccessor { - element: HTMLElement; + readonly element: HTMLElement; } export interface ILinkifierAccessor { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 7528bb55..b0924296 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -325,28 +325,32 @@ declare module 'xterm' { /** * The element containing the terminal. */ - element: HTMLElement; + readonly element: HTMLElement; /** * The textarea that accepts input for the terminal. */ - textarea: HTMLTextAreaElement; + readonly textarea: HTMLTextAreaElement; /** - * The number of rows in the terminal's viewport. + * The number of rows in the terminal's viewport. Use + * `ITerminalOptions.rows` to set this in the constructor and + * `Terminal.resize` for when the terminal exists. */ - rows: number; + readonly rows: number; /** - * The number of columns in the terminal's viewport. + * The number of columns in the terminal's viewport. Use + * `ITerminalOptions.cols` to set this in the constructor and + * `Terminal.resize` for when the terminal exists. */ - cols: number; + readonly cols: number; /** * (EXPERIMENTAL) Get all markers registered against the buffer. If the alt * buffer is active this will always return []. */ - markers: IMarker[]; + readonly markers: IMarker[]; /** * Natural language strings that can be localized. From 6581eb7d88015ac5b499d576436133d37ca51ae3 Mon Sep 17 00:00:00 2001 From: jerch Date: Sat, 5 Jan 2019 01:47:44 +0100 Subject: [PATCH 25/50] fix leftover BufferLineConstructor --- src/Buffer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 74cec107..987e0324 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -210,7 +210,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._hasScrollback && this._bufferLineConstructor === BufferLine) { + if (this._hasScrollback) { this._reflow(newCols); // Trim the end of the line off if cols shrunk @@ -343,7 +343,7 @@ export class Buffer implements IBuffer { if (this.ybase === 0) { this.y--; // Add an extra row at the bottom of the viewport - this.lines.push(new this._bufferLineConstructor(newCols, FILL_CHAR_DATA)); + this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); } else { if (this.ydisp === this.ybase) { this.ydisp--; From 249f8800af98bf9a715b77bedd086bd350f975c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 7 Jan 2019 11:30:26 +0100 Subject: [PATCH 26/50] account empty cells in stringIndexToBufferIndex --- src/Buffer.test.ts | 6 +++--- src/Buffer.ts | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0546dfe8..663c7000 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -511,7 +511,7 @@ describe('Buffer', () => { const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 10; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); const j = (i - 0) << 1; assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); } @@ -523,7 +523,7 @@ describe('Buffer', () => { const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); } }); @@ -535,7 +535,7 @@ describe('Buffer', () => { const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); assert.equal( (!(i % 3)) ? input[i] diff --git a/src/Buffer.ts b/src/Buffer.ts index 625a2497..7b3bcee5 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -220,14 +220,16 @@ export class Buffer implements IBuffer { * @param stringIndex index within the string * @param startCol column offset the string was retrieved from */ - public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex { + public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex { while (stringIndex) { const line = this.lines.get(lineIndex); if (!line) { return [-1, -1]; } - for (let i = 0; i < line.length; ++i) { - stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; + const length = (trimRight) ? line.getTrimmedLength() : line.length; + for (let i = 0; i < length; ++i) { + if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; if (stringIndex < 0) { return [lineIndex, i]; } From 883ad01bd508e6df1f69127449b31710c8d9b6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 7 Jan 2019 11:39:16 +0100 Subject: [PATCH 27/50] make linter happy --- src/Buffer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 7b3bcee5..0ad86bf1 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -228,8 +228,9 @@ export class Buffer implements IBuffer { } const length = (trimRight) ? line.getTrimmedLength() : line.length; for (let i = 0; i < length; ++i) { - if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) - stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; + if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) { + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; // WHITESPACE_CELL_CHAR.length + } if (stringIndex < 0) { return [lineIndex, i]; } From 7fe3f0a4e3e094ed82fec0b17219cc0187a3ed69 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 11 Jan 2019 12:58:41 -0800 Subject: [PATCH 28/50] Improve BufferLine test Ensure combined is cleared when shrinking and enlarging --- src/BufferLine.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index f5e95fc8..0ef29505 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -158,14 +158,17 @@ describe('BufferLine', function(): void { line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]); chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); - it('should remove combining data', () => { + it('should remove combining data on replaced cells after shrinking then enlarging', () => { const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); + line.set(2, [ null, '😁', 1, '😁'.charCodeAt(0) ]); line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - chai.expect(line.translateToString()).eql('aaaaaaaaa😁'); - chai.expect(Object.keys(line.combined).length).eql(1); + chai.expect(line.translateToString()).eql('aa😁aaaaaa😁'); + chai.expect(Object.keys(line.combined).length).eql(2); line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]); - chai.expect(line.translateToString()).eql('aaaaa'); - chai.expect(Object.keys(line.combined).length).eql(0); + chai.expect(line.translateToString()).eql('aa😁aa'); + line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); + chai.expect(line.translateToString()).eql('aa😁aaaaaaa'); + chai.expect(Object.keys(line.combined).length).eql(1); }); }); describe('getTrimLength', function(): void { From 07d3407892a35e6afeb6a0e4c3355868ab686546 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 17 Jan 2019 09:48:18 -0800 Subject: [PATCH 29/50] Remove unused var and unneeded defensive check ...items as an arg will always be an array --- demo/start.js | 1 - src/common/CircularList.ts | 32 +++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/demo/start.js b/demo/start.js index 78f1ff1d..278c572f 100644 --- a/demo/start.js +++ b/demo/start.js @@ -5,7 +5,6 @@ * This file is the entry point for browserify. */ -const cp = require('child_process'); const path = require('path'); const webpack = require('webpack'); const startServer = require('./server.js'); diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 9faf534a..d23e693f 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -144,24 +144,22 @@ export class CircularList extends EventEmitter implements ICircularList { this._length -= deleteCount; } - if (items && items.length) { - // Add items - for (let i = this._length - 1; i >= start; i--) { - this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)]; - } - for (let i = 0; i < items.length; i++) { - this._array[this._getCyclicIndex(start + i)] = items[i]; - } + // Add items + for (let i = this._length - 1; i >= start; i--) { + this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)]; + } + for (let i = 0; i < items.length; i++) { + this._array[this._getCyclicIndex(start + i)] = items[i]; + } - // Adjust length as needed - if (this._length + items.length > this._maxLength) { - const countToTrim = (this._length + items.length) - this._maxLength; - this._startIndex += countToTrim; - this._length = this._maxLength; - this.emit('trim', countToTrim); - } else { - this._length += items.length; - } + // Adjust length as needed + if (this._length + items.length > this._maxLength) { + const countToTrim = (this._length + items.length) - this._maxLength; + this._startIndex += countToTrim; + this._length = this._maxLength; + this.emit('trim', countToTrim); + } else { + this._length += items.length; } } From 4843ca5bb879ed5e6116cba89d27e6c9c813ba40 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 21 Jan 2019 09:08:18 -0800 Subject: [PATCH 30/50] Fix reflow larger with wide chars --- src/Buffer.test.ts | 37 +++++++++++++++++++++++++++++++++++++ src/Buffer.ts | 18 +++++++++++++++--- src/BufferLine.ts | 4 ++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 81d4484b..040eb3ca 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -510,6 +510,43 @@ describe('Buffer', () => { assert.equal(secondMarker.line, 1, 'second marker should be restored'); assert.equal(thirdMarker.line, 2, 'third marker should be restored'); }); + it('should wrap wide characters correctly when reflowing larger', () => { + buffer.fillViewportRows(); + buffer.resize(12, 10); + for (let i = 0; i < 12; i += 4) { + buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + } + for (let i = 2; i < 12; i += 4) { + buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]); + buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + buffer.lines.get(0).set(i, [null, '', 0, undefined]); + buffer.lines.get(1).set(i, [null, '', 0, undefined]); + } + buffer.lines.get(1).isWrapped = true; + // Buffer: + // 汉语汉语汉语 (wrapped) + // 汉语汉语汉语 + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); + buffer.resize(13, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语 '); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(false), '汉语汉语汉语 '); + buffer.resize(14, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语汉'); + assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 '); + }); + it('should wrap wide characters correctly when reflowing smaller', () => { + // TODO: .. + }); describe('reflowLarger cases', () => { beforeEach(() => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 987e0324..72e43aa0 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -259,24 +259,36 @@ export class Buffer implements IBuffer { // Copy buffer data to new locations let destLineIndex = 0; - let destCol = this._cols; + let destCol = wrappedLines[destLineIndex].getTrimmedLength(); let srcLineIndex = 1; let srcCol = 0; while (srcLineIndex < wrappedLines.length) { - const srcRemainingCells = this._cols - srcCol; + const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength(); + const srcRemainingCells = srcTrimmedTineLength - srcCol; const destRemainingCells = newCols - destCol; const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + destCol += cellsToCopy; if (destCol === newCols) { destLineIndex++; destCol = 0; } srcCol += cellsToCopy; - if (srcCol === this._cols) { + if (srcCol === srcTrimmedTineLength) { srcLineIndex++; srcCol = 0; } + + // Make sure the last cell isn't wide, if it is copy it to the current dest + if (destCol === 0) { + if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); + // Null out the end of the last row + wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA); + } + } } // Clear out remaining cells or fragments could remain; diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 2fa4ef13..6bc30586 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -54,6 +54,10 @@ export class BufferLine implements IBufferLine { ]; } + public getWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.WIDTH]; + } + public set(index: number, value: CharData): void { this._data[index * CELL_SIZE + Cell.FLAGS] = value[0]; if (value[1].length > 1) { From ce69dceee7fec72cc80e2f61aafb382abbd5bfaf Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 21:46:35 -0800 Subject: [PATCH 31/50] Progress on reflow smaller with wide chars --- src/Buffer.test.ts | 35 ++++++++++++- src/Buffer.ts | 24 ++++++--- src/BufferReflow.test.ts | 80 +++++++++++++++++++++++++++++ src/BufferReflow.ts | 107 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 src/BufferReflow.test.ts create mode 100644 src/BufferReflow.ts diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 040eb3ca..8e1a2f60 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -545,7 +545,40 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 '); }); it('should wrap wide characters correctly when reflowing smaller', () => { - // TODO: .. + buffer.fillViewportRows(); + buffer.resize(12, 10); + for (let i = 0; i < 12; i += 4) { + buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + } + for (let i = 2; i < 12; i += 4) { + buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]); + buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + buffer.lines.get(0).set(i, [null, '', 0, undefined]); + buffer.lines.get(1).set(i, [null, '', 0, undefined]); + } + buffer.lines.get(1).isWrapped = true; + // Buffer: + // 汉语汉语汉语 (wrapped) + // 汉语汉语汉语 + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); + buffer.resize(11, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉', '1'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语', '2'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); + buffer.resize(10, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); + buffer.resize(9, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); }); describe('reflowLarger cases', () => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 72e43aa0..30941324 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,6 +9,7 @@ import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; +import { reflowSmallerGetLinesNeeded, reflowSmallerGetNewLineLengths } from './BufferReflow'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -386,11 +387,17 @@ export class Buffer implements IBuffer { wrappedLines.unshift(nextLine); } - // Determine how many lines need to be inserted at the end, based on the trimmed length of - // the last wrapped line + + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; - const linesNeeded = Math.ceil(cellsNeeded / newCols); + // const linesNeeded = reflowSmallerGetLinesNeeded(wrappedLines, this._cols, newCols); + const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); + console.log(destLineLengths); + const linesNeeded = destLineLengths.length; + + + const linesToAdd = linesNeeded - wrappedLines.length; let trimmedLines: number; if (this.ybase === 0 && this.y !== this.lines.length - 1) { @@ -418,8 +425,8 @@ export class Buffer implements IBuffer { wrappedLines.push(...newLines); // Copy buffer data to new locations, this needs to happen backwards to do in-place - let destLineIndex = Math.floor(cellsNeeded / newCols); - let destCol = cellsNeeded % newCols; + let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols); + let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols; if (destCol === 0) { destLineIndex--; destCol = newCols; @@ -432,12 +439,13 @@ export class Buffer implements IBuffer { destCol -= cellsToCopy; if (destCol === 0) { destLineIndex--; - destCol = newCols; + destCol = destLineLengths[destLineIndex]; } srcCol -= cellsToCopy; if (srcCol === 0) { srcLineIndex--; - srcCol = this._cols; + // TODO: srcCol shoudl take trimmed length into account + srcCol = wrappedLines[Math.max(srcLineIndex, 0)].getTrimmedLength(); // this._cols; } } @@ -516,6 +524,8 @@ export class Buffer implements IBuffer { } } + // private _reflowSmallerGetLinesNeeded() + /** * Translates a string index back to a BufferIndex. * To get the correct buffer position the string must start at `startCol` 0 diff --git a/src/BufferReflow.test.ts b/src/BufferReflow.test.ts new file mode 100644 index 00000000..a788fbc8 --- /dev/null +++ b/src/BufferReflow.test.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { assert } from 'chai'; +import { BufferLine } from './BufferLine'; +import { reflowSmallerGetNewLineLengths } from './BufferReflow'; + +describe('BufferReflow', () => { + describe('reflowSmallerGetNewLineLengths', () => { + it('should return correct line lengths for a small line with wide characters', () => { + const line = new BufferLine(4); + line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]); + line.set(1, [null, '', 0, undefined]); + line.set(2, [null, '语', 2, '语'.charCodeAt(0)]); + line.set(3, [null, '', 0, undefined]); + assert.equal(line.translateToString(true), '汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语'); + }); + it('should return correct line lengths for a large line with wide characters', () => { + const line = new BufferLine(12); + for (let i = 0; i < 12; i += 4) { + line.set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); + line.set(i + 2, [null, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + line.set(i, [null, '', 0, undefined]); + line.set(i, [null, '', 0, undefined]); + } + assert.equal(line.translateToString(), '汉语汉语汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 11), [10, 2], 'line: 汉语汉语汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 10), [10, 2], 'line: 汉语汉语汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 9), [8, 4], 'line: 汉语汉语, 汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 8), [8, 4], 'line: 汉语汉语, 汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 7), [6, 6], 'line: 汉语汉, 语汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 6), [6, 6], 'line: 汉语汉, 语汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 5), [4, 4, 4], 'line: 汉语, 汉语, 汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 4), [4, 4, 4], 'line: 汉语, 汉语, 汉语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 3), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 2), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语'); + }); + it('should return correct line lengths for a string with wide and single characters', () => { + const line = new BufferLine(6); + line.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + line.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]); + line.set(2, [null, '', 0, undefined]); + line.set(3, [null, '语', 2, '语'.charCodeAt(0)]); + line.set(4, [null, '', 0, undefined]); + line.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]); + assert.equal(line.translateToString(), 'a汉语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 5), [5, 1], 'line: a汉语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 4), [3, 3], 'line: a汉, 语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 3), [3, 3], 'line: a汉, 语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 2), [1, 2, 2, 1], 'line: a, 汉, 语, b'); + }); + it('should return correct line lengths for a wrapped line with wide and single characters', () => { + const line1 = new BufferLine(6); + line1.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + line1.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]); + line1.set(2, [null, '', 0, undefined]); + line1.set(3, [null, '语', 2, '语'.charCodeAt(0)]); + line1.set(4, [null, '', 0, undefined]); + line1.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]); + const line2 = new BufferLine(6, undefined, true); + line2.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); + line2.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]); + line2.set(2, [null, '', 0, undefined]); + line2.set(3, [null, '语', 2, '语'.charCodeAt(0)]); + line2.set(4, [null, '', 0, undefined]); + line2.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]); + assert.equal(line1.translateToString(), 'a汉语b'); + assert.equal(line2.translateToString(), 'a汉语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 5), [5, 4, 3], 'lines: a汉语, ba汉, 语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 4), [3, 4, 4, 1], 'lines: a汉, 语ba, 汉语, b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 3), [3, 3, 3, 3], 'lines: a汉, 语b, a汉, 语b'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b'); + }); + }); +}); diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts new file mode 100644 index 00000000..f56d48c5 --- /dev/null +++ b/src/BufferReflow.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { BufferLine } from './BufferLine'; + +/** + * Determine how many lines need to be inserted at the end. This is done by finding what each + * wrapping point will be and counting the lines needed This would be a lot simpler but in the case + * of a line ending with a wide character, the wide character needs to be put on the following line + * or it would be cut in half. + * @param wrappedLines The original wrapped lines. + * @param newCols The new column count. + */ +export function reflowSmallerGetLinesNeeded(wrappedLines: BufferLine[], oldCols: number, newCols: number): number { + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); + // const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; + + // TODO: Make faster + const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); + + // Lines needed needs to take into account what the ending character of each new line is + let linesNeeded = 0; + let cellsAvailable = 0; + // let currentCol = 0; + + // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and + // linesNeeded + let srcCol = -1; + let srcLine = 0; + while (cellsAvailable < cellsNeeded) { + // if (srcLine === wrappedLines.length - 1) { + // cellsAvailable += newCols; + // linesNeeded++; + // break; + // } + + srcCol += newCols; + if (srcCol >= oldCols) { + srcCol -= oldCols; + srcLine++; + } + if (srcLine >= wrappedLines.length) { + linesNeeded++; + break; + } + const endsWithWide = wrappedLines[srcLine].getWidth(srcCol) === 2; + if (endsWithWide) { + srcCol--; + } + cellsAvailable += endsWithWide ? newCols - 1 : newCols; + linesNeeded++; + } + + return linesNeeded; + // return Math.ceil(cellsNeeded / newCols); +} + +/** + * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- + * compute the wrapping points since wide characters may need to be wrapped onto the following line. + * This function will return an array of numbers of where each line wraps to, the resulting array + * will only contain the values `newCols` (when the line does not end with a wide character) and + * `newCols - 1` (when the line does end with a wide character), except for the last value which + * will contain the remaining items to fill the line. + * + * Calling this with a `newCols` value of `1` will lock up. + * + * @param wrappedLines The wrapped lines to evaluate. + * @param oldCols The columns before resize. + * @param newCols The columns after resize. + */ +export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { + const newLineLengths: number[] = []; + + // TODO: Force cols = 2 to be minimum possible value, this will lock up + + const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); + + // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and + // linesNeeded + let srcCol = -1; + let srcLine = 0; + let cellsAvailable = 0; + while (cellsAvailable < cellsNeeded) { + srcCol += newCols; + if (srcCol >= oldCols) { + srcCol -= oldCols; + srcLine++; + } + if (srcLine >= wrappedLines.length) { + // Add the final line and exit the loop + newLineLengths.push(cellsNeeded - cellsAvailable); + break; + } + const endsWithWide = wrappedLines[srcLine].getWidth(srcCol) === 2; + if (endsWithWide) { + srcCol--; + } + const lineLength = endsWithWide ? newCols - 1 : newCols; + newLineLengths.push(lineLength); + cellsAvailable += lineLength; + } + + return newLineLengths; +} From df7cd9c319f42aaff3b3e79ef45a19ebda0cf719 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 22:40:40 -0800 Subject: [PATCH 32/50] Get reflow smaller working for wide chars --- src/Buffer.test.ts | 4 ++-- src/Buffer.ts | 9 ++++++++- src/BufferReflow.test.ts | 13 +++++++++++++ src/BufferReflow.ts | 17 +++++++++-------- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 8e1a2f60..82cdff6b 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -568,8 +568,8 @@ describe('Buffer', () => { buffer.resize(11, 10); assert.equal(buffer.ybase, 0); assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉', '1'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语', '2'); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); buffer.resize(10, 10); assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); diff --git a/src/Buffer.ts b/src/Buffer.ts index 30941324..5d3c1a52 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -429,7 +429,7 @@ export class Buffer implements IBuffer { let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols; if (destCol === 0) { destLineIndex--; - destCol = newCols; + destCol = destLineLengths[destLineIndex]; } let srcLineIndex = wrappedLines.length - linesToAdd - 1; let srcCol = lastLineLength; @@ -449,6 +449,13 @@ export class Buffer implements IBuffer { } } + // Null out the end of the line ends if a wide character wrapped to the following line + for (let i = 0; i < wrappedLines.length; i++) { + if (destLineLengths[i] < newCols) { + wrappedLines[i].set(destLineLengths[i], FILL_CHAR_DATA); + } + } + // Adjust viewport as needed let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { diff --git a/src/BufferReflow.test.ts b/src/BufferReflow.test.ts index a788fbc8..9c978dc0 100644 --- a/src/BufferReflow.test.ts +++ b/src/BufferReflow.test.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import { BufferLine } from './BufferLine'; import { reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; describe('BufferReflow', () => { describe('reflowSmallerGetNewLineLengths', () => { @@ -76,5 +77,17 @@ describe('BufferReflow', () => { assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 3), [3, 3, 3, 3], 'lines: a汉, 语b, a汉, 语b'); assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b'); }); + it('should work on lines ending in null space', () => { + const line = new BufferLine(5); + line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]); + line.set(1, [null, '', 0, undefined]); + line.set(2, [null, '语', 2, '语'.charCodeAt(0)]); + line.set(3, [null, '', 0, undefined]); + line.set(4, [null, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + assert.equal(line.translateToString(true), '汉语'); + assert.equal(line.translateToString(false), '汉语 '); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语'); + assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语'); + }); }); }); diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index f56d48c5..f99acd1c 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -80,21 +80,22 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and // linesNeeded - let srcCol = -1; + let srcCol = 0; let srcLine = 0; let cellsAvailable = 0; while (cellsAvailable < cellsNeeded) { - srcCol += newCols; - if (srcCol >= oldCols) { - srcCol -= oldCols; - srcLine++; - } - if (srcLine >= wrappedLines.length) { + if (cellsNeeded - cellsAvailable < newCols) { // Add the final line and exit the loop newLineLengths.push(cellsNeeded - cellsAvailable); break; } - const endsWithWide = wrappedLines[srcLine].getWidth(srcCol) === 2; + srcCol += newCols; + const oldTrimmedLength = wrappedLines[srcLine].getTrimmedLength(); + if (srcCol > oldTrimmedLength) { + srcCol -= oldTrimmedLength; + srcLine++; + } + const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2; if (endsWithWide) { srcCol--; } From 178c513407a86c4d1f64c1cf726091a044a7e16b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 22:48:32 -0800 Subject: [PATCH 33/50] Clean up --- src/Buffer.test.ts | 14 ++++++++++++ src/Buffer.ts | 13 ++--------- src/BufferReflow.ts | 54 --------------------------------------------- src/Terminal.ts | 11 +++++---- 4 files changed, 23 insertions(+), 69 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 82cdff6b..1ef2e92c 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -579,6 +579,20 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); + buffer.resize(8, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); + buffer.resize(7, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(3).translateToString(true), '语汉语'); + buffer.resize(6, 10); + assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(1).translateToString(true), '语汉语'); + assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(3).translateToString(true), '语汉语'); }); describe('reflowLarger cases', () => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 5d3c1a52..f534087d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,7 +9,7 @@ import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetLinesNeeded, reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { reflowSmallerGetNewLineLengths } from './BufferReflow'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -387,18 +387,9 @@ export class Buffer implements IBuffer { wrappedLines.unshift(nextLine); } - - const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; - // const linesNeeded = reflowSmallerGetLinesNeeded(wrappedLines, this._cols, newCols); const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); - console.log(destLineLengths); - const linesNeeded = destLineLengths.length; - - - - const linesToAdd = linesNeeded - wrappedLines.length; + const linesToAdd = destLineLengths.length - wrappedLines.length; let trimmedLines: number; if (this.ybase === 0 && this.y !== this.lines.length - 1) { // If the top section of the buffer is not yet filled diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index f99acd1c..5e815c7a 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -5,58 +5,6 @@ import { BufferLine } from './BufferLine'; -/** - * Determine how many lines need to be inserted at the end. This is done by finding what each - * wrapping point will be and counting the lines needed This would be a lot simpler but in the case - * of a line ending with a wide character, the wide character needs to be put on the following line - * or it would be cut in half. - * @param wrappedLines The original wrapped lines. - * @param newCols The new column count. - */ -export function reflowSmallerGetLinesNeeded(wrappedLines: BufferLine[], oldCols: number, newCols: number): number { - const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - // const cellsNeeded = (wrappedLines.length - 1) * this._cols + lastLineLength; - - // TODO: Make faster - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); - - // Lines needed needs to take into account what the ending character of each new line is - let linesNeeded = 0; - let cellsAvailable = 0; - // let currentCol = 0; - - // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and - // linesNeeded - let srcCol = -1; - let srcLine = 0; - while (cellsAvailable < cellsNeeded) { - // if (srcLine === wrappedLines.length - 1) { - // cellsAvailable += newCols; - // linesNeeded++; - // break; - // } - - srcCol += newCols; - if (srcCol >= oldCols) { - srcCol -= oldCols; - srcLine++; - } - if (srcLine >= wrappedLines.length) { - linesNeeded++; - break; - } - const endsWithWide = wrappedLines[srcLine].getWidth(srcCol) === 2; - if (endsWithWide) { - srcCol--; - } - cellsAvailable += endsWithWide ? newCols - 1 : newCols; - linesNeeded++; - } - - return linesNeeded; - // return Math.ceil(cellsNeeded / newCols); -} - /** * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- * compute the wrapping points since wide characters may need to be wrapped onto the following line. @@ -74,8 +22,6 @@ export function reflowSmallerGetLinesNeeded(wrappedLines: BufferLine[], oldCols: export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { const newLineLengths: number[] = []; - // TODO: Force cols = 2 to be minimum possible value, this will lock up - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and diff --git a/src/Terminal.ts b/src/Terminal.ts index 4c0cd0f8..8a5d96ba 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -69,6 +69,9 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; */ const WRITE_BATCH_SIZE = 300; +const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars +const MINIMUM_ROWS = 1; + /** * The set of options that only have an effect when set in the Terminal constructor. */ @@ -262,8 +265,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: WHy not document.body? this._parent = document ? document.body : null; - this.cols = this.options.cols; - this.rows = this.options.rows; + this.cols = Math.max(this.options.cols, MINIMUM_COLS); + this.rows = Math.max(this.options.rows, MINIMUM_ROWS); if (this.options.handler) { this.on('data', this.options.handler); @@ -1691,8 +1694,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return; } - if (x < 1) x = 1; - if (y < 1) y = 1; + if (x < MINIMUM_COLS) x = MINIMUM_COLS; + if (y < MINIMUM_ROWS) y = MINIMUM_ROWS; this.buffers.resize(x, y); From dfff04cf3db7e29ba1c0e3097bcf16ac8f91af5e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 23:51:28 -0800 Subject: [PATCH 34/50] Pull toRemove step into BufferReflow --- src/Buffer.ts | 77 +++----------------------------------------- src/BufferReflow.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 72 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index f534087d..2f1c2e49 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,7 +9,7 @@ import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove } from './BufferReflow'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -26,7 +26,7 @@ export const WHITESPACE_CELL_CHAR = ' '; export const WHITESPACE_CELL_WIDTH = 1; export const WHITESPACE_CELL_CODE = 32; -const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; +export const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; /** * This class represents a terminal buffer (an internal state of the terminal), where the @@ -240,78 +240,11 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number): void { + // TODO: Can toRemove be pulled out into BufferReflow? + // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once - const toRemove: number[] = []; - for (let y = 0; y < this.lines.length - 1; y++) { - // Check if this row is wrapped - let i = y; - let nextLine = this.lines.get(++i) as BufferLine; - if (!nextLine.isWrapped) { - continue; - } - - // Check how many lines it's wrapped for - const wrappedLines: BufferLine[] = [this.lines.get(y) as BufferLine]; - while (i < this.lines.length && nextLine.isWrapped) { - wrappedLines.push(nextLine); - nextLine = this.lines.get(++i) as BufferLine; - } - - // Copy buffer data to new locations - let destLineIndex = 0; - let destCol = wrappedLines[destLineIndex].getTrimmedLength(); - let srcLineIndex = 1; - let srcCol = 0; - while (srcLineIndex < wrappedLines.length) { - const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength(); - const srcRemainingCells = srcTrimmedTineLength - srcCol; - const destRemainingCells = newCols - destCol; - const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); - - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); - - destCol += cellsToCopy; - if (destCol === newCols) { - destLineIndex++; - destCol = 0; - } - srcCol += cellsToCopy; - if (srcCol === srcTrimmedTineLength) { - srcLineIndex++; - srcCol = 0; - } - - // Make sure the last cell isn't wide, if it is copy it to the current dest - if (destCol === 0) { - if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); - // Null out the end of the last row - wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA); - } - } - } - - // Clear out remaining cells or fragments could remain; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); - - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - - if (countToRemove > 0) { - toRemove.push(y + wrappedLines.length - countToRemove); // index - toRemove.push(countToRemove); - } - - y += wrappedLines.length - 1; - } + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); if (toRemove.length > 0) { // First iterate through the list and get the actual indexes to use for rows diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 5e815c7a..310443b7 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -4,6 +4,84 @@ */ import { BufferLine } from './BufferLine'; +import { CircularList } from './common/CircularList'; +import { IBufferLine } from './Types'; +import { FILL_CHAR_DATA } from './Buffer'; + +export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { + const toRemove: number[] = []; + + for (let y = 0; y < lines.length - 1; y++) { + // Check if this row is wrapped + let i = y; + let nextLine = lines.get(++i) as BufferLine; + if (!nextLine.isWrapped) { + continue; + } + + // Check how many lines it's wrapped for + const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine]; + while (i < lines.length && nextLine.isWrapped) { + wrappedLines.push(nextLine); + nextLine = lines.get(++i) as BufferLine; + } + + // Copy buffer data to new locations + let destLineIndex = 0; + let destCol = wrappedLines[destLineIndex].getTrimmedLength(); + let srcLineIndex = 1; + let srcCol = 0; + while (srcLineIndex < wrappedLines.length) { + const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength(); + const srcRemainingCells = srcTrimmedTineLength - srcCol; + const destRemainingCells = newCols - destCol; + const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); + + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + + destCol += cellsToCopy; + if (destCol === newCols) { + destLineIndex++; + destCol = 0; + } + srcCol += cellsToCopy; + if (srcCol === srcTrimmedTineLength) { + srcLineIndex++; + srcCol = 0; + } + + // Make sure the last cell isn't wide, if it is copy it to the current dest + if (destCol === 0) { + if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { + wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); + // Null out the end of the last row + wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA); + } + } + } + + // Clear out remaining cells or fragments could remain; + wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); + + // Work backwards and remove any rows at the end that only contain null cells + let countToRemove = 0; + for (let i = wrappedLines.length - 1; i > 0; i--) { + if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { + countToRemove++; + } else { + break; + } + } + + if (countToRemove > 0) { + toRemove.push(y + wrappedLines.length - countToRemove); // index + toRemove.push(countToRemove); + } + + y += wrappedLines.length - 1; + } + return toRemove; +} /** * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- From d4bd8ae2be96ff976568811c70e6f3907a700149 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 23 Jan 2019 23:58:38 -0800 Subject: [PATCH 35/50] Pull more parts out of reflow larger --- src/Buffer.ts | 70 ++++++++++++--------------------------------- src/BufferReflow.ts | 43 +++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 2f1c2e49..286d1311 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -9,7 +9,7 @@ import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove } from './BufferReflow'; +import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove, reflowLargerCreateNewLayout, reflowLargerApplyNewLayout } from './BufferReflow'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -240,62 +240,28 @@ export class Buffer implements IBuffer { } private _reflowLarger(newCols: number): void { - // TODO: Can toRemove be pulled out into BufferReflow? - - // Gather all BufferLines that need to be removed from the Buffer here so that they can be - // batched up and only committed once const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); - if (toRemove.length > 0) { - // First iterate through the list and get the actual indexes to use for rows const newLayout: number[] = []; + const countRemoved = reflowLargerCreateNewLayout(this.lines, toRemove, newLayout); + reflowLargerApplyNewLayout(this.lines, newLayout); + this._reflowLargerAdjustViewport(newCols, countRemoved); + } + } - let nextToRemoveIndex = 0; - let nextToRemoveStart = toRemove[nextToRemoveIndex]; - let countRemovedSoFar = 0; - for (let i = 0; i < this.lines.length; i++) { - if (nextToRemoveStart === i) { - const countToRemove = toRemove[++nextToRemoveIndex]; - - // Tell markers that there was a deletion - this.lines.emit('delete', { - index: i - countRemovedSoFar, - amount: countToRemove - } as IDeleteEvent); - - i += countToRemove - 1; - countRemovedSoFar += countToRemove; - nextToRemoveStart = toRemove[++nextToRemoveIndex]; - } else { - newLayout.push(i); - } - } - - // Record original lines so they don't get overridden when we rearrange the list - const newLayoutLines: BufferLine[] = []; - for (let i = 0; i < newLayout.length; i++) { - newLayoutLines.push(this.lines.get(newLayout[i]) as BufferLine); - } - - // Rearrange the list - for (let i = 0; i < newLayoutLines.length; i++) { - this.lines.set(i, newLayoutLines[i]); - } - this.lines.length = newLayout.length; - - // Adjust viewport based on number of items removed - let viewportAdjustments = countRemovedSoFar; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; + private _reflowLargerAdjustViewport(newCols: number, countRemoved: number): void { + // Adjust viewport based on number of items removed + let viewportAdjustments = countRemoved; + while (viewportAdjustments-- > 0) { + if (this.ybase === 0) { + this.y--; + // Add an extra row at the bottom of the viewport + this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + } else { + if (this.ydisp === this.ybase) { + this.ydisp--; } + this.ybase--; } } } diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 310443b7..51f83c18 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -4,11 +4,13 @@ */ import { BufferLine } from './BufferLine'; -import { CircularList } from './common/CircularList'; +import { CircularList, IDeleteEvent } from './common/CircularList'; import { IBufferLine } from './Types'; import { FILL_CHAR_DATA } from './Buffer'; export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { + // Gather all BufferLines that need to be removed from the Buffer here so that they can be + // batched up and only committed once const toRemove: number[] = []; for (let y = 0; y < lines.length - 1; y++) { @@ -83,6 +85,45 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n return toRemove; } +export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[], newLayout: number[]): number { + // First iterate through the list and get the actual indexes to use for rows + let nextToRemoveIndex = 0; + let nextToRemoveStart = toRemove[nextToRemoveIndex]; + let countRemovedSoFar = 0; + for (let i = 0; i < lines.length; i++) { + if (nextToRemoveStart === i) { + const countToRemove = toRemove[++nextToRemoveIndex]; + + // Tell markers that there was a deletion + lines.emit('delete', { + index: i - countRemovedSoFar, + amount: countToRemove + } as IDeleteEvent); + + i += countToRemove - 1; + countRemovedSoFar += countToRemove; + nextToRemoveStart = toRemove[++nextToRemoveIndex]; + } else { + newLayout.push(i); + } + } + return countRemovedSoFar; +} + +export function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void { + // Record original lines so they don't get overridden when we rearrange the list + const newLayoutLines: BufferLine[] = []; + for (let i = 0; i < newLayout.length; i++) { + newLayoutLines.push(lines.get(newLayout[i]) as BufferLine); + } + + // Rearrange the list + for (let i = 0; i < newLayoutLines.length; i++) { + lines.set(i, newLayoutLines[i]); + } + lines.length = newLayout.length; +} + /** * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- * compute the wrapping points since wide characters may need to be wrapped onto the following line. From 99ac78031c8ebb1bef723b9aa1e45e11e0c4079a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 24 Jan 2019 09:11:16 -0800 Subject: [PATCH 36/50] Remove out param from reflow large method --- src/Buffer.ts | 7 +++---- src/BufferReflow.ts | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 286d1311..52d9572d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -242,10 +242,9 @@ export class Buffer implements IBuffer { private _reflowLarger(newCols: number): void { const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); if (toRemove.length > 0) { - const newLayout: number[] = []; - const countRemoved = reflowLargerCreateNewLayout(this.lines, toRemove, newLayout); - reflowLargerApplyNewLayout(this.lines, newLayout); - this._reflowLargerAdjustViewport(newCols, countRemoved); + const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); + reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); + this._reflowLargerAdjustViewport(newCols, newLayoutResult.countRemoved); } } diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 51f83c18..003a3c78 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -8,6 +8,11 @@ import { CircularList, IDeleteEvent } from './common/CircularList'; import { IBufferLine } from './Types'; import { FILL_CHAR_DATA } from './Buffer'; +export interface INewLayoutResult { + layout: number[]; + countRemoved: number; +} + export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once @@ -85,7 +90,8 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n return toRemove; } -export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[], newLayout: number[]): number { +export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult { + const layout: number[] = []; // First iterate through the list and get the actual indexes to use for rows let nextToRemoveIndex = 0; let nextToRemoveStart = toRemove[nextToRemoveIndex]; @@ -104,10 +110,13 @@ export function reflowLargerCreateNewLayout(lines: CircularList, to countRemovedSoFar += countToRemove; nextToRemoveStart = toRemove[++nextToRemoveIndex]; } else { - newLayout.push(i); + layout.push(i); } } - return countRemovedSoFar; + return { + layout, + countRemoved: countRemovedSoFar + }; } export function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void { From 109e3a50e8e91a1b328cac7cc81391222c68dec8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 24 Jan 2019 10:20:18 -0800 Subject: [PATCH 37/50] jsdoc --- src/BufferReflow.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 003a3c78..59934e46 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -13,6 +13,12 @@ export interface INewLayoutResult { countRemoved: number; } +/** + * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed + * when a wrapped line unwraps. + * @param lines The buffer lines. + * @param newCols The columns after resize. + */ export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once @@ -90,6 +96,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n return toRemove; } +/** + * Creates and return the new layout for lines given an array of indexes to be removed. + * @param lines The buffer lines. + * @param toRemove The indexes to remove. + */ export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult { const layout: number[] = []; // First iterate through the list and get the actual indexes to use for rows @@ -119,6 +130,12 @@ export function reflowLargerCreateNewLayout(lines: CircularList, to }; } +/** + * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's + * done all at once in a single iteration through the list since splice is very expensive. + * @param lines The buffer lines. + * @param newLayout The new layout to apply. + */ export function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void { // Record original lines so they don't get overridden when we rearrange the list const newLayoutLines: BufferLine[] = []; From 4855b60ffeef2cfd1e3bb36eb958e68e5713ea8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 25 Jan 2019 17:35:12 +0100 Subject: [PATCH 38/50] move utf32ToString to TextDecoder.ts --- src/EscapeSequenceParser.ts | 2 +- src/InputHandler.ts | 4 +- src/common/TypedArrayUtils.test.ts | 10 +-- src/common/TypedArrayUtils.ts | 22 +----- src/common/Types.ts | 4 ++ src/core/input/TextDecoder.test.ts | 112 ++++++++++++++--------------- src/core/input/TextDecoder.ts | 25 +++++-- 7 files changed, 86 insertions(+), 93 deletions(-) diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index ec7a9da7..7b65624d 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -6,7 +6,7 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; -import { utf32ToString } from './common/TypedArrayUtils'; +import { utf32ToString } from './core/input/TextDecoder'; interface IHandlerCollection { [key: string]: T[]; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index a702e2a6..2f53cfcb 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,8 +14,8 @@ import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; -import { concat, utf32ToString } from './common/TypedArrayUtils'; -import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; +import { concat } from './common/TypedArrayUtils'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder'; /** * Map collect to glevel. Used in `selectCharset`. diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts index 79546ca9..99b0fd82 100644 --- a/src/common/TypedArrayUtils.test.ts +++ b/src/common/TypedArrayUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import { assert } from 'chai'; -import { fillFallback, concat, utf32ToString } from './TypedArrayUtils'; +import { fillFallback, concat } from './TypedArrayUtils'; type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array @@ -94,12 +94,4 @@ describe('typed array convenience functions', () => { const merged = concat(a, b); deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); }); - it('utf16ToString', () => { - const s = 'abcdefg'; - const data = new Uint16Array(s.length); - for (let i = 0; i < s.length; ++i) { - data[i] = s.charCodeAt(i); - } - assert.equal(utf32ToString(data), s); - }); }); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 5f5be782..380dff35 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -2,10 +2,7 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ - -type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray - | Int8Array | Int16Array | Int32Array - | Float32Array | Float64Array; +import { TypedArray } from './Types'; /** * polyfill for TypedArray.fill @@ -49,20 +46,3 @@ export function concat(a: T, b: T): T { result.set(b, a.length); return result; } - -/** - * Convert UTF32 char codes into JS string. - */ -export function utf32ToString(data: T, start: number = 0, end: number = data.length): string { - let result = ''; - let cp; - for (let i = start; i < end; ++i) { - if ((cp = data[i]) > 0xFFFF) { - cp -= 0x10000; - result += String.fromCharCode((cp >> 10) + 0xD800) + String.fromCharCode((cp % 0x400) + 0xDC00); - } else { - result += String.fromCharCode(cp); - } - } - return result; -} diff --git a/src/common/Types.ts b/src/common/Types.ts index 8a416bf1..29b5febb 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -35,3 +35,7 @@ export interface ICircularList extends IEventEmitter { trimStart(count: number): void; shiftElements(start: number, count: number, offset: number): void; } + +export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; diff --git a/src/core/input/TextDecoder.test.ts b/src/core/input/TextDecoder.test.ts index f69fbded..12f3099a 100644 --- a/src/core/input/TextDecoder.test.ts +++ b/src/core/input/TextDecoder.test.ts @@ -4,64 +4,64 @@ */ import { assert } from 'chai'; -import { StringToUtf32, stringFromCodePoint } from './TextDecoder'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from './TextDecoder'; - -// convert UTF32 codepoints to string -function toString(data: Uint32Array, length: number): string { - if ((String as any).fromCodePoint) { - return (String as any).fromCodePoint.apply(null, data.subarray(0, length)); - } - let result = ''; - for (let i = 0; i < length; ++i) { - result += stringFromCodePoint(data[i]); - } - return result; -} - -describe('StringToUtf32 Decoder', () => { - describe('full codepoint test', () => { - it('0..65535', () => { - const decoder = new StringToUtf32(); - const target = new Uint32Array(5); - for (let i = 0; i < 65536; ++i) { - // skip surrogate pairs - if (i >= 0xD800 && i <= 0xDFFF) { - continue; - } - const length = decoder.decode(String.fromCharCode(i), target); - assert.equal(length, 1); - assert.equal(target[0], i); - assert.equal(toString(target, length), String.fromCharCode(i)); - decoder.clear(); - } - }); - it('65536..0x10FFFF (surrogates)', function(): void { - this.timeout(20000); - const decoder = new StringToUtf32(); - const target = new Uint32Array(5); - for (let i = 65536; i < 0x10FFFF; ++i) { - const codePoint = i - 0x10000; - const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); - const length = decoder.decode(s, target); - assert.equal(length, 1); - assert.equal(target[0], i); - assert.equal(toString(target, length), s); - decoder.clear(); - } - }); +describe('text encodings', () => { + it('stringFromCodePoint/utf32ToString', () => { + const s = 'abcdefg'; + const data = new Uint32Array(s.length); + for (let i = 0; i < s.length; ++i) { + data[i] = s.charCodeAt(i); + assert.equal(stringFromCodePoint(data[i]), s[i]); + } + assert.equal(utf32ToString(data), s); }); - describe('stream handling', () => { - it('surrogates mixed advance by 1', () => { - const decoder = new StringToUtf32(); - const target = new Uint32Array(5); - const input = 'Ä€𝄞Ö𝄞€Ü𝄞€'; - let decoded = ''; - for (let i = 0; i < input.length; ++i) { - const written = decoder.decode(input[i], target); - decoded += toString(target, written); - } - assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + + describe('StringToUtf32 Decoder', () => { + describe('full codepoint test', () => { + it('0..65535', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 0; i < 65536; ++i) { + // skip surrogate pairs + if (i >= 0xD800 && i <= 0xDFFF) { + continue; + } + const length = decoder.decode(String.fromCharCode(i), target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(utf32ToString(target, 0, length), String.fromCharCode(i)); + decoder.clear(); + } + }); + it('65536..0x10FFFF (surrogates)', function(): void { + this.timeout(20000); + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 65536; i < 0x10FFFF; ++i) { + const codePoint = i - 0x10000; + const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + const length = decoder.decode(s, target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(utf32ToString(target, 0, length), s); + decoder.clear(); + } + }); + }); + + describe('stream handling', () => { + it('surrogates mixed advance by 1', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + const input = 'Ä€𝄞Ö𝄞€Ü𝄞€'; + let decoded = ''; + for (let i = 0; i < input.length; ++i) { + const written = decoder.decode(input[i], target); + decoded += utf32ToString(target, written); + } + assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + }); }); }); }); diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index 77e6971c..f83959e6 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -2,6 +2,7 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ +import { TypedArray } from '../../common/Types'; /** * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. @@ -73,15 +74,31 @@ export class StringToUtf32 { } /** - * Polyfill - Convert UTF32 codepoint into JS string. + * Convert UTF32 codepoint into JS string. */ export function stringFromCodePoint(codePoint: number): string { - if ((String as any).fromCodePoint) { - return (String as any).fromCodePoint(codePoint); - } if (codePoint > 0xFFFF) { codePoint -= 0x10000; return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); } return String.fromCharCode(codePoint); } + +/** + * Convert UTF32 char codes into JS string. + * Basically the same as `stringFromCodePoint` but for multiple codepoints + * in a loop (which is a lot faster). + */ +export function utf32ToString(data: T, start: number = 0, end: number = data.length): string { + let result = ''; + let cp; + for (let i = start; i < end; ++i) { + if ((cp = data[i]) > 0xFFFF) { + cp -= 0x10000; + result += String.fromCharCode((cp >> 10) + 0xD800) + String.fromCharCode((cp % 0x400) + 0xDC00); + } else { + result += String.fromCharCode(cp); + } + } + return result; +} From 87e97897488c4bbffbed41163484a931b61d7926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 25 Jan 2019 18:16:01 +0100 Subject: [PATCH 39/50] more explicit docs for DCS parsers --- src/Types.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Types.ts b/src/Types.ts index 7caa1335..5bd97b10 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -452,14 +452,22 @@ export interface IParsingState { * DCS handler signature for EscapeSequenceParser. * EscapeSequenceParser handles DCS commands via separate * subparsers that get hook/unhooked and can handle -* arbitrary amount of print data. +* arbitrary amount of data. +* * On entering a DSC sequence `hook` is called by * `EscapeSequenceParser`. Use it to initialize or reset * states needed to handle the current DCS sequence. +* Note: A DCS parser is only instantiated once, therefore +* you cannot rely on the ctor to reinitialize state. +* * EscapeSequenceParser will call `put` several times if the -* parsed string got splitted, therefore you might have to collect -* `data` until `unhook` is called. `unhook` marks the end -* of the current DCS sequence. +* parsed data got split, therefore you might have to collect +* `data` until `unhook` is called. +* Note: `data` is borrowed, if you cannot process the data +* in chunks you have to copy it, doing otherwise will lead to +* data losses or corruption. +* +* `unhook` marks the end of the current DCS sequence. */ export interface IDcsHandler { hook(collect: string, params: number[], flag: number): void; From 90a41a113a16c5e445b4bf56556a1ec475983c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 13:22:34 +0100 Subject: [PATCH 40/50] review changes --- src/core/input/TextDecoder.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index f83959e6..1ff00940 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -2,7 +2,6 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ -import { TypedArray } from '../../common/Types'; /** * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. @@ -78,6 +77,7 @@ export class StringToUtf32 { */ export function stringFromCodePoint(codePoint: number): string { if (codePoint > 0xFFFF) { + // UTF32 to UTF16 conversion (see comments in utf32ToString) codePoint -= 0x10000; return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); } @@ -89,15 +89,20 @@ export function stringFromCodePoint(codePoint: number): string { * Basically the same as `stringFromCodePoint` but for multiple codepoints * in a loop (which is a lot faster). */ -export function utf32ToString(data: T, start: number = 0, end: number = data.length): string { +export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string { let result = ''; - let cp; for (let i = start; i < end; ++i) { - if ((cp = data[i]) > 0xFFFF) { - cp -= 0x10000; - result += String.fromCharCode((cp >> 10) + 0xD800) + String.fromCharCode((cp % 0x400) + 0xDC00); + let codepoint = data[i]; + if (codepoint > 0xFFFF) { + // JS string are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair + // conversion rules: + // - subtract 0x10000 from code point, leaving a 20 bit number + // - add high 10 bits to 0xD800 --> first surrogate + // - add low 10 bits to 0xDC00 --> second surrogate + codepoint -= 0x10000; + result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00); } else { - result += String.fromCharCode(cp); + result += String.fromCharCode(codepoint); } } return result; From e67bef69f49d967f030c35e8abbd50f186a0eb72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 17:28:15 +0100 Subject: [PATCH 41/50] cleanup types --- src/common/TypedArrayUtils.ts | 6 +++++- src/common/Types.ts | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 380dff35..54699835 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -2,7 +2,11 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { TypedArray } from './Types'; + +export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; + /** * polyfill for TypedArray.fill diff --git a/src/common/Types.ts b/src/common/Types.ts index 29b5febb..8a416bf1 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -35,7 +35,3 @@ export interface ICircularList extends IEventEmitter { trimStart(count: number): void; shiftElements(start: number, count: number, offset: number): void; } - -export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray - | Int8Array | Int16Array | Int32Array - | Float32Array | Float64Array; From 358d70d7218df418c8e10b9a6aa43ccf9786dccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 17:34:58 +0100 Subject: [PATCH 42/50] cleanup gitignore --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2ee021f8..b50ab2d9 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,3 @@ coverage/ # Keep bundled code out of Git dist/ demo/dist/ - -# dont pullin other files from .vscode than launch.json -.vscode/ -!.vscode/launch.json From b20730ee4d3090c653e7f00e175815641f0bb149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 19:40:28 +0100 Subject: [PATCH 43/50] add more docs --- src/Buffer.ts | 10 ++++++++++ src/BufferLine.ts | 35 +++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 2b9b2233..f58d0da0 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -18,10 +18,20 @@ export const CHAR_DATA_WIDTH_INDEX = 2; export const CHAR_DATA_CODE_INDEX = 3; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 +/** + * Null cell - a real empty cell (containing nothing). + * Note that code should always be 0 for a null cell as + * several test condition of the buffer line rely on this. + */ export const NULL_CELL_CHAR = ''; export const NULL_CELL_WIDTH = 1; export const NULL_CELL_CODE = 0; +/** + * Whilespace cell. + * This is meant as a replacement for empty cells when needed + * during rendering lines to preserve correct aligment. + */ export const WHITESPACE_CELL_CHAR = ' '; export const WHITESPACE_CELL_WIDTH = 1; export const WHITESPACE_CELL_CODE = 32; diff --git a/src/BufferLine.ts b/src/BufferLine.ts index ade21435..3f7ff076 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -78,8 +78,6 @@ export const enum Content { /** * CellData - represents a single Cell in the terminal buffer. - * - * TODO: attr getter */ export class CellData implements ICellData { @@ -117,7 +115,12 @@ export class CellData implements ICellData { return ''; } - /** Codepoint of cell (or last charCode of combined string) */ + /** + * Codepoint of cell + * Note this returns the UTF32 codepoint of single chars, + * if content is a combined string it returns the codepoint + * of the last char in string to be in line with code in CharData. + * */ public get code(): number { return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); } @@ -127,10 +130,14 @@ export class CellData implements ICellData { this.fg = value[CHAR_DATA_ATTR_INDEX]; this.bg = 0; let combined = false; + + // surrogates and combined strings need special treatment if (value[CHAR_DATA_CHAR_INDEX].length > 2) { combined = true; } else if (value[CHAR_DATA_CHAR_INDEX].length === 2) { const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); + // if the 2-char string is a surrogate create single codepoint + // everything else is combined if (0xD800 <= code && code <= 0xDBFF) { const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); if (0xDC00 <= second && second <= 0xDFFF) { @@ -217,24 +224,36 @@ export class BufferLine implements IBufferLine { return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; } + /** Test whether content has width. */ public hasWidth(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; } + /** Get FG cell component. */ public getFG(index: number): number { return this._data[index * CELL_SIZE + Cell.FG]; } + /** Get BG cell component. */ public getBG(index: number): number { return this._data[index * CELL_SIZE + Cell.BG]; } + /** + * Test whether contains any chars. + * Basically an empty has no content, but other cells might differ in FG/BG + * from real empty cells. + * */ public hasContent(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT; } + /** + * Get codepoint of the cell. + * To be in line with `code` in CharData this either returns + * a single UTF32 codepoint or the last codepoint of a combined string. + */ public getCodePoint(index: number): number { - // returns either the single codepoint or the last charCode in combined const content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED) { return this._combined[index].charCodeAt(this._combined[index].length - 1); @@ -242,10 +261,12 @@ export class BufferLine implements IBufferLine { return content & Content.CODEPOINT_MASK; } + /** Test whether the cell contains a combined string. */ public isCombined(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED; } + /** Returns the string content of the cell. */ public getString(index: number): string { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED) { @@ -254,7 +275,8 @@ export class BufferLine implements IBufferLine { if (content & Content.CODEPOINT_MASK) { return stringFromCodePoint(content & Content.CODEPOINT_MASK); } - return ''; // return empty string for empty cells + // return empty string for empty cells + return ''; } /** @@ -276,9 +298,6 @@ export class BufferLine implements IBufferLine { public setCell(index: number, cell: ICellData): void { if (cell.content & Content.IS_COMBINED) { this._combined[index] = cell.combinedData; - // we also need to clear and set codepoint to index - cell.content &= ~Content.CODEPOINT_MASK; - cell.content |= index; } this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; this._data[index * CELL_SIZE + Cell.FG] = cell.fg; From e77e36cbb724b6e88174551046a0ec71246aa00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 19:59:13 +0100 Subject: [PATCH 44/50] fix typo in HAS_CONTENT --- src/BufferLine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 3f7ff076..09f12028 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -60,7 +60,7 @@ export const enum Content { * whether a cell contains anything * read: `isEmtpy = !(content & Content.hasContent)` */ - HAS_CONTENT = 0x2FFFFF, + HAS_CONTENT = 0x3FFFFF, /** * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) From c60f9b14689520a84ddcdc4060e8875c8cd33262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 27 Jan 2019 20:06:58 +0100 Subject: [PATCH 45/50] code formatting --- src/BufferLine.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 09f12028..84a7bfcd 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -122,7 +122,9 @@ export class CellData implements ICellData { * of the last char in string to be in line with code in CharData. * */ public get code(): number { - return ((this.combined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK); + return (this.combined) + ? this.combinedData.charCodeAt(this.combinedData.length - 1) + : this.content & Content.CODEPOINT_MASK; } /** Set data from CharData */ From 39fd6c427d8042dc2199116be1d7414441eb179e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 28 Jan 2019 10:42:44 +0100 Subject: [PATCH 46/50] name polishing, docs --- src/BufferLine.test.ts | 22 +++++++++++----------- src/BufferLine.ts | 27 +++++++++++++++++++-------- src/InputHandler.ts | 10 +++++----- src/Types.ts | 6 +++--- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 2a874f9d..894cc204 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -28,23 +28,23 @@ describe('CellData', () => { // ASCII cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, 'a', 1, 'a'.charCodeAt(0)]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); // combining cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); // surrogate cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); chai.assert.deepEqual(cell.asCharData, [123, '𝄞', 1, 0x1D11E]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); // surrogate + combining cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); chai.assert.deepEqual(cell.asCharData, [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); // wide char cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); chai.assert.deepEqual(cell.asCharData, [123, '1', 2, '1'.charCodeAt(0)]); - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); }); }); @@ -331,39 +331,39 @@ describe('BufferLine', function(): void { describe('addCharToCell', () => { it('should set width to 1 for empty cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); - line.addCharToCell(0, '\u0301'.charCodeAt(0)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); const cell = line.loadCell(0, new CellData()); // chars contains single combining char // width is set to 1 chai.assert.deepEqual(cell.asCharData, [DEFAULT_ATTR, '\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, 0); + chai.assert.equal(cell.isCombined, 0); }); it('should add char to combining string in cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); const cell = line .loadCell(0, new CellData()); cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]); line.setCell(0, cell); - line.addCharToCell(0, '\u0301'.charCodeAt(0)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); line.loadCell(0, cell); // chars contains 3 chars // width is set to 1 chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); }); it('should create combining string on taken cell', () => { const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); const cell = line .loadCell(0, new CellData()); cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]); line.setCell(0, cell); - line.addCharToCell(0, '\u0301'.charCodeAt(0)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); line.loadCell(0, cell); // chars contains 2 chars // width is set to 1 chai.assert.deepEqual(cell.asCharData, [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined - chai.assert.equal(cell.combined, Content.IS_COMBINED); + chai.assert.equal(cell.isCombined, Content.IS_COMBINED); }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 84a7bfcd..120a15fc 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -95,7 +95,7 @@ export class CellData implements ICellData { public combinedData: string = ''; /** Whether cell contains a combined string. */ - public get combined(): number { + public get isCombined(): number { return this.content & Content.IS_COMBINED; } @@ -122,7 +122,7 @@ export class CellData implements ICellData { * of the last char in string to be in line with code in CharData. * */ public get code(): number { - return (this.combined) + return (this.isCombined) ? this.combinedData.charCodeAt(this.combinedData.length - 1) : this.content & Content.CODEPOINT_MASK; } @@ -168,6 +168,18 @@ export class CellData implements ICellData { /** * Typed array based bufferline implementation. + * + * There are 2 ways to insert data into the cell buffer: + * - `setCellFromCodepoint` + `addCodepointToCell` + * Use these for data that is already UTF32. + * Used during normal input in `InputHandler` for faster buffer access. + * - `setCell` + * This method takes a CellData object and stores the data in the buffer. + * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string). + * + * To retrieve data from the buffer use either one of the primitive methods + * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop + * memory allocs / GC pressure can be greatly reduced by reusing the CellData object. */ export class BufferLine implements IBufferLine { protected _data: Uint32Array | null = null; @@ -311,19 +323,19 @@ export class BufferLine implements IBufferLine { * Since the input handler see the incoming chars as UTF32 codepoints, * it gets an optimized access method. */ - public setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { + public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); this._data[index * CELL_SIZE + Cell.FG] = fg; this._data[index * CELL_SIZE + Cell.BG] = bg; } /** - * Add a char to a cell from input handler. + * Add a codepoint to a cell from input handler. * During input stage combining chars with a width of 0 follow and stack * onto a leading char. Since we already set the attrs * by the previous `setDataFromCodePoint` call, we can omit it here. */ - public addCharToCell(index: number, codePoint: number): void { + public addCodepointToCell(index: number, codePoint: number): void { let content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED) { // we already have a combined string, simply add @@ -332,11 +344,10 @@ export class BufferLine implements IBufferLine { if (content & Content.CODEPOINT_MASK) { // normal case for combining chars: // - move current leading char + new one into combined string - // - set codepoint in cell buffer to index // - set combined flag this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); - content &= ~Content.CODEPOINT_MASK; - content |= index | Content.IS_COMBINED; + content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 + content |= Content.IS_COMBINED; } else { // should not happen - we actually have no data in the cell yet // simply set the data in the cell buffer with a width of 1 diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 86483a29..a9555c76 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -355,9 +355,9 @@ export class InputHandler extends Disposable implements IInputHandler { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars - bufferRow.addCharToCell(buffer.x - 2, code); + bufferRow.addCodepointToCell(buffer.x - 2, code); } else { - bufferRow.addCharToCell(buffer.x - 1, code); + bufferRow.addCodepointToCell(buffer.x - 1, code); } continue; } @@ -401,12 +401,12 @@ export class InputHandler extends Disposable implements IInputHandler { // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell if (bufferRow.loadCell(cols - 1, this._cell).width === 2) { - bufferRow.setDataFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); + bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } } // write current char to buffer and advance cursor - bufferRow.setDataFromCodePoint(buffer.x++, code, chWidth, curAttr, 0); + bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr, 0); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero @@ -414,7 +414,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (chWidth > 0) { while (--chWidth) { // other than a regular empty cell a cell following a wide char has no width - bufferRow.setDataFromCodePoint(buffer.x++, 0, 0, curAttr, 0); + bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr, 0); } } } diff --git a/src/Types.ts b/src/Types.ts index dad86fa2..9588505c 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -527,7 +527,7 @@ export interface ICellData { fg: number; bg: number; combinedData: string; - combined: number; + isCombined: number; width: number; chars: string; code: number; @@ -545,8 +545,8 @@ export interface IBufferLine { set(index: number, value: CharData): void; loadCell(index: number, cell: ICellData): ICellData; setCell(index: number, cell: ICellData): void; - setDataFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; - addCharToCell(index: number, codePoint: number): void; + setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; + addCodepointToCell(index: number, codePoint: number): void; insertCells(pos: number, n: number, ch: ICellData): void; deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData): void; From 33e46682b1a8cbb7bc0946749adee2b1b1113ebd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Jan 2019 15:18:28 -0800 Subject: [PATCH 47/50] Fix various problems with reflow - No longer reflow lines where the full unwrapped line contains the cursor - Add guards to prevent y and ybase becoming invalid values Part of Microsoft/vscode#67364 Fixes #1910 --- src/Buffer.ts | 44 +++++++++++++++++++++++++++++--------------- src/BufferReflow.ts | 14 ++++++++++---- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 52d9572d..bc2d5f20 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; -import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; +import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList'; +import { EventEmitter } from './common/EventEmitter'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove, reflowLargerCreateNewLayout, reflowLargerApplyNewLayout } from './BufferReflow'; +import { BufferIndex, CharData, IBuffer, IBufferLine, IBufferStringIterator, IBufferStringIteratorResult, ITerminal } from './Types'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -212,7 +212,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; if (this._hasScrollback) { - this._reflow(newCols); + this._reflow(newCols, newRows); // Trim the end of the line off if cols shrunk if (this._cols > newCols) { @@ -226,7 +226,7 @@ export class Buffer implements IBuffer { this._rows = newRows; } - private _reflow(newCols: number): void { + private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; } @@ -235,12 +235,12 @@ export class Buffer implements IBuffer { if (newCols > this._cols) { this._reflowLarger(newCols); } else { - this._reflowSmaller(newCols); + this._reflowSmaller(newCols, newRows); } } private _reflowLarger(newCols: number): void { - const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); @@ -253,9 +253,13 @@ export class Buffer implements IBuffer { let viewportAdjustments = countRemoved; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + if (this.y > 0) { + this.y--; + } + if (this.lines.length < this._rows) { + // Add an extra row at the bottom of the viewport + this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + } } else { if (this.ydisp === this.ybase) { this.ydisp--; @@ -265,7 +269,7 @@ export class Buffer implements IBuffer { } } - private _reflowSmaller(newCols: number): void { + private _reflowSmaller(newCols: number, newRows: number): void { // Gather all BufferLines that need to be inserted into the Buffer here so that they can be // batched up and only committed once const toInsert = []; @@ -285,6 +289,13 @@ export class Buffer implements IBuffer { wrappedLines.unshift(nextLine); } + // If these lines contain the cursor don't touch them, the program will handle fixing up + // wrapped lines with the cursor + const absoluteY = this.ybase + this.y; + if (absoluteY >= y && absoluteY < y + wrappedLines.length) { + continue; + } + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); const linesToAdd = destLineLengths.length - wrappedLines.length; @@ -357,10 +368,13 @@ export class Buffer implements IBuffer { this.ydisp++; } } else { - if (this.ybase === this.ydisp) { - this.ydisp++; + // Ensure ybase does not exceed its maximum value + if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) { + if (this.ybase === this.ydisp) { + this.ydisp++; + } + this.ybase++; } - this.ybase++; } } } diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 59934e46..24ab69e6 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -3,10 +3,10 @@ * @license MIT */ +import { FILL_CHAR_DATA } from './Buffer'; import { BufferLine } from './BufferLine'; import { CircularList, IDeleteEvent } from './common/CircularList'; import { IBufferLine } from './Types'; -import { FILL_CHAR_DATA } from './Buffer'; export interface INewLayoutResult { layout: number[]; @@ -19,7 +19,7 @@ export interface INewLayoutResult { * @param lines The buffer lines. * @param newCols The columns after resize. */ -export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { +export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number, bufferAbsoluteY: number): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once const toRemove: number[] = []; @@ -39,6 +39,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n nextLine = lines.get(++i) as BufferLine; } + // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped + // lines with the cursor + if (bufferAbsoluteY >= y && bufferAbsoluteY < i) { + y += wrappedLines.length - 1; + continue; + } + // Copy buffer data to new locations let destLineIndex = 0; let destCol = wrappedLines[destLineIndex].getTrimmedLength(); @@ -64,7 +71,7 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n } // Make sure the last cell isn't wide, if it is copy it to the current dest - if (destCol === 0) { + if (destCol === 0 && destLineIndex !== 0) { if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); // Null out the end of the last row @@ -166,7 +173,6 @@ export function reflowLargerApplyNewLayout(lines: CircularList, new */ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { const newLineLengths: number[] = []; - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and From db31a44da3da0100ae26d0fb6a20eeecf61881f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 30 Jan 2019 22:14:47 -0800 Subject: [PATCH 48/50] Fix tests --- src/Buffer.test.ts | 66 +++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index a56fd63a..2a244472 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -265,6 +265,7 @@ describe('Buffer', () => { const char = String.fromCharCode(code); firstLine.set(i, [null, char, 1, code]); } + buffer.y = 1; assert.equal(buffer.lines.get(0).length, 5); assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); buffer.resize(1, 10); @@ -296,7 +297,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); terminal.options.scrollback = 1; buffer.resize(10, 5); - const lastLine = buffer.lines.get(4); + const lastLine = buffer.lines.get(3); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -308,27 +309,27 @@ describe('Buffer', () => { assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), ' '); - assert.equal(buffer.lines.get(1).translateToString(), 'ab'); - assert.equal(buffer.lines.get(2).translateToString(), 'cd'); - assert.equal(buffer.lines.get(3).translateToString(), 'ef'); - assert.equal(buffer.lines.get(4).translateToString(), 'gh'); - assert.equal(buffer.lines.get(5).translateToString(), 'ij'); + assert.equal(buffer.lines.get(0).translateToString(), 'ab'); + assert.equal(buffer.lines.get(1).translateToString(), 'cd'); + assert.equal(buffer.lines.get(2).translateToString(), 'ef'); + assert.equal(buffer.lines.get(3).translateToString(), 'gh'); + assert.equal(buffer.lines.get(4).translateToString(), 'ij'); + assert.equal(buffer.lines.get(5).translateToString(), ' '); buffer.resize(1, 5); assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), 'e'); - assert.equal(buffer.lines.get(1).translateToString(), 'f'); - assert.equal(buffer.lines.get(2).translateToString(), 'g'); - assert.equal(buffer.lines.get(3).translateToString(), 'h'); - assert.equal(buffer.lines.get(4).translateToString(), 'i'); - assert.equal(buffer.lines.get(5).translateToString(), 'j'); + assert.equal(buffer.lines.get(0).translateToString(), 'f'); + assert.equal(buffer.lines.get(1).translateToString(), 'g'); + assert.equal(buffer.lines.get(2).translateToString(), 'h'); + assert.equal(buffer.lines.get(3).translateToString(), 'i'); + assert.equal(buffer.lines.get(4).translateToString(), 'j'); + assert.equal(buffer.lines.get(5).translateToString(), ' '); buffer.resize(10, 5); - assert.equal(buffer.y, 0); + assert.equal(buffer.y, 1); assert.equal(buffer.ybase, 0); assert.equal(buffer.lines.length, 5); - assert.equal(buffer.lines.get(0).translateToString(), 'efghij '); + assert.equal(buffer.lines.get(0).translateToString(), 'fghij '); assert.equal(buffer.lines.get(1).translateToString(), ' '); assert.equal(buffer.lines.get(2).translateToString(), ' '); assert.equal(buffer.lines.get(3).translateToString(), ' '); @@ -339,6 +340,7 @@ describe('Buffer', () => { // 3+ lines removed on a reflow actually remove the right lines buffer.fillViewportRows(); buffer.resize(10, 10); + buffer.y = 2; const firstLine = buffer.lines.get(0); const secondLine = buffer.lines.get(1); for (let i = 0; i < 10; i++) { @@ -358,8 +360,8 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(i).translateToString(), ' '); } buffer.resize(2, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 11); assert.equal(buffer.lines.get(0).translateToString(), 'ab'); assert.equal(buffer.lines.get(1).translateToString(), 'cd'); assert.equal(buffer.lines.get(2).translateToString(), 'ef'); @@ -370,7 +372,10 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(7).translateToString(), '45'); assert.equal(buffer.lines.get(8).translateToString(), '67'); assert.equal(buffer.lines.get(9).translateToString(), '89'); + assert.equal(buffer.lines.get(10).translateToString(), ' '); buffer.resize(10, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); for (let i = 2; i < 10; i++) { @@ -379,22 +384,23 @@ describe('Buffer', () => { }); it('should transfer combined char data over to reflowed lines', () => { buffer.fillViewportRows(); - buffer.resize(4, 2); + buffer.resize(4, 3); + buffer.y = 2; const firstLine = buffer.lines.get(0); firstLine.set(0, [ null, 'a', 1, 'a'.charCodeAt(0) ]); firstLine.set(1, [ null, 'b', 1, 'b'.charCodeAt(0) ]); firstLine.set(2, [ null, 'c', 1, 'c'.charCodeAt(0) ]); firstLine.set(3, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - assert.equal(buffer.lines.length, 2); + assert.equal(buffer.lines.length, 3); assert.equal(buffer.lines.get(0).translateToString(), 'abc😁'); assert.equal(buffer.lines.get(1).translateToString(), ' '); - buffer.resize(2, 2); + buffer.resize(2, 3); assert.equal(buffer.lines.get(0).translateToString(), 'ab'); assert.equal(buffer.lines.get(1).translateToString(), 'c😁'); }); it('should adjust markers when reflowing', () => { buffer.fillViewportRows(); - buffer.resize(10, 15); + buffer.resize(10, 16); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -410,6 +416,7 @@ describe('Buffer', () => { const char = String.fromCharCode(code); buffer.lines.get(2).set(i, [null, char, 1, code]); } + buffer.y = 3; // Buffer: // abcdefghij // 0123456789 @@ -423,7 +430,7 @@ describe('Buffer', () => { assert.equal(firstMarker.line, 0); assert.equal(secondMarker.line, 1); assert.equal(thirdMarker.line, 2); - buffer.resize(2, 15); + buffer.resize(2, 16); assert.equal(buffer.lines.get(0).translateToString(), 'ab'); assert.equal(buffer.lines.get(1).translateToString(), 'cd'); assert.equal(buffer.lines.get(2).translateToString(), 'ef'); @@ -442,7 +449,7 @@ describe('Buffer', () => { assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped'); assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped'); - buffer.resize(10, 15); + buffer.resize(10, 16); assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); @@ -456,7 +463,7 @@ describe('Buffer', () => { it('should dispose markers whose rows are trimmed during a reflow', () => { buffer.fillViewportRows(); terminal.options.scrollback = 1; - buffer.resize(10, 10); + buffer.resize(10, 11); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -472,6 +479,7 @@ describe('Buffer', () => { const char = String.fromCharCode(code); buffer.lines.get(2).set(i, [null, char, 1, code]); } + buffer.y = 10; // Buffer: // abcdefghij // 0123456789 @@ -479,14 +487,14 @@ describe('Buffer', () => { const firstMarker = buffer.addMarker(0); const secondMarker = buffer.addMarker(1); const thirdMarker = buffer.addMarker(2); - buffer.y = 2; + buffer.y = 3; assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); assert.equal(firstMarker.line, 0); assert.equal(secondMarker.line, 1); assert.equal(thirdMarker.line, 2); - buffer.resize(2, 10); + buffer.resize(2, 11); assert.equal(buffer.lines.get(0).translateToString(), 'ij'); assert.equal(buffer.lines.get(1).translateToString(), '01'); assert.equal(buffer.lines.get(2).translateToString(), '23'); @@ -503,7 +511,7 @@ describe('Buffer', () => { assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); assert.equal(secondMarker.isDisposed, false); assert.equal(thirdMarker.isDisposed, false); - buffer.resize(10, 10); + buffer.resize(10, 11); assert.equal(buffer.lines.get(0).translateToString(), 'ij '); assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); @@ -513,6 +521,7 @@ describe('Buffer', () => { it('should wrap wide characters correctly when reflowing larger', () => { buffer.fillViewportRows(); buffer.resize(12, 10); + buffer.y = 2; for (let i = 0; i < 12; i += 4) { buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); @@ -547,6 +556,7 @@ describe('Buffer', () => { it('should wrap wide characters correctly when reflowing smaller', () => { buffer.fillViewportRows(); buffer.resize(12, 10); + buffer.y = 2; for (let i = 0; i < 12; i += 4) { buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); @@ -937,6 +947,7 @@ describe('Buffer', () => { describe('&& ydisp === ybase', () => { it('should trim lines and keep ydisp = ybase', () => { buffer.ydisp = 10; + buffer.y = 13; buffer.resize(2, 10); assert.equal(buffer.ydisp, 10); assert.equal(buffer.ybase, 10); @@ -962,6 +973,7 @@ describe('Buffer', () => { describe('&& ydisp !== ybase', () => { it('should trim lines and not change ydisp', () => { buffer.ydisp = 5; + buffer.y = 13; buffer.resize(2, 10); assert.equal(buffer.ydisp, 5); assert.equal(buffer.ybase, 10); From 7b7f85e3cf0976b157a64c727276ad3b9d807e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 31 Jan 2019 22:56:55 +0100 Subject: [PATCH 49/50] add test for correct tab handling, docs --- src/Buffer.test.ts | 7 +++++++ src/Buffer.ts | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 1ef2271f..53de19b7 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -1312,6 +1312,13 @@ describe('Buffer', () => { terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); } }); + + it('should handle \t in lines correctly', () => { + const input = '\thttps://google.de'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(s, Array(terminal.getOption('tabStopWidth') + 1).join(' ') + 'https://google.de'); + }); }); describe('BufferStringIterator', function(): void { it('iterator does not overflow buffer limits', function(): void { diff --git a/src/Buffer.ts b/src/Buffer.ts index 8eddf73f..7f4b6071 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -458,7 +458,9 @@ export class Buffer implements IBuffer { const length = (trimRight) ? line.getTrimmedLength() : line.length; for (let i = 0; i < length; ++i) { if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) { - stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; // WHITESPACE_CELL_CHAR.length + // empty cells report a string length of 0, but get replaced + // with a whitespace in translateToString, thus replace with 1 + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; } if (stringIndex < 0) { return [lineIndex, i]; From 828f14f2abd58f42497903e745fbfc0af02c34bd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 31 Jan 2019 14:00:39 -0800 Subject: [PATCH 50/50] Improve docs on Terminal.resize --- typings/xterm.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index dc9ebbae..d813bc5f 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -443,7 +443,9 @@ declare module 'xterm' { addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; /** - * Resizes the terminal. + * Resizes the terminal. It's best practice to debounce calls to resize, + * this will help ensure that the pty can respond to the resize event + * before another one occurs. * @param x The number of columns to resize to. * @param y The number of rows to resize to. */