From 841a33be2830705697a196fca6d8df4d5916bc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 31 Aug 2018 04:40:08 +0200 Subject: [PATCH 01/16] remove push/pop/splice calls to BufferLine --- src/Buffer.test.ts | 46 +++++----- src/Buffer.ts | 4 +- src/BufferLine.test.ts | 62 ++++--------- src/BufferLine.ts | 42 +++++++-- src/InputHandler.test.ts | 86 +++++++++---------- src/InputHandler.ts | 33 ++++--- src/Linkifier.test.ts | 4 +- src/SelectionManager.test.ts | 12 +-- src/Types.ts | 7 +- src/renderer/CharacterJoinerRegistry.test.ts | 22 +++-- .../dom/DomRendererRowFactory.test.ts | 4 +- 11 files changed, 161 insertions(+), 161 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 6f417f8e..902d54b0 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -273,11 +273,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 +285,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 +296,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 +313,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 +326,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 +337,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 81b8a517..4ae168b4 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -124,9 +124,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); } } diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 61dfe543..10f4815f 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -18,35 +18,20 @@ describe('BufferLine', function(): void { it('ctor', function(): void { let line: IBufferLine = new TestBufferLine(); 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); chai.expect(line.length).equals(10); - chai.expect(line.pop()).eql([123, 'a', 456, 789]); + chai.expect(line.get(0)).eql([123, 'a', 456, 789]); 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); @@ -58,39 +43,30 @@ describe('BufferLine', function(): void { 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]); + const line = new TestBufferLine(3); + line.set(0, [1, 'a', 0, 0]); + line.set(1, [2, 'b', 0, 0]); + line.set(2, [3, 'c', 0, 0]); 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]]); }); 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]); + const line = new TestBufferLine(5); + line.set(0, [1, 'a', 0, 0]); + line.set(1, [2, 'b', 0, 0]); + line.set(2, [3, 'c', 0, 0]); + line.set(3, [4, 'd', 0, 0]); + line.set(4, [5, 'e', 0, 0]); 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]]); }); 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]); + const line = new TestBufferLine(5); + line.set(0, [1, 'a', 0, 0]); + line.set(1, [2, 'b', 0, 0]); + line.set(2, [3, 'c', 0, 0]); + line.set(3, [4, 'd', 0, 0]); + line.set(4, [5, 'e', 0, 0]); 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]]); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 639049e2..bc54067f 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -25,7 +25,7 @@ export class BufferLine implements IBufferLine { 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) + this._push(ch); // Note: the ctor ch is not cloned (resembles old behavior) } } if (isWrapped) { @@ -41,18 +41,31 @@ export class BufferLine implements IBufferLine { this._data[index] = data; } - public pop(): CharData | undefined { + /** + * @deprecated + */ + private _pop(): CharData | undefined { const data = this._data.pop(); this.length = this._data.length; return data; } - public push(data: CharData): void { + /** + * @deprecated + * @param data + */ + private _push(data: CharData): void { this._data.push(data); this.length = this._data.length; } - public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { + /** + * @deprecated + * @param start + * @param deleteCount + * @param items + */ + private _splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { const removed = this._data.splice(start, deleteCount, ...items); this.length = this._data.length; return removed; @@ -61,16 +74,16 @@ export class BufferLine implements IBufferLine { /** 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 { while (n--) { - this.splice(pos, 1); - this.push(fill); + this._splice(pos, 1); + this._push(fill); } } @@ -80,4 +93,17 @@ export class BufferLine implements IBufferLine { this.set(start++, fill); // Note: fill is not cloned (resembles old behavior) } } + + /** resize line to cols filling new cells with fill */ + public resize(cols: number, fill: CharData, shrink: boolean = false): void { + if (shrink) { + while (this._data.length > cols) { + this._data.pop(); + } + } + while (this._data.length < cols) { + this._data.push(fill); + } + this.length = cols; + } } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 0db6c04f..724f67e9 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -97,6 +97,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', () => { @@ -177,8 +207,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]; @@ -194,23 +222,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')); @@ -225,7 +237,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; @@ -236,7 +248,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; @@ -247,7 +259,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; @@ -258,7 +270,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; @@ -269,25 +281,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')); @@ -302,7 +296,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; @@ -313,7 +307,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; @@ -324,7 +318,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; @@ -335,7 +329,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 3c6aae0f..52ece5df 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -392,10 +392,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 +405,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 +435,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 +451,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]); } } diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index eeaaeedc..5afad3c7 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -50,9 +50,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/Types.ts b/src/Types.ts index 5ea2024a..90e1a531 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -516,10 +516,11 @@ 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[]; + // 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; } diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index bb7a2c5d..e1981698 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -24,14 +24,18 @@ describe('CharacterJoinerRegistry', () => { lines.set(4, new BufferLine()); 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 17b1b938..ae180554 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -148,9 +148,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; } From 3f2605f35ced8ab0db4f2f189daabbbd1a5ce4ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 31 Aug 2018 17:45:32 +0200 Subject: [PATCH 02/16] typed array based BufferLine --- src/Buffer.test.ts | 8 ++- src/BufferLine.test.ts | 64 ++++++++++++++-------- src/BufferLine.ts | 120 ++++++++++++++++++++++++++++++++++++++++- src/Terminal.test.ts | 62 ++++++++++----------- 4 files changed, 197 insertions(+), 57 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 902d54b0..81f3ba4c 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -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'); diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 10f4815f..8b0bcbb3 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -10,7 +10,11 @@ import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, CHAR_DATA_ATTR_INDEX, 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; } } @@ -27,9 +31,9 @@ describe('BufferLine', function(): void { chai.expect(line.length).equals(10); 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.get(0)).eql([123, 'a', 456, 789]); + chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]); chai.expect(line.isWrapped).equals(true); }); it('TerminalLine.blankLine', function(): void { @@ -44,30 +48,46 @@ describe('BufferLine', function(): void { }); it('insertCells', function(): void { const line = new TestBufferLine(3); - line.set(0, [1, 'a', 0, 0]); - line.set(1, [2, 'b', 0, 0]); - line.set(2, [3, 'c', 0, 0]); - 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]]); + 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(5); - line.set(0, [1, 'a', 0, 0]); - line.set(1, [2, 'b', 0, 0]); - line.set(2, [3, 'c', 0, 0]); - line.set(3, [4, 'd', 0, 0]); - line.set(4, [5, 'e', 0, 0]); - 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]]); + 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(5); - line.set(0, [1, 'a', 0, 0]); - line.set(1, [2, 'b', 0, 0]); - line.set(2, [3, 'c', 0, 0]); - line.set(3, [4, 'd', 0, 0]); - line.set(4, [5, 'e', 0, 0]); - 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]]); + 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)] + ]); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index bc54067f..369210c4 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -8,10 +8,10 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; /** * Class representing a terminal line. */ -export class BufferLine implements IBufferLine { +export class BufferLineOld 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); + return new BufferLineOld(cols, ch, isWrapped); } protected _data: CharData[]; public isWrapped = false; @@ -107,3 +107,119 @@ export class BufferLine implements IBufferLine { this.length = cols; } } + +const enum Cell { + FLAGS = 0, + STRING = 1, + WIDTH = 2, + SIZE = 3 +} + +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: Uint32Array | null = null; + protected _combined: {[index: number]: string} = {}; + public length: number; + + constructor(cols?: number, ch?: CharData, public isWrapped: boolean = false) { + this.length = cols || 0; + if (cols) { + if (!ch) { + ch = [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, ch); + } + } + } + + 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 + ]; + } + + 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, fill: 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, fill); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.set(i, fill); + } + } + } + + public deleteCells(pos: number, n: number, fill: 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, fill); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.set(i, fill); + } + } + } + + public replaceCells(start: number, end: number, fill: CharData): void { + while (start < end && start < this.length) { + this.set(start++, fill); + } + } + + public resize(cols: number, fill: 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, fill); + } + } else if (shrink) { + if (cols) { + const data = new Uint32Array(cols * Cell.SIZE); + data.set(this._data.subarray(0, this.length)); + this._data = data; + } else { + this._data = null; + } + } + this.length = cols; + } +} diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 83ab9a68..789751d1 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -338,8 +338,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); @@ -349,9 +349,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(); @@ -361,11 +361,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(); @@ -379,11 +379,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; @@ -404,9 +404,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(); @@ -419,9 +419,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(); @@ -431,11 +431,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(); @@ -448,11 +448,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; From f3561fad3015e562877f3da338738962b889ab69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 31 Aug 2018 17:51:47 +0200 Subject: [PATCH 03/16] cleanup --- src/BufferLine.ts | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 369210c4..891aa177 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -33,44 +33,31 @@ export class BufferLineOld implements IBufferLine { } } - public get(index: number): CharData { - return this._data[index]; - } - - public set(index: number, data: CharData): void { - this._data[index] = data; - } - - /** - * @deprecated - */ private _pop(): CharData | undefined { const data = this._data.pop(); this.length = this._data.length; return data; } - /** - * @deprecated - * @param data - */ private _push(data: CharData): void { this._data.push(data); this.length = this._data.length; } - /** - * @deprecated - * @param start - * @param deleteCount - * @param items - */ 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 { + return this._data[index]; + } + + public set(index: number, data: CharData): void { + this._data[index] = data; + } + /** insert n cells ch at pos, right cells are lost (stable length) */ public insertCells(pos: number, n: number, ch: CharData): void { while (n--) { From 024404f5488d1ca9d75f962324f4c62af30638f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 1 Sep 2018 00:18:52 +0200 Subject: [PATCH 04/16] make cols mandatory in BufferLine ctor --- src/BufferLine.test.ts | 2 +- src/BufferLine.ts | 4 ++-- src/Types.ts | 3 --- src/renderer/CharacterJoinerRegistry.test.ts | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 8b0bcbb3..9d6ba036 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -20,7 +20,7 @@ class TestBufferLine extends BufferLine { 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.isWrapped).equals(false); line = new TestBufferLine(10); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 891aa177..de36a968 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -17,7 +17,7 @@ export class BufferLineOld implements IBufferLine { public isWrapped = false; public length: number; - constructor(cols?: number, ch?: CharData, isWrapped?: boolean) { + constructor(cols: number, ch?: CharData, isWrapped?: boolean) { this._data = []; this.length = this._data.length; if (cols) { @@ -111,7 +111,7 @@ export class BufferLine implements IBufferLine { protected _combined: {[index: number]: string} = {}; public length: number; - constructor(cols?: number, ch?: CharData, public isWrapped: boolean = false) { + constructor(cols: number, ch?: CharData, public isWrapped: boolean = false) { this.length = cols || 0; if (cols) { if (!ch) { diff --git a/src/Types.ts b/src/Types.ts index 90e1a531..3a83ee89 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -516,9 +516,6 @@ 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; diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index e1981698..383d2a7f 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -21,7 +21,7 @@ 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.resize(line6.length + 1, [0, '¥', 2, '¥'.charCodeAt(0)]); From f9453ef4fa17b578a86b585c912673b2f4423c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 1 Sep 2018 00:50:40 +0200 Subject: [PATCH 05/16] rename fill to fillCharData --- src/BufferLine.ts | 66 ++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index de36a968..170fc81d 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -17,20 +17,18 @@ export class BufferLineOld implements IBufferLine { 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 { @@ -67,29 +65,29 @@ export class BufferLineOld implements IBufferLine { } /** 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._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, fill: CharData, shrink: boolean = false): void { + 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(fill); + this._data.push(fillCharData); } this.length = cols; } @@ -111,17 +109,15 @@ export class BufferLine implements IBufferLine { protected _combined: {[index: number]: string} = {}; public length: number; - constructor(cols: number, ch?: CharData, public isWrapped: boolean = false) { - this.length = cols || 0; - if (cols) { - if (!ch) { - ch = [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, ch); - } + 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 { @@ -147,45 +143,45 @@ export class BufferLine implements IBufferLine { this._data[index * Cell.SIZE + Cell.WIDTH] = value[2]; } - public insertCells(pos: number, n: number, fill: CharData): void { + 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, fill); + this.set(pos + i, fillCharData); } } else { for (let i = pos; i < this.length; ++i) { - this.set(i, fill); + this.set(i, fillCharData); } } } - public deleteCells(pos: number, n: number, fill: CharData): void { + 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, fill); + this.set(i, fillCharData); } } else { for (let i = pos; i < this.length; ++i) { - this.set(i, fill); + this.set(i, fillCharData); } } } - 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); + this.set(start++, fillCharData); } } - public resize(cols: number, fill: CharData, shrink: boolean = false): void { + public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void { if (cols === this.length) { return; } @@ -196,7 +192,7 @@ export class BufferLine implements IBufferLine { } this._data = data; for (let i = this.length; i < cols; ++i) { - this.set(i, fill); + this.set(i, fillCharData); } } else if (shrink) { if (cols) { From 284b0832746daccc26b523018eb7fdd735cf0a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 1 Sep 2018 02:10:11 +0200 Subject: [PATCH 06/16] note on copy on data access --- src/BufferLine.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 170fc81d..eef73a90 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -100,6 +100,20 @@ const enum Cell { SIZE = 3 } + +/** + * 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 + * - clear/reset method + */ 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]; From 76edd8a3bb75cd8722ce6d7b78dcc5b143acc9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 1 Sep 2018 05:31:32 +0200 Subject: [PATCH 07/16] fix error in resize --- src/BufferLine.ts | 3 +- src/Memory.ts | 1113 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1114 insertions(+), 2 deletions(-) create mode 100644 src/Memory.ts diff --git a/src/BufferLine.ts b/src/BufferLine.ts index eef73a90..2ecc9f26 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -100,7 +100,6 @@ const enum Cell { SIZE = 3 } - /** * Typed array based bufferline implementation. * Note: Unlike the JS variant the access to the data @@ -211,7 +210,7 @@ export class BufferLine implements IBufferLine { } else if (shrink) { if (cols) { const data = new Uint32Array(cols * Cell.SIZE); - data.set(this._data.subarray(0, this.length)); + data.set(this._data.subarray(0, cols * Cell.SIZE)); this._data = data; } else { this._data = null; diff --git a/src/Memory.ts b/src/Memory.ts new file mode 100644 index 00000000..546436f8 --- /dev/null +++ b/src/Memory.ts @@ -0,0 +1,1113 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +// TODO: ctypes.Structure: write optional packed regrouping alignment function +// TODO: string convenient functions + +/** + * Address type + */ +export type Address = number; + +/** + * memory access primitives + * used to distinguish between typed array types + */ +export const enum AccessType { + UINT8 = 1, + UINT16 = 2, + UINT32 = 4, + INT8 = 8, + INT16 = 16, + INT32 = 32, + FLOAT32 = 64 + // FLOAT64 = 128 // not supported by default +} + +/** + * bitwidth of the access types + * used to adjust the address size and alignments + */ +export const enum AccessBits { + BIT8 = AccessType.UINT8 | AccessType.INT8, + BIT16 = AccessType.UINT16 | AccessType.INT16, + BIT32 = AccessType.UINT32 | AccessType.INT32 | AccessType.FLOAT32 +} + +// 2, 4, 8 byte alignments +export function align2(num: number): number { + return (num + 1) & ~1; +} +export function align4(num: number): number { + return (num + 3) & ~3; +} +export function align8(num: number): number { + return (num + 7) & ~7; +} + +/** + * Interface to access different typed array based memory types. + * This is used by memory implementations. + */ +export interface IMemory { + [index: number]: Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Int32Array | Float32Array; + data: Uint32Array; + registeredAccessTypes: AccessType; + alloc(bytes: number): Address; + free(idx: Address): void; + registerAccess(acc: AccessType): void; + updateAccess(): void; + clear(): void; + readonly RESERVED_BYTES: number; +} + +// max usable bytes - due to 4 byte alignment the last 4 bytes to 2^32 are treated as not accessible +// Note: most JS engines will not allow you to allocate that much memory for a typed array +const MAX_BYTES = 0xFFFFFFFC; + +/** + * Base class for memory implementations. + */ +abstract class Memory implements IMemory { + /** memory accessors */ + [index: number]: Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Int32Array | Float32Array; + public [AccessType.UINT8]: Uint8Array; + public [AccessType.UINT16]: Uint16Array; + public [AccessType.UINT32]: Uint32Array; + public [AccessType.INT8]: Int8Array; + public [AccessType.INT16]: Int16Array; + public [AccessType.INT32]: Int32Array; + public [AccessType.FLOAT32]: Float32Array; + public registeredAccessTypes = 0; + /** data storage */ + public data: Uint32Array; + /** reversed bytes at the beginning */ + readonly RESERVED_BYTES = 16; + /** allocate `bytes`, returns 8bit address */ + abstract alloc(bytes: number): Address; + /** free address `idx` */ + abstract free(idx: Address): void; + /** frees the whole memory */ + abstract clear(): void; + protected _callbacks: void[] = []; + constructor(public initialBytes: number, public maxBytes?: number) { + this.initialBytes >>>= 0; + if (!this.maxBytes) this.maxBytes = MAX_BYTES; + this.maxBytes >>>= 0; + if (this.initialBytes % 4) throw new Error('initialBytes must be a multiple of 4'); + if (this.maxBytes % 4) throw new Error('maxBytes must be a multiple of 4'); + if (this.initialBytes > this.maxBytes) throw new Error('initialBytes is greater than maxBytes'); + this[AccessType.UINT8] = null; + this[AccessType.UINT16] = null; + this[AccessType.UINT32] = null; + this[AccessType.INT8] = null; + this[AccessType.INT16] = null; + this[AccessType.INT32] = null; + this[AccessType.FLOAT32] = null; + } + /** install typed array type if needed */ + registerAccess(acc: AccessType): void { + if (acc & AccessType.UINT8 && !this[AccessType.UINT8]) { + this[AccessType.UINT8] = new Uint8Array(this.data.buffer); + } + if (acc & AccessType.UINT16 && !this[AccessType.UINT16]) { + this[AccessType.UINT16] = new Uint16Array(this.data.buffer); + } + if (acc & AccessType.UINT32 && !this[AccessType.UINT32]) { + this[AccessType.UINT32] = this.data; + } + if (acc & AccessType.INT8 && !this[AccessType.INT8]) { + this[AccessType.INT8] = new Int8Array(this.data.buffer); + } + if (acc & AccessType.INT16 && !this[AccessType.INT16]) { + this[AccessType.INT16] = new Int16Array(this.data.buffer); + } + if (acc & AccessType.INT32 && !this[AccessType.INT32]) { + this[AccessType.INT32] = new Int32Array(this.data.buffer); + } + if (acc & AccessType.FLOAT32 && !this[AccessType.FLOAT32]) { + this[AccessType.FLOAT32] = new Float32Array(this.data.buffer); + } + this.registeredAccessTypes |= acc; + } + /** updates typed arrays, should be called after resize */ + updateAccess(): void { + const acc = this.registeredAccessTypes; + if (acc & AccessType.UINT8) { + this[AccessType.UINT8] = new Uint8Array(this.data.buffer); + } + if (acc & AccessType.UINT16) { + this[AccessType.UINT16] = new Uint16Array(this.data.buffer); + } + if (acc & AccessType.UINT32) { + this[AccessType.UINT32] = this.data; + } + if (acc & AccessType.INT8) { + this[AccessType.INT8] = new Int8Array(this.data.buffer); + } + if (acc & AccessType.INT16) { + this[AccessType.INT16] = new Int16Array(this.data.buffer); + } + if (acc & AccessType.INT32) { + this[AccessType.INT32] = new Int32Array(this.data.buffer); + } + if (acc & AccessType.FLOAT32) { + this[AccessType.FLOAT32] = new Float32Array(this.data.buffer); + } + } +} + +/** + * StackMemory + * This memory uses a linear allocator similar to stack memory in C. + * It maintains a stack pointer `sp` to indicate next free portion in the memory. + * Any allocation will advance `sp` in a linear fashion, a call to `free` will + * treat any later allocation as freed. Note that there are no bound checks, + * therefore call `free` only with returned pointer from a previous `alloc` or `sp`. + * A typical use pattern is to save the stack pointer at the beginning, + * do some work with additional allocations and call `free` with the saved stack pointer + * to free all used memory at once. The stack memory will grow to `maxBytes` if needed. + * + * properties: + * - alloc O(1) (w'o growing) + * - free O(1) + * - double free safe + * - null pointer free safe + * - aligment 4 byte, start at 16 + */ +export class StackMemory extends Memory { + public sp: number; + constructor(initialBytes: number, maxBytes?: number) { + super(initialBytes, maxBytes); + this.data = new Uint32Array((this.initialBytes + this.RESERVED_BYTES) >>> 2); + this.clear(); + } + public alloc(bytes: number): Address { + if (!bytes) return 0; + const address = this.sp; + this.sp += align4(bytes) >>> 2; + if (this.data.length <= this.sp) { + let newSize = this.data.length << 1; + while (newSize < this.sp) { + newSize <<= 1; + } + if (newSize > (this.maxBytes >>> 2)) { + newSize = this.maxBytes >>> 2; + } + if ((newSize - address) << 2 < bytes) { + throw new Error('out of memory'); + } + const data = new Uint32Array(newSize); + data.set(this.data); + this.data = data; + this.updateAccess(); + } + return address << 2; + } + public free(address: Address): void { + if (address && address >>> 2 < this.sp) this.sp = address >>> 2; + } + public clear(): void { + this.sp = this.RESERVED_BYTES >>> 2; + } +} + +/** + * PoolMemory + * Allocates memory of a fixed `blockSize` in bytes (aligned to 4 bytes). + * The allocator uses internally a linked list for free blocks. + * The underlying memory will grow to `blockSize` if needed. + * + * properties: + * - alloc O(1) (w'o growing) + * - free O(1) + * - not double free safe + * - null pointer free safe + * - alignment 4 byte, start at 16 + */ +export class PoolMemory extends Memory { + public head: Address; + public blockSize: number; + public numBlocks: number; + public maxBlocks: number; + public entrySize: number; + constructor(blockSize: number, initialBlocks: number, maxBlocks?: number) { + blockSize = align4(blockSize); + super(initialBlocks * blockSize, maxBlocks * blockSize || MAX_BYTES - (MAX_BYTES % blockSize)); + this.blockSize = blockSize; + this.entrySize = blockSize >> 2; + this.numBlocks = initialBlocks; + this.maxBlocks = this.maxBytes / blockSize; + this.data = new Uint32Array((this.initialBytes + this.RESERVED_BYTES) >>> 2); + this.clear(); + } + public alloc(bytes: number): Address { + if (!bytes) return 0; + if (align4(bytes) > this.blockSize) throw new Error('blockSize exceeded'); + if (!this.head) { + let newBlocks = this.numBlocks * 2; + if (newBlocks > this.maxBlocks) newBlocks = this.maxBlocks; + if (newBlocks === this.numBlocks) throw new Error('out of memory'); + const data = new Uint32Array(this.entrySize * newBlocks + (this.RESERVED_BYTES >>> 2)); + data.set(this.data); + for (let i = this.data.length; i < data.length; i += this.entrySize) data[i] = i + this.entrySize; + data[data.length - this.entrySize] = 0; + this.head = this.data.length; + this.numBlocks = newBlocks; + this.data = data; + this.updateAccess(); + } + const address = this.head; + this.head = this.data[address]; + return address << 2; + } + public free(address: Address): void { + if (address) { + this.data[address >>> 2] = this.head; + this.head = address >>> 2; + } + } + public clear(): void { + this.head = this.RESERVED_BYTES >>> 2; + for (let i = this.head; i < this.data.length; i += this.entrySize) this.data[i] = i + this.entrySize; + this.data[this.data.length - this.entrySize] = 0; + } +} + +// seglist constants +export const enum SL { + PREV_SIZE = 0, // offset to real previous block size + SIZE = 1, // offset to own block size + PREV_LINKED = 2, // offset to pointer to previous in seglist + NEXT_LINKED = 3, // offset to pointer to next in seglist + DATA = 2, // offset of data part + HEADER_SIZE = 2 // block header size +} + +/** + * SeglistMemory + * Unlike `StackMemory` and `PoolMemory` this memory is a general purpose heap + * similar to malloc/free in C, thus different sizes can be allocated and freed + * independently. The allocator implements the seglist paradigm with LIFO first fit. + * The allocator maintains 8 seglists for these sizes: + * list: 0 1 2 3 4 5 6 7 + * bytes: 1-8, 9-16, 17-32, 33-64, 65-128, 129-256, 257-512, >512 + * For allocations up to 512 bytes the runtime is O(1), for bigger allocations + * it is O(k) for k free slots in the biggest seglist. + * On allocation a found free block will be split if the remaining space can hold + * another block (min block size is 16 bytes). Freed blocks will be merged with + * free neighbour blocks (left and right coalesce). + * To guarantee 8 byte alignment of the data location, the right coalesce size hint + * is not at the end of a block, instead it was moved to the next block header. + * Therefore it is needed to track the highest defined block separately with + * the `.last` property. + * block header layout: + * free block [ blocksize of left block, own blocksize, previous block, next block, ..... ] + * taken block [ blocksize of left block, own blocksize, data, ..... ] + * A block is marked as taken with the 1st bit in blocksize. + * Note: There are no boundary checks, writing outside of the allocated location is likely + * to corrupt the heap data. Same goes for double free. + * + * properties: + * - alloc O(1) for allocations <= 512 bytes + * - alloc O(k) for allocations > 512 bytes + * - free O(1) + * - not double free safe + * - null pointer free safe + * - alignment 8 byte, block start at 16 (24 is the first data location) + */ +export class SeglistMemory extends Memory { + public heads: Address[]; + public last: Address; + public readonly SEGLIST_SIZE = 8; + public readonly HEAD_IDX = [ + 0, 7, 1, 7, 7, 7, 2, 7, 7, 7, 7, 7, 7, 7, 3, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 7, 5, 4, 31 + ]; + private _toHeadIndex(v: number): number { + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + return this.HEAD_IDX[(32 + ((v * 0x07C4ACDD) >> 27)) & 31]; + } + constructor(initialBytes: number, maxBytes?: number) { + super(initialBytes, maxBytes || MAX_BYTES - 4); + if (this.initialBytes % 8) throw new Error('initialBytes must be a multiple of 8'); + if (this.maxBytes % 8) throw new Error('maxBytes must be a multiple of 8'); + this.data = new Uint32Array((this.initialBytes + this.RESERVED_BYTES) >>> 2); + this.heads = []; + this.clear(); + } + public getFromHead(v: number): number { + return this._toHeadIndex(v - 1); + } + public setToHead(v: number): number { + return (v >= 256) ? 7 : this._toHeadIndex(v) - 1; + } + public isTaken(block: Address): number { + if (!block) return 1; + return this.data[block + SL.SIZE] & 1; + } + public realNext(block: Address): Address { + const next = block + (this.data[block + SL.SIZE] & ~1) + SL.HEADER_SIZE; + return (next < this.data.length) ? next : 0; + } + public realPrev(block: Address): Address { + const prev = block - (this.data[block + SL.PREV_SIZE] & ~1) - SL.HEADER_SIZE; + return (prev > 3) ? prev : 0; + } + public removeFromList(block: Address, hIdx: number): void { + const prev = this.data[block + SL.PREV_LINKED]; + const next = this.data[block + SL.NEXT_LINKED]; + if (next) this.data[next + SL.PREV_LINKED] = prev; + if (prev) this.data[prev + SL.NEXT_LINKED] = next; + if (this.heads[hIdx] === block) this.heads[hIdx] = next; + } + public insertToList(block: Address, hIdx: number): void { + const next = this.heads[hIdx]; + this.data[block + SL.PREV_LINKED] = 0; + this.data[block + SL.NEXT_LINKED] = next; + if (next) this.data[next + SL.PREV_LINKED] = block; + this.heads[hIdx] = block; + } + public splitBlock(block: Address, size: number): void { + const newBlockSize = this.data[block + SL.SIZE] - size - SL.HEADER_SIZE; + const next = this.realNext(block); + if (next) this.data[next + SL.PREV_SIZE] = newBlockSize; + const newBlock = block + size + SL.HEADER_SIZE; + this.data[newBlock + SL.PREV_SIZE] = size; + this.data[newBlock + SL.SIZE] = newBlockSize; + this.insertToList(newBlock, this.setToHead(this.data[newBlock + SL.SIZE])); + this.data[block + SL.SIZE] = size; + if (block === this.last) this.last = newBlock; + } + public leftCoalesce(prev: Address, size: number): void { + const oldhIdx = this.setToHead(this.data[prev + SL.SIZE]); + this.data[prev + SL.SIZE] += size; + const newhIdx = this.setToHead(this.data[prev + SL.SIZE]); + if (oldhIdx !== newhIdx) { + this.removeFromList(prev, oldhIdx); + this.insertToList(prev, newhIdx); + } + } + public alloc(bytes: number): Address { + if (!bytes) return 0; + const size = align8(bytes) >>> 2; + let block = 0; + let hIdx = this.getFromHead(size); + while (!(block = this.heads[hIdx]) && hIdx < 7) hIdx++; + if (hIdx === 7) { + block = this.heads[7]; + while (block && this.data[block + SL.SIZE] < size) block = this.data[block + SL.NEXT_LINKED]; + } + if (!block) { + // no suitable block found, resize + const oldSize = this.data.length; + let newSize = this.data.length << 1; + let requestedSize = this.data.length + size + 2; + if (!this.isTaken(this.last)) requestedSize -= this.data[this.last + SL.SIZE]; + while (newSize < requestedSize) newSize <<= 1; + if (newSize > (this.maxBytes >>> 2)) newSize = this.maxBytes >>> 2; + if (newSize < requestedSize) throw new Error('out of memory'); + const data = new Uint32Array(newSize); + data.set(this.data); + this.data = data; + if (this.isTaken(this.last)) { + // last block is taken, create fresh block at the end and insert as head in seglist + block = oldSize; + const blockSize = newSize - oldSize - SL.HEADER_SIZE; + this.data[block + SL.PREV_SIZE] = this.data[this.last + SL.SIZE]; + this.data[block + SL.SIZE] = blockSize; + this.last = block; + hIdx = this.setToHead(blockSize); + this.insertToList(block, hIdx); + } else { + // last block is free, merge added space with last + block = this.last; + this.leftCoalesce(block, newSize - oldSize); + hIdx = this.setToHead(this.data[block + SL.SIZE]); + } + this.updateAccess(); + } + this.removeFromList(block, hIdx); + if (this.data[block + SL.SIZE] - size > 3) this.splitBlock(block, size); + const next = this.realNext(block); + if (next) this.data[next + SL.PREV_SIZE] |= 1; + this.data[block + SL.SIZE] |= 1; + return (block + SL.DATA) << 2; + } + public free(address: Address): void { + if (!address) return; + const block = (address >>> 2) - SL.DATA; + this.data[block + SL.SIZE] &= ~1; + const realNext = this.realNext(block); + const realPrev = this.realPrev(block); + if (realNext) { + this.data[realNext + SL.PREV_SIZE] &= ~1; + if (!this.isTaken(realNext)) { + this.removeFromList(realNext, this.setToHead(this.data[realNext + SL.SIZE])); + this.data[block + SL.SIZE] += this.data[realNext + SL.SIZE] + SL.HEADER_SIZE; + if (this.last === realNext) this.last = block; + else this.data[this.realNext(realNext) + SL.PREV_SIZE] = this.data[block + SL.SIZE]; + } + } + if (realPrev && !this.isTaken(realPrev)) { + this.leftCoalesce(realPrev, this.data[block + SL.SIZE] + SL.HEADER_SIZE); + if (this.last === block) this.last = realPrev; + else this.data[this.realNext(realPrev) + SL.PREV_SIZE] = this.data[realPrev + SL.SIZE]; + } else { + this.insertToList(block, this.setToHead(this.data[block + SL.SIZE])); + } + } + public clear(): void { + this.heads = []; + for (let i = 0; i < this.SEGLIST_SIZE; ++i) this.heads.push(0); + const start = this.RESERVED_BYTES >>> 2; + this.data[start + SL.PREV_SIZE] = 1; + this.data[start + SL.SIZE] = this.data.length - start - SL.HEADER_SIZE; + this.data[start + SL.NEXT_LINKED] = 0; + this.data[start + SL.PREV_LINKED] = 0; + this.last = start; + this.heads[this.setToHead(this.data[start + SL.SIZE])] = start; + } +} + +/** + * Basic C like types. + * + * Implemented types: + * - numerical types up to 32 bit (see `NumberType`) + * - character types (`Char` and `WChar`) + * - pointer types (`VoidPointer` and `TypedPointer`) + * - array types creation with any of the others (`CArray`) + * - struct creation with any of the others (`Structure`) + * Missing: + * - double support (see notes below) + * - convenient types for strings + * + * All ctypes have the same constructor signature: + * new ctype(memory: Memory, value?: any, address?: Address) + * + * The ctor creates a JS object with some bookkeeping for + * the underlying memory access. The ctor will alloc the needed + * space from `memory` automatically if `address` is omitted. + * `value` can be any suitable object that closely reassembles the + * created ctype: + * - numbers for `NumberType` (including pointers) + * - string for `CharType` + * - iterable for `CArray`, base type must match though + * - object with similar properties for `Structure` + * If `value` is omitted the memory is not touched (no default). + * + * About lifecycle: + * Any ctype object created without `address` needs to be freed afterwards + * by calling `memory.free(ctypeObject.address)`. + * There are no plans to implement a generic ref counting or GC on top of this + * (use native JS objects if you rely on such). For short living values + * consider using `StackMemory` and free the memory at once when done. + * About Performance: + * Generally creating and freeing ctype objects has worse performance + * than native JS due to the translation overhead. It will run faster + * if you rearrange your code to use the ctypes as references + * ("move" them around by adjusting the address with `setAddress`) to load + * and store data to and from JS and do the work directly on the memory. + * With reusing the ctype objects the GC will drop almost to 0%. + * Note on double support: + * To save memory `StackMemory` and `PoolMemory` are aligned to 4 byte, + * therefore doubles will not work out of the box (needs 8 byte alignment). + * With `StackMemory` you can pad with a dummy allocation to get a multiple of 8. + * With `PoolMemory` the `blockSize` must be a multiple of 8 to get double support. + * `SeglistMemory` automatically aligns memory locations to 8 byte. + */ + +export namespace ctypes { + + /** + * Interfaces. + */ + export interface ICTypeConstructor { + new(memory: IMemory, value?: any, address?: Address): T; + typename: string; + bytes: number; + accessType: AccessType; + fromAddress(accessor: Memory, address: Address): T; + } + export interface IPointerConstructor extends ICTypeConstructor { + new(memory: IMemory, value: any, address?: Address): IPointer; + type: T; + } + export interface IVoidPointerConstructor extends IPointerConstructor { + } + export interface ICArrayConstructor extends ICTypeConstructor { + new(memory: IMemory, value: any, address?: Address): CArrayBase; + type: T; + size: number; + } + export interface IStructureConstructor extends ICTypeConstructor { + fields: [string, ICTypeConstructor][]; + alignments: { [index: string]: number[] } | null; + } + + // interface for all ctypes + export interface ICType { + accessType: AccessType; + memory: IMemory; + setAddress(address: Address): void; + value: any; + getValue(): any; + setValue(value: any): void; + getBytes(): Uint8Array; + setBytes(value: Uint8Array): void; + address: Address; + } + + // pointer interface + export interface IPointer extends ICType { + deref(): T | never; + cast(type: ICTypeConstructor): IPointer; + inc(): void | never; + dec(): void | never; + add(value: number): void | never; + } + + // array interface + export interface ICArray extends ICType { + length: number; + getValue(): any[]; + setValue(value: any): void; + get(index: number): any; + set(index: number, value: any): void; + reverse(): void; + } + + // structure interface + interface IStructure extends ICType { + fields: { [index: string]: ICType }; + } + + + /** + * CType base class. + */ + export abstract class CType implements ICType { + static typename = 'CType'; + static bytes = 0; + static accessType = 0; + static fromAddress(accessor: Memory, address: Address): T { + return new (this as ICTypeConstructor)(accessor, null, address); + } + public accessType: AccessType; + public address: Address; + protected _accessAddress: Address; + protected _bytearray: Uint8Array; + constructor(public memory: IMemory, value?: any | null, address?: Address) { + this.accessType = (this.constructor as typeof CType).accessType; + this.setAddress(address || memory.alloc((this.constructor as typeof CType).bytes)); + memory.registerAccess(this.accessType); + this.setValue(value); + } + public setAddress(address: Address): void { + this.address = address; + this._accessAddress = (this.accessType & AccessBits.BIT32) + ? this.address >> 2 + : (this.accessType & AccessBits.BIT16) ? this.address >> 1 : this.address; + } + public getBytes(): Uint32Array { + if (this._bytearray) return this._bytearray; + this.memory.registerAccess(AccessType.UINT8); + this._bytearray = this.memory[AccessType.UINT8].subarray( + this.address, + this.address + (this.constructor as typeof CType).bytes); + return this._bytearray; + } + public setBytes(value: Uint8Array): void { + this.memory.registerAccess(AccessType.UINT8); + this.memory[AccessType.UINT8].set(value, this.address); + } + abstract value: any; + abstract getValue(): any; + abstract setValue(value: any): void; + } + + /** + * Numerical types. + */ + export abstract class NumberType extends CType implements ICType { + public getValue(): number { + return this.memory[this.accessType][this._accessAddress]; + } + public setValue(value: number | NumberType): void { + if (value === null || value === undefined) return; + if (value instanceof NumberType) { + this.memory[this.accessType][this._accessAddress] = value.memory[value.accessType][value._accessAddress]; + } else { + this.memory[this.accessType][this._accessAddress] = value; + } + } + get value(): number { + return this.memory[this.accessType][this._accessAddress]; + } + set value(value: number) { + this.memory[this.accessType][this._accessAddress] = value; + } + public inc(): void { + this.memory[this.accessType][this._accessAddress]++; + } + public dec(): void { + this.memory[this.accessType][this._accessAddress]--; + } + public iadd(value: NumberType): void { + this.memory[this.accessType][this._accessAddress] += value.memory[value.accessType][value._accessAddress]; + } + public isub(value: NumberType): void { + this.memory[this.accessType][this._accessAddress] -= value.memory[value.accessType][value._accessAddress]; + } + public imul(value: NumberType): void { + this.memory[this.accessType][this._accessAddress] *= value.memory[value.accessType][value._accessAddress]; + } + public idiv(value: NumberType): void { + this.memory[this.accessType][this._accessAddress] /= value.memory[value.accessType][value._accessAddress]; + } + public imod(value: NumberType): void { + this.memory[this.accessType][this._accessAddress] %= value.memory[value.accessType][value._accessAddress]; + } + } + export class Uint8 extends NumberType { + static typename = 'Uint8'; + static bytes = 1; + static accessType = AccessType.UINT8; + } + export class Uint16 extends NumberType { + static typename = 'Uint16'; + static bytes = 2; + static accessType = AccessType.UINT16; + } + export class Uint32 extends NumberType { + static typename = 'Uint32'; + static bytes = 4; + static accessType = AccessType.UINT32; + } + export class Int8 extends NumberType { + static typename = 'Int8'; + static bytes = 1; + static accessType = AccessType.INT8; + } + export class Int16 extends NumberType { + static typename = 'Int16'; + static bytes = 2; + static accessType = AccessType.INT16; + } + export class Int32 extends NumberType { + static typename = 'Int32'; + static bytes = 4; + static accessType = AccessType.INT32; + } + export class Float extends NumberType { + static typename = 'Float'; + static bytes = 4; + static accessType = AccessType.FLOAT32; + } + + /** + * Character types. + */ + export class CharType extends CType implements ICType { + public getValue(): string { + return String.fromCharCode(this.memory[this.accessType][this._accessAddress]); + } + public setValue(value: string | CharType): void { + if (value === null || value === undefined) return; + if (value instanceof CharType) { + this.memory[this.accessType][this._accessAddress] = value.memory[value.accessType][value._accessAddress]; + } else { + this.memory[this.accessType][this._accessAddress] = (value.length) ? value.charCodeAt(0) : 0; + } + } + get value(): string { + return String.fromCharCode(this.memory[this.accessType][this._accessAddress]); + } + set value(value: string) { + this.memory[this.accessType][this._accessAddress] = (value.length) ? value.charCodeAt(0) : 0; + } + } + export class Char extends CharType { + static typename = 'Char'; + static bytes = 1; + static accessType = AccessType.UINT8; + } + export class WChar extends CharType { + static typename = 'WChar'; + static bytes = 2; + static accessType = AccessType.UINT16; + } + + /** + * Pointer types. + * A call `pointer(ctype)` creates the `ctype*` pointer constructor. + * Casting is done by `.cast(new_ctype)`. + * Pointers follow the `IPointer` interface to get proper type checks. + * Double pointers can be created by several `Pointer` invocations. + * Example: + * let P_Char = pointer(Char); // ctor for pointer type char* + * let PP_Char = pointer>(P_Char); // ctor for pointer type char** + * let c = new Char(stack, '!'); + * let p = new PChar(stack, c.address); // creates pointer to c + * let pp = new PP_Char(stack, p.address); // double pointer to c + * which is roughly equivalent to: + * char c = '!'; + * char *p = &c; + * char **pp = &p; + */ + export class VoidPointer extends NumberType implements IPointer { + static typename = 'void*'; + static bytes = 4; + static accessType = AccessType.UINT32; + static type: null = null; + public deref(): never { + throw new Error('trying to deref void pointer'); + } + public cast(type: ICTypeConstructor): IPointer { + if (type === null) { + return this; + } + return new (pointer(type))(this.memory, this.value, this.address); + } + public inc(): never { + throw new Error('arithmetic on void pointer'); + } + public dec(): never { + throw new Error('arithmetic on void pointer'); + } + public add(value: number): never { + throw new Error('arithmetic on void pointer'); + } + } + + // save pointer type ctors + const registeredPointerTypes: { [type: string]: IPointerConstructor } = {}; + + // Pointer type factory function. + export function pointer(type: ICTypeConstructor | null): IVoidPointerConstructor | IPointerConstructor { + if (!type === null) { + return VoidPointer; + } + if (registeredPointerTypes[type.typename]) { + return registeredPointerTypes[type.typename]; + } + + class TypedPointer extends NumberType implements IPointer { + static typename = type.typename + '*'; + static type = type; + static bytes = 4; + static accessType = AccessType.UINT32; + public deref(): T { + if (!this.value) { + throw new Error('trying to deref NULL pointer'); + } + return new type(this.memory, null, this.value); + } + public cast(type: ICTypeConstructor | null): IPointer { + if (type === null) { + return new VoidPointer(this.memory, this.value, this.address); + } + return new (pointer(type))(this.memory, this.value, this.address); + } + public inc(): void { + this.value += type.bytes; + } + public dec(): void { + this.value -= type.bytes; + } + public add(value: number): void { + this.value += value * type.bytes; + } + } + + if (!registeredPointerTypes[type.typename]) { + registeredPointerTypes[type.typename] = TypedPointer; + } + return registeredPointerTypes[type.typename]; + } + + /** + * Array types. + * A call `array(ctype, 10)` creates the `ctype[10]` array constructor. + * Example usage: + * let Uint8_10 = array(Uint8, 10); + * let array = new Uint8_10(stack, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + */ + // we need this array base class to get something in the prototype chain + // to test against with `instanceof` to find CArray types + abstract class CArrayBase extends CType implements ICArray { + abstract length: number = 0; + abstract getValue(): any[]; + abstract setValue(value: any): void; + abstract get(index: number): any; + abstract set(index: number, value: any): void; + abstract reverse(): void; + } + + // save array type ctors + const registeredArrayTypes: { [type: string]: ICArrayConstructor } = {}; + + // Array type factory function. + export function array(type: ICTypeConstructor, length: number): ICArrayConstructor { + const typename = `${type.typename}[${length}]`; + if (registeredArrayTypes[typename]) { + return registeredArrayTypes[typename]; + } + + class CArray extends CArrayBase { + static typename = typename; + static type = type; + static bytes = type.bytes * length; + static accessType = type.accessType; + static size = length; + public length: number; + constructor(memory: Memory, value?: any, address?: Address) { + super(memory, null, address); + this.length = length; + this.setValue(value); + } + public get value(): any[] | CArrayBase | any { + return this.getValue(); + } + public set value(value: any[] | CArrayBase | any) { + this.setValue(value); + } + public getValue(): any[] { + const res = []; + const obj = new type(this.memory, null, this.address); + let p = this.address; + for (let i = 0; i < this.length; ++i, p += type.bytes) { + obj.setAddress(p); + res.push(obj.getValue()); + } + return res; + } + public setValue(value: any): void { + if (value === null || value === undefined) return; + const corrThis = (this.accessType & AccessBits.BIT32) ? 2 : (this.accessType & AccessBits.BIT16) ? 1 : 0; + const end = (this.length < value.length) ? this.length : value.length; + if (value instanceof CArrayBase) { + const corrValue = (value.accessType & AccessBits.BIT32) ? 2 : (value.accessType & AccessBits.BIT16) ? 1 : 0; + const valueCtor = (value.constructor as typeof CArray).type.prototype; + // direct copy for character types and number types + if ((type.prototype instanceof NumberType && valueCtor instanceof NumberType) + || (type.prototype instanceof CharType && valueCtor instanceof CharType)) { + let pThis = this.address; + let pValue = value.address; + for (let i = 0; i < end; ++i, pThis += type.bytes, pValue += (value.constructor as typeof CArray).type.bytes) { + this.memory[this.accessType][pThis >> corrThis] = value.memory[value.accessType][pValue >> corrValue]; + } + return; + } + // copy struct arrays of same type + if (type.prototype instanceof Structure && type === (value.constructor as typeof CArray).type) { + const access = (this.accessType & AccessBits.BIT32) + ? AccessType.UINT32 + : (this.accessType & AccessBits.BIT16) ? AccessType.UINT16 : AccessType.UINT8; + const pThis = this.address >> corrThis; + const pValue = value.address >> corrThis; + const slots = (type.bytes * end) >> corrThis; + for (let i = 0; i < slots; ++i) { + this.memory[access][pThis + i] = value.memory[access][pValue + i]; + } + return; + } + } + // get values by index access + if (value instanceof array || value.length !== undefined) { + const obj = new type(this.memory, null, this.address); + let p = this.address; + for (let i = 0; i < end; ++i, p += type.bytes) { + obj.setAddress(p); + obj.setValue(value[i]); + } + return; + } + // FIXME: How to deal with different CArray types? + // fallthrough to for .. of + const pend = this.address + type.bytes * this.length; + const obj = new type(this.memory, null, this.address); + let p = this.address; + for (const v of value) { + obj.setAddress(p); + obj.setValue(v); + p += type.bytes; + if (p >= pend) { + break; + } + } + } + public get(index: number): any { + return new type(this.memory, null, this.address + type.bytes * (index % this.length)).getValue(); + } + public set(index: number, value: any): void { + new type(this.memory, null, this.address + type.bytes * (index % this.length)).setValue(value); + } + public reverse(): void { + const corr = (this.accessType & AccessBits.BIT32) ? 2 : (this.accessType & AccessBits.BIT16) ? 1 : 0; + const slotLength = type.bytes >> corr; + for (let i = 0; i < this.length >> 1; ++i) { + let start = (this.address + type.bytes * i) >> corr; + let end = (this.address + type.bytes * (this.length - 1 - i)) >> corr; + for (let j = 0; j < slotLength; ++j, ++start, ++end) { + const temp = this.memory[this.accessType][start]; + this.memory[this.accessType][start] = this.memory[this.accessType][end]; + this.memory[this.accessType][end] = temp; + } + } + } + } + + if (!registeredArrayTypes[typename]) { + registeredArrayTypes[typename] = CArray; + } + return registeredArrayTypes[typename]; + } + + /** + * Structure base class. + * Base class to create C like struct types. Simply subclass it and define + * the struct members in the `fields` property. The `typename` is needed for + * pointers or arrays of the struct and must be unique across all ctypes. + * After instantiation the struct members are exposed under `fields`. + * + * Example usage: + * class Foo extends Structure { + * static typename = 'Foo'; + * static fields: [string, ICTypeConstructor][] = [ + * ['a', Uint8], + * ['b', Float] + * ]; + * } + * let foo = new Foo(stack, {a: 123, b: 1.23456}); + * foo.fields.a.value == 123; // true + * foo.fields.a.value = 42; // assignment + * + * Note: The struct size is aligned to the highest member access type to avoid + * offset errors in arrays. Members are aligned according to their access type + * thus creating lots of padding bytes if the next bytes cannot be addressed by + * the following member's access type. To get a better pack rate group similar types together. + * + * Example: + * layout [['a', Int8], ['b', Int32], ['c', Int8], ['d', Int32], ['e', Int16]] + * byte usage [X--- XXXX X--- XXXX XX** ] = 20 bytes + * The '-' bytes are lost since the access type of Int32 can only address every 4th byte. + * The '*' bytes are lost due to alignment of the struct size to the access type of Int32. + * In total 8 bytes are wasted. With some regrouping all bytes can be used: + * layout [['a', Int8], ['c', Int8], ['e', Int16], ['b', Int32], ['d', Int32]] + * byte usage [X X XX XXXX XXXX ] = 12 bytes + */ + export abstract class Structure extends CType implements IStructure { + public fields: { [index: string]: ICType }; + static fields: [string, ICTypeConstructor][] = []; + private static _accessors: AccessType = 0; + private static _aligments: { [index: string]: number[] } | null = null; + private static _bytes: number; + static get accessType(): any { + if (!this._accessors) { + for (let i = 0; i < this.fields.length; ++i) { + this._accessors |= this.fields[i][1].accessType; + } + } + return this._accessors; + } + static get alignments(): { [index: string]: number[] } { + if (!this._aligments) { + this._aligments = {}; + let p = 0; + for (let i = 0; i < this.fields.length; ++i) { + const byteSize = this.fields[i][1].bytes; + const acc = this.fields[i][1].accessType; + if (acc & AccessBits.BIT32 && p & 3) { + p = ((p >> 2) + 1) << 2; // TODO: use align function + } + else if (acc & AccessBits.BIT16 && p & 1) { + p++; + } + this._aligments[this.fields[i][0]] = [p, byteSize]; + p += byteSize; + } + } + return this._aligments; + } + static get bytes(): number { + if (!this._bytes) { + const lastMemberAlign = this.alignments[this.fields[this.fields.length - 1][0]]; + this._bytes = lastMemberAlign[0] + lastMemberAlign[1]; + if (this.accessType & AccessBits.BIT32 && this._bytes & 3) { + this._bytes = ((this._bytes >> 2) + 1) << 2; + } + else if (this.accessType & AccessBits.BIT16 && this._bytes & 1) { + this._bytes++; + } + } + return this._bytes; + } + constructor(memory: IMemory, value?: any, address?: Address) { + super(memory, null, address); + const fields = (this.constructor as IStructureConstructor).fields; + const alignments = (this.constructor as IStructureConstructor).alignments; + this.fields = {}; + for (let i = 0; i < fields.length; ++i) { + this.fields[fields[i][0]] = new fields[i][1](memory, null, this.address + alignments[fields[i][0]][0]); + } + this.setValue(value); + } + public get value(): any { + return this.getValue(); + } + public set value(value: any) { + this.setValue(value); + } + public getValue(): any { + const res: { [index: string]: any } = {}; + for (const el in this.fields) { + res[el] = this.fields[el].getValue(); + } + return res; + } + public setValue(value: any): void { + if (value === null || value === undefined) { + return; + } + if (value && this.constructor === value.constructor) { + const corr = (this.accessType & AccessBits.BIT32) ? 2 : (this.accessType & AccessBits.BIT16) ? 1 : 0; + const access = (this.accessType & AccessBits.BIT32) + ? AccessType.UINT32 + : (this.accessType & AccessBits.BIT16) ? AccessType.UINT16 : AccessType.UINT8; + const pThis = this.address >> corr; + const pValue = value._address >> corr; + const slots = (this.constructor as IStructureConstructor).bytes >> corr; + for (let i = 0; i < slots; ++i) { + this.memory[access][pThis + i] = value.memory[access][pValue + i]; + } + return; + } + if (value instanceof Structure) { + value = value.fields; + } + for (const el in this.fields) { + if (value[el] !== undefined) { + this.fields[el].setValue(value[el]); + } + } + } + public setAddress(address: Address): void { + super.setAddress(address); + if (!this.fields) return; + const fields = (this.constructor as IStructureConstructor).fields; + const alignments = (this.constructor as IStructureConstructor).alignments; + for (let i = 0; i < fields.length; ++i) { + this.fields[fields[i][0]].setAddress(this.address + alignments[fields[i][0]][0]); + } + } + } + +} // end namespace ctypes From 165c8d4522926f2025ecb1a870153dee9cc9b905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 4 Sep 2018 16:30:17 +0200 Subject: [PATCH 08/16] convenient methods fill, makeCopyOf and clone for BufferLine --- src/BufferLine.test.ts | 53 ++++++++++++++++++++++++++++++++ src/BufferLine.ts | 68 ++++++++++++++++++++++++++++++++++++++++-- src/Types.ts | 3 ++ 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 9d6ba036..47073a4e 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -90,4 +90,57 @@ describe('BufferLine', function(): void { [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('makeCopyOf', 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.makeCopyOf(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.makeCopyOf(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 2ecc9f26..1fa5ca61 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -91,6 +91,27 @@ export class BufferLineOld implements IBufferLine { } this.length = cols; } + + public fill(fillCharData: CharData): void { + for (let i = 0; i < this.length; ++i) { + this.set(i, fillCharData); + } + } + + public makeCopyOf(line: IBufferLine): void { + this._data = []; + for (let i = 0; i < line.length; ++i) { + this.set(i, line.get(i)); + } + this.length = line.length; + this.isWrapped = line.isWrapped; + } + + public clone(): IBufferLine { + const newLine = new BufferLineOld(0); + newLine.makeCopyOf(this); + return newLine; + } } const enum Cell { @@ -111,7 +132,6 @@ const enum Cell { * line.set(0, ch); // do this to update line data * TODO: * - provide getData/setData to directly access the data - * - clear/reset method */ export class BufferLine implements IBufferLine { static blankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine { @@ -141,7 +161,9 @@ export class BufferLine implements IBufferLine { ? this._combined[index] : (stringData) ? String.fromCharCode(stringData) : '', this._data[index * Cell.SIZE + Cell.WIDTH], - stringData & ~0x80000000 + (stringData & 0x80000000) + ? this._combined[index].charCodeAt(this._combined[index].length - 1) + : stringData ]; } @@ -218,4 +240,46 @@ export class BufferLine implements IBufferLine { } this.length = cols; } + + /** + * new methods... + */ + + /** 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 makeCopyOf(line: BufferLine): 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 BufferLine(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/Types.ts b/src/Types.ts index 615ae2d2..b852b09f 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -519,4 +519,7 @@ export interface IBufferLine { 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; + makeCopyOf(line: IBufferLine): void; + clone(): IBufferLine; } From 2877c0da80b55e4af3ce09a9b62cb1ce3f4059dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 4 Sep 2018 18:20:01 +0200 Subject: [PATCH 09/16] switch for BufferLineJsArray vs BufferLineTypedArray --- demo/main.js | 3 ++- src/Buffer.ts | 40 ++++++++++++++++++++++++++++++++++++++-- src/BufferLine.ts | 29 +++++++++++++++++------------ src/Terminal.ts | 15 ++++++++++----- src/Types.ts | 6 ++++++ 5 files changed, 73 insertions(+), 20 deletions(-) diff --git a/demo/main.js b/demo/main.js index 70544e38..e0fb5aa4 100644 --- a/demo/main.js +++ b/demo/main.js @@ -184,7 +184,8 @@ function initOptions(term) { 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'], + bufferLineConstructor: ['JsArray', 'TypedArray'] }; var options = Object.keys(term._core.options); var booleanOptions = []; diff --git a/src/Buffer.ts b/src/Buffer.ts index 4ae168b4..3f0445bc 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,10 +4,10 @@ */ import { CircularList } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine } from './Types'; +import { CharData, ITerminal, IBuffer, IBufferLine, IBufferLineConstructor } from './Types'; import { EventEmitter } from './EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine } from './BufferLine'; +import { BufferLine, BufferLineJsArray, 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,40 @@ export class Buffer implements IBuffer { this.clear(); } + public setBufferLineFactory(type: string): void { + if (type === 'JsArray') { + if (this._bufferLineConstructor === BufferLineJsArray) { + return; + } + this._bufferLineConstructor = BufferLineJsArray; + this._recreateLines(); + } else if (type === 'TypedArray') { + if (this._bufferLineConstructor === BufferLineTypedArray) { + return; + } + this._bufferLineConstructor = BufferLineTypedArray; + this._recreateLines(); + } else { + this._bufferLineConstructor = BufferLine; + } + } + + 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(cols: number, attr: number, isWrapped?: boolean): IBufferLine { + return this._bufferLineConstructor.blankLine(cols, attr, isWrapped); + } + public get hasScrollback(): boolean { return this._hasScrollback && this.lines.maxLength > this._terminal.rows; } @@ -94,6 +129,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.bufferLineConstructor); this.ydisp = 0; this.ybase = 0; this.y = 0; diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 1fa5ca61..aa2a2181 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -2,16 +2,16 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { CharData, IBufferLine } from './Types'; +import { CharData, IBufferLine, IBufferLineConstructor } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; /** * Class representing a terminal line. */ -export class BufferLineOld implements IBufferLine { +export class BufferLineJsArray 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 BufferLineOld(cols, ch, isWrapped); + return new BufferLineJsArray(cols, ch, isWrapped); } protected _data: CharData[]; public isWrapped = false; @@ -108,7 +108,7 @@ export class BufferLineOld implements IBufferLine { } public clone(): IBufferLine { - const newLine = new BufferLineOld(0); + const newLine = new BufferLineJsArray(0); newLine.makeCopyOf(this); return newLine; } @@ -133,10 +133,10 @@ const enum Cell { * TODO: * - provide getData/setData to directly access the data */ -export class BufferLine implements IBufferLine { +export class BufferLineTypedArray 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); + return new BufferLineTypedArray(cols, ch, isWrapped); } protected _data: Uint32Array | null = null; protected _combined: {[index: number]: string} = {}; @@ -241,10 +241,6 @@ export class BufferLine implements IBufferLine { this.length = cols; } - /** - * new methods... - */ - /** fill a line with fillCharData */ public fill(fillCharData: CharData): void { this._combined = {}; @@ -254,7 +250,7 @@ export class BufferLine implements IBufferLine { } /** alter to a full copy of line */ - public makeCopyOf(line: BufferLine): void { + public makeCopyOf(line: BufferLineTypedArray): void { if (this.length !== line.length) { this._data = new Uint32Array(line._data); } else { @@ -271,7 +267,7 @@ export class BufferLine implements IBufferLine { /** create a new clone */ public clone(): IBufferLine { - const newLine = new BufferLine(0); + 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); @@ -283,3 +279,12 @@ export class BufferLine implements IBufferLine { return newLine; } } + +/** + * implementation switch + * needed to test the different implementation throughout the + * whole code base and tests + * FIXME: remove once we are settled with one + */ +export const BufferLine = BufferLineJsArray; +// export const BufferLine: IBufferLineConstructor = BufferLineTypedArray; diff --git a/src/Terminal.ts b/src/Terminal.ts index 3927d8b5..7104ba21 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -52,7 +52,7 @@ 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'; +import { BufferLine, BufferLineJsArray } from './BufferLine'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -106,7 +106,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { tabStopWidth: 8, theme: null, rightClickSelectsWord: Browser.isMac, - rendererType: 'canvas' + rendererType: 'canvas', + bufferLineConstructor: (BufferLine === BufferLineJsArray) ? 'JsArray' : 'TypedArray' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -493,6 +494,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; + case 'bufferLineConstructor': + this.buffers.normal.setBufferLineFactory(value); + this.buffers.alt.setBufferLineFactory(value); + break; } // Inform renderer of changes if (this.renderer) { @@ -1170,7 +1175,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(this.cols, DEFAULT_ATTR, isWrapped); const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1722,7 +1727,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(this.cols, DEFAULT_ATTR)); } this.refresh(0, this.rows - 1); this.emit('scroll', this.buffer.ydisp); @@ -1814,7 +1819,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.cols, this.eraseAttr())); this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollBottom); } else { diff --git a/src/Types.ts b/src/Types.ts index b852b09f..5cc8cf19 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -264,6 +264,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions { screenKeys?: boolean; termName?: string; useFlowControl?: boolean; + bufferLineConstructor?: string; } export interface IBuffer { @@ -523,3 +524,8 @@ export interface IBufferLine { makeCopyOf(line: IBufferLine): void; clone(): IBufferLine; } + +export interface IBufferLineConstructor { + new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine; + blankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine; +} From 7873598a41a5d3bee4b032264c2cfaf4b17165f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 4 Sep 2018 21:41:00 +0200 Subject: [PATCH 10/16] rename BufferLineJsArray as BufferLine --- src/Buffer.ts | 14 ++++---------- src/BufferLine.ts | 17 ++++------------- src/Terminal.ts | 3 +-- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 3f0445bc..914f0744 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -7,7 +7,7 @@ import { CircularList } from './common/CircularList'; import { CharData, ITerminal, IBuffer, IBufferLine, IBufferLineConstructor } from './Types'; import { EventEmitter } from './EventEmitter'; import { IMarker } from 'xterm'; -import { BufferLine, BufferLineJsArray, BufferLineTypedArray } from './BufferLine'; +import { BufferLine, BufferLineTypedArray } from './BufferLine'; export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -55,16 +55,10 @@ export class Buffer implements IBuffer { } public setBufferLineFactory(type: string): void { - if (type === 'JsArray') { - if (this._bufferLineConstructor === BufferLineJsArray) { - return; - } - this._bufferLineConstructor = BufferLineJsArray; + if (type === 'JsArray' && this._bufferLineConstructor !== BufferLine) { + this._bufferLineConstructor = BufferLine; this._recreateLines(); - } else if (type === 'TypedArray') { - if (this._bufferLineConstructor === BufferLineTypedArray) { - return; - } + } else if (type === 'TypedArray' && this._bufferLineConstructor !== BufferLineTypedArray) { this._bufferLineConstructor = BufferLineTypedArray; this._recreateLines(); } else { diff --git a/src/BufferLine.ts b/src/BufferLine.ts index aa2a2181..c2bcb172 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -2,16 +2,16 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { CharData, IBufferLine, IBufferLineConstructor } from './Types'; +import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; /** * Class representing a terminal line. */ -export class BufferLineJsArray implements IBufferLine { +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 BufferLineJsArray(cols, ch, isWrapped); + return new BufferLine(cols, ch, isWrapped); } protected _data: CharData[]; public isWrapped = false; @@ -108,7 +108,7 @@ export class BufferLineJsArray implements IBufferLine { } public clone(): IBufferLine { - const newLine = new BufferLineJsArray(0); + const newLine = new BufferLine(0); newLine.makeCopyOf(this); return newLine; } @@ -279,12 +279,3 @@ export class BufferLineTypedArray implements IBufferLine { return newLine; } } - -/** - * implementation switch - * needed to test the different implementation throughout the - * whole code base and tests - * FIXME: remove once we are settled with one - */ -export const BufferLine = BufferLineJsArray; -// export const BufferLine: IBufferLineConstructor = BufferLineTypedArray; diff --git a/src/Terminal.ts b/src/Terminal.ts index 7104ba21..a1a66dc5 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, BufferLineJsArray } from './BufferLine'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -107,7 +106,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { theme: null, rightClickSelectsWord: Browser.isMac, rendererType: 'canvas', - bufferLineConstructor: (BufferLine === BufferLineJsArray) ? 'JsArray' : 'TypedArray' + bufferLineConstructor: 'JsArray' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { From c8b8e39dde27acec9b14dc7bbdeaa6886a368021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Tue, 4 Sep 2018 22:08:01 +0200 Subject: [PATCH 11/16] move blankLine to Buffer --- src/Buffer.test.ts | 6 +++--- src/Buffer.ts | 10 ++++++---- src/BufferLine.test.ts | 12 +----------- src/BufferLine.ts | 8 -------- src/InputHandler.ts | 9 ++++----- src/Terminal.test.ts | 7 +++---- src/Terminal.ts | 6 +++--- src/Types.ts | 2 +- src/utils/TestUtils.test.ts | 9 +++++++-- 9 files changed, 28 insertions(+), 41 deletions(-) diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 81f3ba4c..ebd036d4 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++) { @@ -184,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; @@ -204,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; diff --git a/src/Buffer.ts b/src/Buffer.ts index 914f0744..ea62edf2 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -78,8 +78,9 @@ export class Buffer implements IBuffer { } } - public getBlankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine { - return this._bufferLineConstructor.blankLine(cols, attr, isWrapped); + 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 { @@ -114,7 +115,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)); } } } @@ -175,7 +176,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 47073a4e..a4011f9e 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -5,7 +5,7 @@ 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 { @@ -36,16 +36,6 @@ describe('BufferLine', function(): void { chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]); chai.expect(line.isWrapped).equals(true); }); - 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(3); line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index c2bcb172..bcc9990e 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -9,10 +9,6 @@ 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; @@ -134,10 +130,6 @@ const enum Cell { * - provide getData/setData to directly access the data */ export class BufferLineTypedArray 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 BufferLineTypedArray(cols, ch, isWrapped); - } protected _data: Uint32Array | null = null; protected _combined: {[index: number]: string} = {}; public length: number; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index d0d6db47..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`. @@ -831,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(); @@ -861,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(); @@ -893,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); @@ -912,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/Terminal.test.ts b/src/Terminal.test.ts index 789751d1..0111de75 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; @@ -142,7 +141,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', () => { @@ -159,7 +158,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', () => { @@ -172,7 +171,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)); } }); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index a1a66dc5..5c020371 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1174,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 = this.buffer.getBlankLine(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; @@ -1726,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(this.buffer.getBlankLine(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); @@ -1818,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, this.buffer.getBlankLine(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 5cc8cf19..03407cfc 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -284,6 +284,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; } export interface IBufferSet extends IEventEmitter { @@ -527,5 +528,4 @@ export interface IBufferLine { export interface IBufferLineConstructor { new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine; - blankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine; } diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index b9bb0348..a5de2b69 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -4,10 +4,11 @@ */ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler, IBufferLine } from '../Types'; -import { Buffer } from '../Buffer'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler, IBufferLine, CharData } from '../Types'; +import { Buffer, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH } from '../Buffer'; import * as Browser from '../shared/utils/Browser'; import { ITheme, IDisposable, IMarker } from 'xterm'; +import { BufferLine } from '../BufferLine'; export class MockTerminal implements ITerminal { markers: IMarker[]; @@ -310,6 +311,10 @@ export class MockBuffer implements IBuffer { setLines(lines: ICircularList): void { this.lines = lines; } + getBlankLine(attr: number, isWrapped: boolean = false): IBufferLine { + const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + return new BufferLine(80, fillCharData, isWrapped); + } } export class MockRenderer implements IRenderer { From d4f7c6bcf5e3b034f6726883f19abeac45935496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 8 Sep 2018 17:51:22 +0200 Subject: [PATCH 12/16] rename to copyFrom; use _push for JSArray based bufferline --- src/BufferLine.test.ts | 6 +++--- src/BufferLine.ts | 8 ++++---- src/Types.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index a4011f9e..a1a8e0ff 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -108,7 +108,7 @@ describe('BufferLine', function(): void { chai.expect(line2.length).equals(line.length); chai.expect(line2.isWrapped).equals(line.isWrapped); }); - it('makeCopyOf', function(): void { + 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)]); @@ -116,7 +116,7 @@ describe('BufferLine', function(): void { 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.makeCopyOf(line); + line2.copyFrom(line); chai.expect(line2.toArray()).eql(line.toArray()); chai.expect(line2.length).equals(line.length); chai.expect(line2.isWrapped).equals(line.isWrapped); @@ -128,7 +128,7 @@ describe('BufferLine', function(): void { 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.makeCopyOf(line); + 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 bcc9990e..2587fad8 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -94,10 +94,10 @@ export class BufferLine implements IBufferLine { } } - public makeCopyOf(line: IBufferLine): void { + public copyFrom(line: IBufferLine): void { this._data = []; for (let i = 0; i < line.length; ++i) { - this.set(i, line.get(i)); + this._push(line.get(i)); } this.length = line.length; this.isWrapped = line.isWrapped; @@ -105,7 +105,7 @@ export class BufferLine implements IBufferLine { public clone(): IBufferLine { const newLine = new BufferLine(0); - newLine.makeCopyOf(this); + newLine.copyFrom(this); return newLine; } } @@ -242,7 +242,7 @@ export class BufferLineTypedArray implements IBufferLine { } /** alter to a full copy of line */ - public makeCopyOf(line: BufferLineTypedArray): void { + public copyFrom(line: BufferLineTypedArray): void { if (this.length !== line.length) { this._data = new Uint32Array(line._data); } else { diff --git a/src/Types.ts b/src/Types.ts index e764f249..cdac9156 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -508,7 +508,7 @@ export interface IBufferLine { replaceCells(start: number, end: number, fill: CharData): void; resize(cols: number, fill: CharData, shrink?: boolean): void; fill(fillCharData: CharData): void; - makeCopyOf(line: IBufferLine): void; + copyFrom(line: IBufferLine): void; clone(): IBufferLine; } From a754f372094636eabafd4b945e059a7bb2d78e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 8 Sep 2018 18:02:59 +0200 Subject: [PATCH 13/16] fix conditions in Buffer.setBufferLineFactory --- src/Buffer.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index ae4619b0..ee982564 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -55,14 +55,16 @@ export class Buffer implements IBuffer { } public setBufferLineFactory(type: string): void { - if (type === 'JsArray' && this._bufferLineConstructor !== BufferLine) { - this._bufferLineConstructor = BufferLine; - this._recreateLines(); - } else if (type === 'TypedArray' && this._bufferLineConstructor !== BufferLineTypedArray) { - this._bufferLineConstructor = BufferLineTypedArray; - this._recreateLines(); + if (type === 'TypedArray') { + if (this._bufferLineConstructor !== BufferLineTypedArray) { + this._bufferLineConstructor = BufferLineTypedArray; + this._recreateLines(); + } } else { - this._bufferLineConstructor = BufferLine; + if (this._bufferLineConstructor !== BufferLine) { + this._bufferLineConstructor = BufferLine; + this._recreateLines(); + } } } From 7447a8d08d9531b39add8bc86ef8dbe554dcef6d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 9 Sep 2018 07:20:13 -0700 Subject: [PATCH 14/16] Rename setting to experimentalBufferLineImpl, expose in d.ts --- demo/client.ts | 2 +- src/Buffer.ts | 2 +- src/Terminal.ts | 4 ++-- src/Types.ts | 1 - typings/xterm.d.ts | 11 +++++++++++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 22095997..1765bdb3 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -209,7 +209,7 @@ function initOptions(term: TerminalType): void { 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'], - bufferLineConstructor: ['JsArray', 'TypedArray'] + experimentalBufferLineImpl: ['JsArray', 'TypedArray'] }; const options = Object.keys((term)._core.options); const booleanOptions = []; diff --git a/src/Buffer.ts b/src/Buffer.ts index ee982564..d24b8b3b 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -126,7 +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.bufferLineConstructor); + this.setBufferLineFactory(this._terminal.options.experimentalBufferLineConstructor); this.ydisp = 0; this.ybase = 0; this.y = 0; diff --git a/src/Terminal.ts b/src/Terminal.ts index 2d935b57..c995af62 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -106,7 +106,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { theme: null, rightClickSelectsWord: Browser.isMac, rendererType: 'canvas', - bufferLineConstructor: 'JsArray' + experimentalBufferLineImpl: 'JsArray' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -493,7 +493,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; - case 'bufferLineConstructor': + case 'experimentalBufferLineImpl': this.buffers.normal.setBufferLineFactory(value); this.buffers.alt.setBufferLineFactory(value); break; diff --git a/src/Types.ts b/src/Types.ts index cdac9156..a64c2c45 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -263,7 +263,6 @@ export interface ITerminalOptions extends IPublicTerminalOptions { screenKeys?: boolean; termName?: string; useFlowControl?: boolean; - bufferLineConstructor?: string; } export interface IBuffer { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 80b41db5..33546206 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -99,6 +99,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. */ From 6c2acd6e349bac5986d6c1d917507eef57c81e24 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 9 Sep 2018 08:00:05 -0700 Subject: [PATCH 15/16] Fix reference to new setting --- src/Buffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index d24b8b3b..eae5c837 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -126,7 +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.experimentalBufferLineConstructor); + this.setBufferLineFactory(this._terminal.options.experimentalBufferLineImpl); this.ydisp = 0; this.ybase = 0; this.y = 0; From 3dbc6e604ff5fa6a05c1e3bd0f41064a94c31ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 7 Oct 2018 22:07:55 +0200 Subject: [PATCH 16/16] move SIZE out of Cell enum --- src/BufferLine.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 2587fad8..4fdceee1 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -110,11 +110,14 @@ export class BufferLine implements IBufferLine { } } +/** typed array slots taken by one cell */ +const CELL_SIZE = 3; + +/** cell member indices */ const enum Cell { FLAGS = 0, STRING = 1, - WIDTH = 2, - SIZE = 3 + WIDTH = 2 } /** @@ -138,7 +141,7 @@ export class BufferLineTypedArray implements IBufferLine { if (!fillCharData) { fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; } - this._data = new Uint32Array(cols * Cell.SIZE); + this._data = new Uint32Array(cols * CELL_SIZE); for (let i = 0; i < cols; ++i) { this.set(i, fillCharData); } @@ -146,13 +149,13 @@ export class BufferLineTypedArray implements IBufferLine { } public get(index: number): CharData { - const stringData = this._data[index * Cell.SIZE + Cell.STRING]; + const stringData = this._data[index * CELL_SIZE + Cell.STRING]; return [ - this._data[index * Cell.SIZE + Cell.FLAGS], + this._data[index * CELL_SIZE + Cell.FLAGS], (stringData & 0x80000000) ? this._combined[index] : (stringData) ? String.fromCharCode(stringData) : '', - this._data[index * Cell.SIZE + Cell.WIDTH], + this._data[index * CELL_SIZE + Cell.WIDTH], (stringData & 0x80000000) ? this._combined[index].charCodeAt(this._combined[index].length - 1) : stringData @@ -160,14 +163,14 @@ export class BufferLineTypedArray implements IBufferLine { } public set(index: number, value: CharData): void { - this._data[index * Cell.SIZE + Cell.FLAGS] = value[0]; + 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; + 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.STRING] = value[1].charCodeAt(0); } - this._data[index * Cell.SIZE + Cell.WIDTH] = value[2]; + this._data[index * CELL_SIZE + Cell.WIDTH] = value[2]; } public insertCells(pos: number, n: number, fillCharData: CharData): void { @@ -213,7 +216,7 @@ export class BufferLineTypedArray implements IBufferLine { return; } if (cols > this.length) { - const data = new Uint32Array(cols * Cell.SIZE); + const data = new Uint32Array(cols * CELL_SIZE); if (this._data) { data.set(this._data); } @@ -223,8 +226,8 @@ export class BufferLineTypedArray implements IBufferLine { } } else if (shrink) { if (cols) { - const data = new Uint32Array(cols * Cell.SIZE); - data.set(this._data.subarray(0, cols * Cell.SIZE)); + const data = new Uint32Array(cols * CELL_SIZE); + data.set(this._data.subarray(0, cols * CELL_SIZE)); this._data = data; } else { this._data = null;