From 8a50729a98b3c9ea4daf249e35d96ab709e6b859 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 21:58:54 -0800 Subject: [PATCH 01/33] 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 74750a8b4e..6b247afee0 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 3f93af626a..aafc9051c4 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/33] Mostly working for reflowing to smaller --- src/Buffer.ts | 215 ++++++++++++++++++++++++++++++++++------------ src/BufferLine.ts | 17 ++-- 2 files changed, 171 insertions(+), 61 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 6b247afee0..fc8e008e60 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -235,81 +235,185 @@ 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; + if (newCols > this._terminal.cols) { + 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; + // 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; } + } - 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; - } - } + // 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; + } + } - // 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 - // 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--; - } - } + 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; } /** @@ -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 aafc9051c4..ea44de8770 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/33] 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 fc8e008e60..961032d9d9 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 ea44de8770..fd28af846a 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/33] 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 961032d9d9..5c1d8e458e 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 8ebb28d3e3..b1fea90357 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/33] 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 5c1d8e458e..dcb0504908 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/33] 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 dcb0504908..ef91055d66 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/33] 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 ef91055d66..acba0c3297 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/33] 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 acba0c3297..143e2b2bf1 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 fbf8b0517b..c652ff4e77 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 10033a3355..3b59ee5d69 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/33] 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 143e2b2bf1..41a76ccc48 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 b1fea90357..d7715f8199 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/33] 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 0546dfe809..189b3ad674 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 41a76ccc48..5f6791d8f2 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/33] 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 5f6791d8f2..0b660321df 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/33] 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 189b3ad674..cee0fbad43 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 0b660321df..feb3a1ee38 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 d7715f8199..d48362b8ef 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/33] 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 cee0fbad43..bece8c226a 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 feb3a1ee38..ba3353093d 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/33] 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 | 223 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 150 insertions(+), 73 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index ba3353093d..acb462ab2d 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); - - // 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; + // 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; } - 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++; + if (this.ybase === this.ydisp) { + this.ybase++; + this.ydisp++; + } } - } else { - if (this.ybase === this.ydisp) { - this.ybase++; - this.ydisp++; + } + + // y -= 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 + } - return wrappedLines.length - 1 - linesToAdd + trimmedLines; + // 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/33] 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 acb462ab2d..1dfc3ef63b 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/33] Move loop into reflowLarger (adjust indent) --- src/Buffer.ts | 120 +++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 1dfc3ef63b..65e42de056 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; - } + 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; + } - // 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; + // 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; } - srcCol += cellsToCopy; - if (srcCol === this._cols) { - srcLineIndex++; - srcCol = 0; + + // 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; + } } - } - // Clear out remaining cells or fragments could remain; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, 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; + // 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--; + 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--; } - this.ybase--; } } - } - return wrappedLines.length - countToRemove - 1; + 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/33] 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 65e42de056..fc74f679b7 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/33] 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 bece8c226a..faed180833 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 fc74f679b7..27662708df 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/33] 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 faed180833..4c348ec654 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 fd28af846a..619fa9caea 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/33] 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 c652ff4e77..f5e95fc857 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 619fa9caea..1c39147184 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/33] 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 4c348ec654..7abf6f726d 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 27662708df..5b9af7428d 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 9faf534aef..af4d8af507 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 f9c0c0014e..c6b09901bb 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/33] 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 7abf6f726d..81d4484ba2 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 5b9af7428d..861471b5bd 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/33] 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 3b59ee5d69..10033a3355 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 6581eb7d88015ac5b499d576436133d37ca51ae3 Mon Sep 17 00:00:00 2001 From: jerch Date: Sat, 5 Jan 2019 01:47:44 +0100 Subject: [PATCH 24/33] 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 74cec10769..987e03247a 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 7fe3f0a4e3e094ed82fec0b17219cc0187a3ed69 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 11 Jan 2019 12:58:41 -0800 Subject: [PATCH 25/33] 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 f5e95fc857..0ef29505e3 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 4843ca5bb879ed5e6116cba89d27e6c9c813ba40 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 21 Jan 2019 09:08:18 -0800 Subject: [PATCH 26/33] 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 81d4484ba2..040eb3ca79 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 987e03247a..72e43aa0d3 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 2fa4ef139d..6bc305860c 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 27/33] 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 040eb3ca79..8e1a2f601f 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 72e43aa0d3..3094132415 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 0000000000..a788fbc800 --- /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 0000000000..f56d48c571 --- /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 28/33] 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 8e1a2f601f..82cdff6b36 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 3094132415..5d3c1a521f 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 a788fbc800..9c978dc0ab 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 f56d48c571..f99acd1ca7 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 29/33] 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 82cdff6b36..1ef2e92c66 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 5d3c1a521f..f534087d57 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 f99acd1ca7..5e815c7aab 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 4c0cd0f8a4..8a5d96ba27 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 30/33] 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 f534087d57..2f1c2e491c 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 5e815c7aab..310443b794 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 31/33] 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 2f1c2e491c..286d1311ec 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 310443b794..51f83c1865 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 32/33] 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 286d1311ec..52d9572d5c 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 51f83c1865..003a3c7804 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 33/33] jsdoc --- src/BufferReflow.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 003a3c7804..59934e46cb 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[] = [];