diff --git a/demo/client.ts b/demo/client.ts index 4de78999..d5196d37 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -207,7 +207,8 @@ function initOptions(term: TerminalType): void { fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - rendererType: ['dom', 'canvas'] + rendererType: ['dom', 'canvas'], + experimentalBufferLineImpl: ['JsArray', 'TypedArray'] }; const options = Object.keys((term)._core.options); const booleanOptions = []; diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 0dcdd02d..db8a460d 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -37,7 +37,7 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = BufferLine.blankLine(terminal.cols, DEFAULT_ATTR).get(0); + const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).get(0); buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); for (let y = 0; y < INIT_ROWS; y++) { @@ -155,8 +155,12 @@ describe('Buffer', () => { assert.equal(buffer.lines.maxLength, INIT_ROWS); buffer.y = INIT_ROWS - 1; buffer.fillViewportRows(); - buffer.lines.get(5).get(0)[1] = 'a'; - buffer.lines.get(INIT_ROWS - 1).get(0)[1] = 'b'; + let chData = buffer.lines.get(5).get(0); + chData[1] = 'a'; + buffer.lines.get(5).set(0, chData); + chData = buffer.lines.get(INIT_ROWS - 1).get(0); + chData[1] = 'b'; + buffer.lines.get(INIT_ROWS - 1).set(0, chData); buffer.resize(INIT_COLS, INIT_ROWS - 5); assert.equal(buffer.lines.get(0).get(0)[1], 'a'); assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b'); @@ -180,7 +184,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR)); + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -200,7 +204,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR)); + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -273,11 +277,11 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { - const line = new BufferLine(); - line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); - line.push([ null, 'b', 1, 'b'.charCodeAt(0)]); - line.push([ null, 'c', 1, 'c'.charCodeAt(0)]); - line.push([ null, 'd', 1, 'd'.charCodeAt(0)]); + const line = new BufferLine(4); + line.set(0, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line.set(1, [ null, 'b', 1, 'b'.charCodeAt(0)]); + line.set(2, [ null, 'c', 1, 'c'.charCodeAt(0)]); + line.set(3, [ null, 'd', 1, 'd'.charCodeAt(0)]); buffer.lines.set(0, line); const str = buffer.translateBufferLineToString(0, true, 0, 2); @@ -285,10 +289,10 @@ describe('Buffer', () => { }); it('should handle a cut-off double width character by including it', () => { - const line = new BufferLine(); - line.push([ null, '語', 2, 35486 ]); - line.push([ null, '', 0, null]); - line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); + const line = new BufferLine(3); + line.set(0, [ null, '語', 2, 35486 ]); + line.set(1, [ null, '', 0, null]); + line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -296,10 +300,10 @@ describe('Buffer', () => { }); it('should handle a zero width character in the middle of the string by not including it', () => { - const line = new BufferLine(); - line.push([ null, '語', 2, '語'.charCodeAt(0) ]); - line.push([ null, '', 0, null]); - line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); + const line = new BufferLine(3); + line.set(0, [ null, '語', 2, '語'.charCodeAt(0) ]); + line.set(1, [ null, '', 0, null]); + line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); buffer.lines.set(0, line); const str0 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -313,9 +317,9 @@ describe('Buffer', () => { }); it('should handle single width emojis', () => { - const line = new BufferLine(); - line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]); - line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); + const line = new BufferLine(2); + line.set(0, [ null, '😁', 1, '😁'.charCodeAt(0) ]); + line.set(1, [ null, 'a', 1, 'a'.charCodeAt(0)]); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -326,9 +330,9 @@ describe('Buffer', () => { }); it('should handle double width emojis', () => { - const line = new BufferLine(); - line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]); - line.push([ null, '', 0, null]); + const line = new BufferLine(2); + line.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]); + line.set(1, [ null, '', 0, null]); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -337,10 +341,10 @@ describe('Buffer', () => { const str2 = buffer.translateBufferLineToString(0, true, 0, 2); assert.equal(str2, '😁'); - const line2 = new BufferLine(); - line2.push([ null, '😁', 2, '😁'.charCodeAt(0) ]); - line2.push([ null, '', 0, null]); - line2.push([ null, 'a', 1, 'a'.charCodeAt(0)]); + const line2 = new BufferLine(3); + line2.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]); + line2.set(1, [ null, '', 0, null]); + line2.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); buffer.lines.set(0, line2); const str3 = buffer.translateBufferLineToString(0, true, 0, 3); diff --git a/src/Buffer.ts b/src/Buffer.ts index be5a47f1..e54f752b 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,10 +4,10 @@ */ import { CircularList } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; +import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types'; import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine } from './BufferLine'; +import { BufferLine, BufferLineTypedArray } from './BufferLine'; export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -39,6 +39,7 @@ export class Buffer implements IBuffer { public savedY: number; public savedX: number; public markers: Marker[] = []; + private _bufferLineConstructor: IBufferLineConstructor; /** * Create a new Buffer. @@ -53,6 +54,37 @@ export class Buffer implements IBuffer { this.clear(); } + public setBufferLineFactory(type: string): void { + if (type === 'TypedArray') { + if (this._bufferLineConstructor !== BufferLineTypedArray) { + this._bufferLineConstructor = BufferLineTypedArray; + this._recreateLines(); + } + } else { + if (this._bufferLineConstructor !== BufferLine) { + this._bufferLineConstructor = BufferLine; + this._recreateLines(); + } + } + } + + private _recreateLines(): void { + if (!this.lines) return; + for (let i = 0; i < this.lines.length; ++i) { + const oldLine = this.lines.get(i); + const newLine = new this._bufferLineConstructor(oldLine.length); + for (let j = 0; j < oldLine.length; ++j) { + newLine.set(j, oldLine.get(j)); + } + this.lines.set(i, newLine); + } + } + + 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); + } + public get hasScrollback(): boolean { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } @@ -85,7 +117,7 @@ export class Buffer implements IBuffer { if (this.lines.length === 0) { let i = this._terminal.rows; while (i--) { - this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); + this.lines.push(this.getBlankLine(DEFAULT_ATTR)); } } } @@ -94,6 +126,7 @@ export class Buffer implements IBuffer { * Clears the buffer to it's initial state, discarding all previous data. */ public clear(): void { + this.setBufferLineFactory(this._terminal.options.experimentalBufferLineImpl); this.ydisp = 0; this.ybase = 0; this.y = 0; @@ -124,9 +157,7 @@ export class Buffer implements IBuffer { 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++) { - while (this.lines.get(i).length < newCols) { - this.lines.get(i).push(ch); - } + this.lines.get(i).resize(newCols, ch); } } @@ -147,7 +178,8 @@ 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 - this.lines.push(BufferLine.blankLine(newCols, DEFAULT_ATTR)); + const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + this.lines.push(new this._bufferLineConstructor(newCols, fillCharData)); } } } diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 61dfe543..a1a8e0ff 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -5,93 +5,132 @@ import * as chai from 'chai'; import { BufferLine } from './BufferLine'; import { CharData, IBufferLine } from './Types'; -import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; class TestBufferLine extends BufferLine { public toArray(): CharData[] { - return this._data; + const result = []; + for (let i = 0; i < this.length; ++i) { + result.push(this.get(i)); + } + return result; } } describe('BufferLine', function(): void { it('ctor', function(): void { - let line: IBufferLine = new TestBufferLine(); + let line: IBufferLine = new TestBufferLine(0); chai.expect(line.length).equals(0); - chai.expect(line.pop()).equals(undefined); chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10, null, true); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(true); - line = new TestBufferLine(10, [123, 'a', 456, 789], true); + line = new TestBufferLine(10, [123, 'a', 456, 'a'.charCodeAt(0)], true); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql([123, 'a', 456, 789]); + chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]); chai.expect(line.isWrapped).equals(true); }); - it('splice', function(): void { - const line = new TestBufferLine(); - const data: CharData[] = [ - [1, 'a', 0, 0], - [2, 'b', 0, 0], - [3, 'c', 0, 0] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); - chai.expect(line.length).equals(data.length); - const removed1 = line.splice(1, 1, [4, 'd', 0, 0]); - const removed2 = data.splice(1, 1, [4, 'd', 0, 0]); - chai.expect(removed1).eql(removed2); - chai.expect(line.toArray()).eql(data); - }); - it('TerminalLine.blankLine', function(): void { - const line = TestBufferLine.blankLine(5, 123); - chai.expect(line.length).equals(5); - chai.expect(line.isWrapped).equals(false); - const ch = line.get(0); - chai.expect(ch[CHAR_DATA_ATTR_INDEX]).equals(123); - chai.expect(ch[CHAR_DATA_CHAR_INDEX]).equals(NULL_CELL_CHAR); - chai.expect(ch[CHAR_DATA_WIDTH_INDEX]).equals(NULL_CELL_WIDTH); - chai.expect(ch[CHAR_DATA_CODE_INDEX]).equals(NULL_CELL_CODE); - }); it('insertCells', function(): void { - const line = new TestBufferLine(); - const data: CharData[] = [ - [1, 'a', 0, 0], - [2, 'b', 0, 0], - [3, 'c', 0, 0] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); - line.insertCells(1, 3, [4, 'd', 0, 0]); - chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [4, 'd', 0, 0]]); + const line = new TestBufferLine(3); + line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); + line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); + line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); + line.insertCells(1, 3, [4, 'd', 0, 'd'.charCodeAt(0)]); + chai.expect(line.toArray()).eql([ + [1, 'a', 0, 'a'.charCodeAt(0)], + [4, 'd', 0, 'd'.charCodeAt(0)], + [4, 'd', 0, 'd'.charCodeAt(0)] + ]); }); it('deleteCells', function(): void { - const line = new TestBufferLine(); - const data: CharData[] = [ - [1, 'a', 0, 0], - [2, 'b', 0, 0], - [3, 'c', 0, 0], - [4, 'd', 0, 0], - [5, 'e', 0, 0] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); - line.deleteCells(1, 2, [6, 'f', 0, 0]); - chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [5, 'e', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0]]); + const line = new TestBufferLine(5); + line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); + line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); + line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); + line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); + line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.deleteCells(1, 2, [6, 'f', 0, 'f'.charCodeAt(0)]); + chai.expect(line.toArray()).eql([ + [1, 'a', 0, 'a'.charCodeAt(0)], + [4, 'd', 0, 'd'.charCodeAt(0)], + [5, 'e', 0, 'e'.charCodeAt(0)], + [6, 'f', 0, 'f'.charCodeAt(0)], + [6, 'f', 0, 'f'.charCodeAt(0)] + ]); }); it('replaceCells', function(): void { - const line = new TestBufferLine(); - const data: CharData[] = [ - [1, 'a', 0, 0], - [2, 'b', 0, 0], - [3, 'c', 0, 0], - [4, 'd', 0, 0], - [5, 'e', 0, 0] - ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); - line.replaceCells(2, 4, [6, 'f', 0, 0]); - chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [2, 'b', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0], [5, 'e', 0, 0]]); + const line = new TestBufferLine(5); + line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); + line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); + line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); + line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); + line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.replaceCells(2, 4, [6, 'f', 0, 'f'.charCodeAt(0)]); + chai.expect(line.toArray()).eql([ + [1, 'a', 0, 'a'.charCodeAt(0)], + [2, 'b', 0, 'b'.charCodeAt(0)], + [6, 'f', 0, 'f'.charCodeAt(0)], + [6, 'f', 0, 'f'.charCodeAt(0)], + [5, 'e', 0, 'e'.charCodeAt(0)] + ]); + }); + it('fill', function(): void { + const line = new TestBufferLine(5); + line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); + line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); + line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); + line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); + line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.fill([123, 'z', 0, 'z'.charCodeAt(0)]); + chai.expect(line.toArray()).eql([ + [123, 'z', 0, 'z'.charCodeAt(0)], + [123, 'z', 0, 'z'.charCodeAt(0)], + [123, 'z', 0, 'z'.charCodeAt(0)], + [123, 'z', 0, 'z'.charCodeAt(0)], + [123, 'z', 0, 'z'.charCodeAt(0)] + ]); + }); + it('clone', function(): void { + const line = new TestBufferLine(5, null, true); + line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); + line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); + line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); + line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); + line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + const line2 = line.clone(); + chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray()); + chai.expect(line2.length).equals(line.length); + chai.expect(line2.isWrapped).equals(line.isWrapped); + }); + it('copyFrom', function(): void { + const line = new TestBufferLine(5); + line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); + line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); + line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); + line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); + line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + const line2 = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], true); + line2.copyFrom(line); + chai.expect(line2.toArray()).eql(line.toArray()); + chai.expect(line2.length).equals(line.length); + chai.expect(line2.isWrapped).equals(line.isWrapped); + }); + it('should support combining chars', function(): void { + // CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print + // --> set code to the last charCodeAt value of the string + // Note: needs to be fixed once the string pointer is in place + const line = new TestBufferLine(2, [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]); + chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); + const line2 = new TestBufferLine(5, [1, 'a', 0, '\u0301'.charCodeAt(0)], true); + line2.copyFrom(line); + chai.expect(line2.toArray()).eql(line.toArray()); + const line3 = line.clone(); + chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray()); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 639049e2..4fdceee1 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -9,28 +9,39 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; * Class representing a terminal line. */ export class BufferLine implements IBufferLine { - static blankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine { - const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - return new BufferLine(cols, ch, isWrapped); - } protected _data: CharData[]; public isWrapped = false; public length: number; - constructor(cols?: number, ch?: CharData, isWrapped?: boolean) { + constructor(cols: number, fillCharData?: CharData, isWrapped?: boolean) { this._data = []; - this.length = this._data.length; - if (cols) { - if (!ch) { - ch = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } - for (let i = 0; i < cols; i++) { - this.push(ch); // Note: the ctor ch is not cloned (resembles old behavior) - } + if (!fillCharData) { + fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + } + for (let i = 0; i < cols; i++) { + this._push(fillCharData); // Note: the ctor ch is not cloned (resembles old behavior) } if (isWrapped) { this.isWrapped = true; } + this.length = this._data.length; + } + + private _pop(): CharData | undefined { + const data = this._data.pop(); + this.length = this._data.length; + return data; + } + + private _push(data: CharData): void { + this._data.push(data); + this.length = this._data.length; + } + + private _splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { + const removed = this._data.splice(start, deleteCount, ...items); + this.length = this._data.length; + return removed; } public get(index: number): CharData { @@ -41,43 +52,225 @@ export class BufferLine implements IBufferLine { this._data[index] = data; } - public pop(): CharData | undefined { - const data = this._data.pop(); - this.length = this._data.length; - return data; - } - - public push(data: CharData): void { - this._data.push(data); - this.length = this._data.length; - } - - public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { - const removed = this._data.splice(start, deleteCount, ...items); - this.length = this._data.length; - return removed; - } - /** insert n cells ch at pos, right cells are lost (stable length) */ public insertCells(pos: number, n: number, ch: CharData): void { while (n--) { - this.splice(pos, 0, ch); - this.pop(); + this._splice(pos, 0, ch); + this._pop(); } } /** delete n cells at pos, right side is filled with fill (stable length) */ - public deleteCells(pos: number, n: number, fill: CharData): void { + public deleteCells(pos: number, n: number, fillCharData: CharData): void { while (n--) { - this.splice(pos, 1); - this.push(fill); + this._splice(pos, 1); + this._push(fillCharData); } } /** replace cells from pos to pos + n - 1 with fill */ - public replaceCells(start: number, end: number, fill: CharData): void { + public replaceCells(start: number, end: number, fillCharData: CharData): void { while (start < end && start < this.length) { - this.set(start++, fill); // Note: fill is not cloned (resembles old behavior) + this.set(start++, fillCharData); // Note: fill is not cloned (resembles old behavior) } } + + /** resize line to cols filling new cells with fill */ + public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { + if (shrink) { + while (this._data.length > cols) { + this._data.pop(); + } + } + while (this._data.length < cols) { + this._data.push(fillCharData); + } + this.length = cols; + } + + public fill(fillCharData: CharData): void { + for (let i = 0; i < this.length; ++i) { + this.set(i, fillCharData); + } + } + + public copyFrom(line: IBufferLine): void { + this._data = []; + for (let i = 0; i < line.length; ++i) { + this._push(line.get(i)); + } + this.length = line.length; + this.isWrapped = line.isWrapped; + } + + public clone(): IBufferLine { + const newLine = new BufferLine(0); + newLine.copyFrom(this); + return newLine; + } +} + +/** typed array slots taken by one cell */ +const CELL_SIZE = 3; + +/** cell member indices */ +const enum Cell { + FLAGS = 0, + STRING = 1, + WIDTH = 2 +} + +/** + * Typed array based bufferline implementation. + * Note: Unlike the JS variant the access to the data + * via set/get is always a copy action. + * Sloppy ref style coding will not work anymore: + * line = new BufferLine(10); + * char = line.get(0); // char is a copy + * char[some_index] = 123; // will not update the line + * line.set(0, ch); // do this to update line data + * TODO: + * - provide getData/setData to directly access the data + */ +export class BufferLineTypedArray implements IBufferLine { + protected _data: Uint32Array | null = null; + protected _combined: {[index: number]: string} = {}; + public length: number; + + constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) { + if (!fillCharData) { + fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + } + this._data = new Uint32Array(cols * CELL_SIZE); + for (let i = 0; i < cols; ++i) { + this.set(i, fillCharData); + } + this.length = cols || 0; + } + + public get(index: number): CharData { + const stringData = this._data[index * CELL_SIZE + Cell.STRING]; + return [ + this._data[index * CELL_SIZE + Cell.FLAGS], + (stringData & 0x80000000) + ? this._combined[index] + : (stringData) ? String.fromCharCode(stringData) : '', + this._data[index * CELL_SIZE + Cell.WIDTH], + (stringData & 0x80000000) + ? this._combined[index].charCodeAt(this._combined[index].length - 1) + : stringData + ]; + } + + public set(index: number, value: CharData): void { + this._data[index * CELL_SIZE + Cell.FLAGS] = value[0]; + if (value[1].length > 1) { + this._combined[index] = value[1]; + this._data[index * CELL_SIZE + Cell.STRING] = index | 0x80000000; + } else { + this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0); + } + this._data[index * CELL_SIZE + Cell.WIDTH] = value[2]; + } + + public insertCells(pos: number, n: number, fillCharData: CharData): void { + pos %= this.length; + if (n < this.length - pos) { + for (let i = this.length - pos - n - 1; i >= 0; --i) { + this.set(pos + n + i, this.get(pos + i)); + } + for (let i = 0; i < n; ++i) { + this.set(pos + i, fillCharData); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.set(i, fillCharData); + } + } + } + + public deleteCells(pos: number, n: number, fillCharData: CharData): void { + pos %= this.length; + if (n < this.length - pos) { + for (let i = 0; i < this.length - pos - n; ++i) { + this.set(pos + i, this.get(pos + n + i)); + } + for (let i = this.length - n; i < this.length; ++i) { + this.set(i, fillCharData); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.set(i, fillCharData); + } + } + } + + public replaceCells(start: number, end: number, fillCharData: CharData): void { + while (start < end && start < this.length) { + this.set(start++, fillCharData); + } + } + + public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { + if (cols === this.length) { + return; + } + if (cols > this.length) { + const data = new Uint32Array(cols * CELL_SIZE); + if (this._data) { + data.set(this._data); + } + this._data = data; + for (let i = this.length; i < cols; ++i) { + this.set(i, fillCharData); + } + } else if (shrink) { + if (cols) { + const data = new Uint32Array(cols * CELL_SIZE); + data.set(this._data.subarray(0, cols * CELL_SIZE)); + this._data = data; + } else { + this._data = null; + } + } + this.length = cols; + } + + /** fill a line with fillCharData */ + public fill(fillCharData: CharData): void { + this._combined = {}; + for (let i = 0; i < this.length; ++i) { + this.set(i, fillCharData); + } + } + + /** alter to a full copy of line */ + public copyFrom(line: BufferLineTypedArray): void { + if (this.length !== line.length) { + this._data = new Uint32Array(line._data); + } else { + // use high speed copy if lengths are equal + this._data.set(line._data); + } + this.length = line.length; + this._combined = {}; + for (const el in line._combined) { + this._combined[el] = line._combined[el]; + } + this.isWrapped = line.isWrapped; + } + + /** create a new clone */ + public clone(): IBufferLine { + const newLine = new BufferLineTypedArray(0); + // creation of new typed array from another is actually pretty slow :( + // still faster than copying values one by one + newLine._data = new Uint32Array(this._data); + newLine.length = this.length; + for (const el in this._combined) { + newLine._combined[el] = this._combined[el]; + } + newLine.isWrapped = this.isWrapped; + return newLine; + } } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 722cc287..bdd9ccc6 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -98,6 +98,36 @@ class OldInputHandler extends InputHandler { public eraseLine(y: number): void { this.eraseRight(0, y); } + + public insertChars(params: number[]): void { + let param = params[0]; + if (param < 1) param = 1; + + // make buffer local for faster access + const buffer = this._terminal.buffer; + + const row = buffer.y + buffer.ybase; + let j = buffer.x; + while (param-- && j < this._terminal.cols) { + buffer.lines.get(row).insertCells(j++, 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + } + } + + public deleteChars(params: number[]): void { + let param: number = params[0]; + if (param < 1) { + param = 1; + } + + // make buffer local for faster access + const buffer = this._terminal.buffer; + + const row = buffer.y + buffer.ybase; + while (param--) { + buffer.lines.get(row).deleteCells(buffer.x, 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + } + this._terminal.updateRange(buffer.y); + } } describe('InputHandler', () => { @@ -178,8 +208,6 @@ describe('InputHandler', () => { }); }); describe('regression tests', function(): void { - type CharData = [number, string, number, number]; - function lineContent(line: IBufferLine): string { let content = ''; for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX]; @@ -195,23 +223,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); const inputHandler = new InputHandler(term); - - // old variant of the method - function insertChars(params: number[]): void { - let param = params[0]; - if (param < 1) param = 1; - - // make buffer local for faster access - const buffer = term.buffer; - - const row = buffer.y + buffer.ybase; - let j = buffer.x; - const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm - while (param-- && j < term.cols) { - buffer.lines.get(row).splice(j++, 0, ch); - buffer.lines.get(row).pop(); - } - } + const oldInputHandler = new OldInputHandler(term); // insert some data in first and second line inputHandler.parse(Array(term.cols - 9).join('a')); @@ -226,7 +238,7 @@ describe('InputHandler', () => { // insert one char from params = [0] term.buffer.y = 0; term.buffer.x = 70; - insertChars([0]); + oldInputHandler.insertChars([0]); expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789'); term.buffer.y = 1; term.buffer.x = 70; @@ -237,7 +249,7 @@ describe('InputHandler', () => { // insert one char from params = [1] term.buffer.y = 0; term.buffer.x = 70; - insertChars([1]); + oldInputHandler.insertChars([1]); expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678'); term.buffer.y = 1; term.buffer.x = 70; @@ -248,7 +260,7 @@ describe('InputHandler', () => { // insert two chars from params = [2] term.buffer.y = 0; term.buffer.x = 70; - insertChars([2]); + oldInputHandler.insertChars([2]); expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456'); term.buffer.y = 1; term.buffer.x = 70; @@ -259,7 +271,7 @@ describe('InputHandler', () => { // insert 10 chars from params = [10] term.buffer.y = 0; term.buffer.x = 70; - insertChars([10]); + oldInputHandler.insertChars([10]); expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' '); term.buffer.y = 1; term.buffer.x = 70; @@ -270,25 +282,7 @@ describe('InputHandler', () => { it('deleteChars', function(): void { const term = new Terminal(); const inputHandler = new InputHandler(term); - - // old variant of the method - function deleteChars(params: number[]): void { - let param: number = params[0]; - if (param < 1) { - param = 1; - } - - // make buffer local for faster access - const buffer = term.buffer; - - const row = buffer.y + buffer.ybase; - const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm - while (param--) { - buffer.lines.get(row).splice(buffer.x, 1); - buffer.lines.get(row).push(ch); - } - term.updateRange(buffer.y); - } + const oldInputHandler = new OldInputHandler(term); // insert some data in first and second line inputHandler.parse(Array(term.cols - 9).join('a')); @@ -303,7 +297,7 @@ describe('InputHandler', () => { // delete one char from params = [0] term.buffer.y = 0; term.buffer.x = 70; - deleteChars([0]); + oldInputHandler.deleteChars([0]); expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 '); term.buffer.y = 1; term.buffer.x = 70; @@ -314,7 +308,7 @@ describe('InputHandler', () => { // insert one char from params = [1] term.buffer.y = 0; term.buffer.x = 70; - deleteChars([1]); + oldInputHandler.deleteChars([1]); expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 '); term.buffer.y = 1; term.buffer.x = 70; @@ -325,7 +319,7 @@ describe('InputHandler', () => { // insert two chars from params = [2] term.buffer.y = 0; term.buffer.x = 70; - deleteChars([2]); + oldInputHandler.deleteChars([2]); expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 '); term.buffer.y = 1; term.buffer.x = 70; @@ -336,7 +330,7 @@ describe('InputHandler', () => { // insert 10 chars from params = [10] term.buffer.y = 0; term.buffer.x = 70; - deleteChars([10]); + oldInputHandler.deleteChars([10]); expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' '); term.buffer.y = 1; term.buffer.x = 70; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index ad5aad39..400e9b93 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -13,7 +13,6 @@ import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { Disposable } from './common/Lifecycle'; -import { BufferLine } from './BufferLine'; /** * Map collect to glevel. Used in `selectCharset`. @@ -392,10 +391,12 @@ export class InputHandler extends Disposable implements IInputHandler { if (chMinusTwo) { chMinusTwo[CHAR_DATA_CHAR_INDEX] += char; chMinusTwo[CHAR_DATA_CODE_INDEX] = code; + bufferRow.set(buffer.x - 2, chMinusTwo); // must be set explicitly now } } else { chMinusOne[CHAR_DATA_CHAR_INDEX] += char; chMinusOne[CHAR_DATA_CODE_INDEX] = code; + bufferRow.set(buffer.x - 1, chMinusOne); // must be set explicitly now } } continue; @@ -403,6 +404,9 @@ export class InputHandler extends Disposable implements IInputHandler { // goto next line if ch would overflow // TODO: needs a global min terminal width of 2 + // FIXME: additionally ensure chWidth fits into a line + // --> maybe forbid cols= cols) { // autowrap - DECAWM // automatically wraps to the beginning of the next line @@ -430,23 +434,15 @@ export class InputHandler extends Disposable implements IInputHandler { } // insert mode: move characters to right - // To achieve insert, we remove cells from the right - // and insert empty ones at cursor position if (insertMode) { - // do this twice for a fullwidth char - for (let moves = 0; moves < chWidth; ++moves) { - // remove last cell - // if it's width is 0, we have to adjust the second last cell as well - const removed = bufferRow.pop(); - const chMinusTwo = bufferRow.get(buffer.x - 2); - if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && chMinusTwo - && chMinusTwo[CHAR_DATA_WIDTH_INDEX] === 2) { - bufferRow.set(this._terminal.cols - 2, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); - } - - // insert empty cell at cursor - bufferRow.splice(buffer.x, 0, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + // right shift cells according to the width + bufferRow.insertCells(buffer.x, chWidth, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + // test last cell - since the last cell has only room for + // a halfwidth char any fullwidth shifted there is lost + // and will be set to eraseChar + const lastCell = bufferRow.get(cols - 1); + if (lastCell[CHAR_DATA_WIDTH_INDEX] === 2) { + bufferRow.set(cols - 1, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); } } @@ -454,7 +450,9 @@ export class InputHandler extends Disposable implements IInputHandler { bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]); // fullwidth char - also set next cell to placeholder stub and advance cursor - if (chWidth === 2) { + // for graphemes bigger than fullwidth we can simply loop to zero + // we already made sure above, that buffer.x + chWidth will not overflow right + while (--chWidth) { bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]); } } @@ -832,7 +830,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior buffer.lines.splice(scrollBottomAbsolute - 1, 1); - buffer.lines.splice(row, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); + buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr())); } // this.maxRange(); @@ -862,7 +860,7 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior buffer.lines.splice(row, 1); - buffer.lines.splice(j, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); + buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr())); } // this.maxRange(); @@ -894,7 +892,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -913,7 +911,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 1e4c0cdc..22e797aa 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -51,9 +51,9 @@ describe('Linkifier', () => { }); function stringToRow(text: string): IBufferLine { - const result = new BufferLine(); + const result = new BufferLine(text.length); for (let i = 0; i < text.length; i++) { - result.push([0, text.charAt(i), 1, text.charCodeAt(i)]); + result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); } return result; } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 9359793d..c42735d5 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -54,16 +54,16 @@ describe('SelectionManager', () => { }); function stringToRow(text: string): IBufferLine { - const result = new BufferLine(); + const result = new BufferLine(text.length); for (let i = 0; i < text.length; i++) { - result.push([0, text.charAt(i), 1, text.charCodeAt(i)]); + result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); } return result; } function stringArrayToRow(chars: string[]): IBufferLine { - const line = new BufferLine(); - chars.map(c => line.push([0, c, 1, c.charCodeAt(0)])); + const line = new BufferLine(chars.length); + chars.map((c, idx) => line.set(idx, [0, c, 1, c.charCodeAt(0)])); return line; } @@ -100,7 +100,6 @@ describe('SelectionManager', () => { }); it('should expand selection for wide characters', () => { // Wide characters use a special format - const line = new BufferLine(); const data: [number, string, number, number][] = [ [null, '中', 2, '中'.charCodeAt(0)], [null, '', 0, null], @@ -118,7 +117,8 @@ describe('SelectionManager', () => { [null, 'o', 1, 'o'.charCodeAt(0)], [null, 'o', 1, 'o'.charCodeAt(0)] ]; - for (let i = 0; i < data.length; ++i) line.push(data[i]); + const line = new BufferLine(data.length); + for (let i = 0; i < data.length; ++i) line.set(i, data[i]); buffer.lines.set(0, line); // Ensure wide characters take up 2 columns selectionManager.selectWordAt([0, 0]); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index e8a5d2d8..fd59144c 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -7,7 +7,6 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer'; -import { BufferLine } from './BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -260,7 +259,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); } }); it('should clear a buffer larger than rows', () => { @@ -277,7 +276,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); } }); it('should not break the prompt when cleared twice', () => { @@ -290,7 +289,7 @@ describe('term.js addons', () => { assert.equal(term.buffer.lines.length, term.rows); assert.deepEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); + assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); } }); }); @@ -456,8 +455,8 @@ describe('term.js addons', () => { describe('scroll() function', () => { describe('when scrollback > 0', () => { it('should create a new line and scroll', () => { - term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b'; + term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); + term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); @@ -467,9 +466,9 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); + term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(); @@ -479,11 +478,11 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c'; - term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd'; - term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); + term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); + term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); term.buffer.y = 3; term.buffer.scrollBottom = 3; term.scroll(); @@ -497,11 +496,11 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c'; - term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd'; - term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); + term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); + term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; @@ -522,9 +521,9 @@ describe('term.js addons', () => { }); it('should create a new line and shift everything up', () => { - term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); + term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line assert.equal(term.buffer.lines.length, INIT_ROWS); term.scroll(); @@ -537,9 +536,9 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c'; + term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); + term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(); @@ -549,11 +548,11 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c'; - term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd'; - term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); + term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); + term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); term.buffer.y = 3; term.buffer.scrollBottom = 3; term.scroll(); @@ -566,11 +565,11 @@ describe('term.js addons', () => { }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c'; - term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd'; - term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e'; + term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); + term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); + term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; diff --git a/src/Terminal.ts b/src/Terminal.ts index ef9cb9ab..c995af62 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -52,7 +52,6 @@ import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; -import { BufferLine } from './BufferLine'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -106,7 +105,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { tabStopWidth: 8, theme: null, rightClickSelectsWord: Browser.isMac, - rendererType: 'canvas' + rendererType: 'canvas', + experimentalBufferLineImpl: 'JsArray' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -493,6 +493,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; + case 'experimentalBufferLineImpl': + this.buffers.normal.setBufferLineFactory(value); + this.buffers.alt.setBufferLineFactory(value); + break; } // Inform renderer of changes if (this.renderer) { @@ -1170,7 +1174,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(isWrapped?: boolean): void { - const newLine = BufferLine.blankLine(this.cols, DEFAULT_ATTR, isWrapped); + const newLine = this.buffer.getBlankLine(DEFAULT_ATTR, isWrapped); const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1722,7 +1726,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.buffer.ybase = 0; this.buffer.y = 0; for (let i = 1; i < this.rows; i++) { - this.buffer.lines.push(BufferLine.blankLine(this.cols, DEFAULT_ATTR)); + this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR)); } this.refresh(0, this.rows - 1); this.emit('scroll', this.buffer.ydisp); @@ -1814,7 +1818,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // blankLine(true) is xterm/linux behavior const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop; this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1); - this.buffer.lines.set(this.buffer.y + this.buffer.ybase, BufferLine.blankLine(this.cols, this.eraseAttr())); + this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttr())); this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollBottom); } else { diff --git a/src/Types.ts b/src/Types.ts index 40b3bd02..e8578426 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -295,6 +295,7 @@ export interface IBuffer { getWrappedRangeForLine(y: number): { first: number, last: number }; nextStop(x?: number): number; prevStop(x?: number): number; + getBlankLine(attr: number, isWrapped?: boolean): IBufferLine; stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; } @@ -516,10 +517,15 @@ export interface IBufferLine { isWrapped: boolean; get(index: number): CharData; set(index: number, value: CharData): void; - pop(): CharData | undefined; - push(data: CharData): void; - splice(start: number, deleteCount: number, ...items: CharData[]): CharData[]; 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; + fill(fillCharData: CharData): void; + copyFrom(line: IBufferLine): void; + clone(): IBufferLine; +} + +export interface IBufferLineConstructor { + new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine; } diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index bb7a2c5d..383d2a7f 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -21,17 +21,21 @@ describe('CharacterJoinerRegistry', () => { lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]])); lines.set(3, lineData([['no joined ranges']])); - lines.set(4, new BufferLine()); + lines.set(4, new BufferLine(0)); lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]])); const line6 = lineData([['wi']]); - line6.push([0, '¥', 2, '¥'.charCodeAt(0)]); - line6.push([0, '', 0, null]); + line6.resize(line6.length + 1, [0, '¥', 2, '¥'.charCodeAt(0)]); + line6.resize(line6.length + 1, [0, '', 0, null]); let sub = lineData([['deemo']]); - for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i)); - line6.push([0, '\xf0\x9f\x98\x81', 1, 128513]); - line6.push([0, ' ', 1, ' '.charCodeAt(0)]); + let oldSize = line6.length; + line6.resize(oldSize + sub.length, [0, '', 0, 0]); + for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i)); + line6.resize(line6.length + 1, [0, '\xf0\x9f\x98\x81', 1, 128513]); + line6.resize(line6.length + 1, [0, ' ', 1, ' '.charCodeAt(0)]); sub = lineData([['jiabc']]); - for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i)); + oldSize = line6.length; + line6.resize(oldSize + sub.length, [0, '', 0, 0]); + for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i)); lines.set(6, line6); (terminal.buffer).setLines(lines); @@ -264,11 +268,13 @@ describe('CharacterJoinerRegistry', () => { type IPartialLineData = ([string] | [string, number]); function lineData(data: IPartialLineData[]): IBufferLine { - const tline = new BufferLine(); + const tline = new BufferLine(0); for (let i = 0; i < data.length; ++i) { const line = data[i][0]; const attr = (data[i][1] || 0); - line.split('').map(char => tline.push([attr, char, 1, char.charCodeAt(0)])); + const offset = tline.length; + tline.resize(tline.length + line.split('').length, [0, '', 0, 0]); + line.split('').map((char, idx) => tline.set(idx + offset, [attr, char, 1, char.charCodeAt(0)])); } return tline; } diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 4e795b6c..2c46d8cc 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -150,9 +150,9 @@ describe('DomRendererRowFactory', () => { } function createEmptyLineData(cols: number): IBufferLine { - const lineData = new BufferLine(); + const lineData = new BufferLine(cols); for (let i = 0; i < cols; i++) { - lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + lineData.set(i, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); } return lineData; } diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index b38f4b85..f575855a 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -319,6 +319,9 @@ export class MockBuffer implements IBuffer { setLines(lines: ICircularList): void { this.lines = lines; } + getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { + return Buffer.prototype.getBlankLine.apply(this, arguments); + } stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] { return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 3982874e..b5a8a109 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -101,6 +101,17 @@ declare module 'xterm' { */ experimentalCharAtlas?: 'none' | 'static' | 'dynamic'; + /** + * (EXPERIMENTAL) Defines which implementation to use for buffer lines. + * + * - 'JsArray': The default/stable implementation. + * - 'TypedArray': The new experimental implementation based on TypedArrays that is expected to + * significantly boost performance and memory consumption. Use at your own risk. + * + * This option will be removed in the future. + */ + experimentalBufferLineImpl?: 'JsArray' | 'TypedArray'; + /** * The font size used to render text. */