diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index a51e5456..6f417f8e 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,9 +5,10 @@ import { assert } from 'chai'; import { ITerminal } from './Types'; -import { Buffer } from './Buffer'; +import { Buffer, DEFAULT_ATTR } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; +import { BufferLine } from './BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -36,13 +37,13 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = terminal.blankLine()[0]; + const blankLineChar = BufferLine.blankLine(terminal.cols, DEFAULT_ATTR).get(0); buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); for (let y = 0; y < INIT_ROWS; y++) { assert.equal(buffer.lines.get(y).length, INIT_COLS); for (let x = 0; x < INIT_COLS; x++) { - assert.deepEqual(buffer.lines.get(y)[x], blankLineChar); + assert.deepEqual(buffer.lines.get(y).get(x), blankLineChar); } } }); @@ -66,40 +67,40 @@ describe('Buffer', () => { describe('wrapped', () => { it('should return a range for the first row', () => { buffer.fillViewportRows(); - ( buffer.lines.get(1)).isWrapped = true; + buffer.lines.get(1).isWrapped = true; assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 }); }); it('should return a range for a middle row wrapping upwards', () => { buffer.fillViewportRows(); - ( buffer.lines.get(12)).isWrapped = true; + buffer.lines.get(12).isWrapped = true; assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 }); }); it('should return a range for a middle row wrapping downwards', () => { buffer.fillViewportRows(); - ( buffer.lines.get(13)).isWrapped = true; + buffer.lines.get(13).isWrapped = true; assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 }); }); it('should return a range for a middle row wrapping both ways', () => { buffer.fillViewportRows(); - ( buffer.lines.get(11)).isWrapped = true; - ( buffer.lines.get(12)).isWrapped = true; - ( buffer.lines.get(13)).isWrapped = true; - ( buffer.lines.get(14)).isWrapped = true; + buffer.lines.get(11).isWrapped = true; + buffer.lines.get(12).isWrapped = true; + buffer.lines.get(13).isWrapped = true; + buffer.lines.get(14).isWrapped = true; assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 }); }); it('should return a range for the last row', () => { buffer.fillViewportRows(); - ( buffer.lines.get(23)).isWrapped = true; + buffer.lines.get(23).isWrapped = true; assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 }); }); it('should return a range for a row that wraps upward to first row', () => { buffer.fillViewportRows(); - ( buffer.lines.get(1)).isWrapped = true; + buffer.lines.get(1).isWrapped = true; assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 }); }); it('should return a range for a row that wraps downward to last row', () => { buffer.fillViewportRows(); - ( buffer.lines.get(buffer.lines.length - 1)).isWrapped = true; + buffer.lines.get(buffer.lines.length - 1).isWrapped = true; assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 }); }); }); @@ -154,11 +155,11 @@ describe('Buffer', () => { assert.equal(buffer.lines.maxLength, INIT_ROWS); buffer.y = INIT_ROWS - 1; buffer.fillViewportRows(); - buffer.lines.get(5)[0][1] = 'a'; - buffer.lines.get(INIT_ROWS - 1)[0][1] = 'b'; + buffer.lines.get(5).get(0)[1] = 'a'; + buffer.lines.get(INIT_ROWS - 1).get(0)[1] = 'b'; buffer.resize(INIT_COLS, INIT_ROWS - 5); - assert.equal(buffer.lines.get(0)[0][1], 'a'); - assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5)[0][1], 'b'); + assert.equal(buffer.lines.get(0).get(0)[1], 'a'); + assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b'); }); }); }); @@ -179,7 +180,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(terminal.blankLine()); + buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -199,7 +200,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); // Create 10 extra blank lines for (let i = 0; i < 10; i++) { - buffer.lines.push(terminal.blankLine()); + buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR)); } // Set cursor to the bottom of the buffer buffer.y = INIT_ROWS - 1; @@ -272,34 +273,34 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { - buffer.lines.set(0, [ - [ null, 'a', 1, 'a'.charCodeAt(0)], - [ null, 'b', 1, 'b'.charCodeAt(0)], - [ null, 'c', 1, 'c'.charCodeAt(0)], - [ null, 'd', 1, 'd'.charCodeAt(0)] - ]); + 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)]); + buffer.lines.set(0, line); const str = buffer.translateBufferLineToString(0, true, 0, 2); assert.equal(str, 'ab'); }); it('should handle a cut-off double width character by including it', () => { - buffer.lines.set(0, [ - [ null, '語', 2, 35486 ], - [ null, '', 0, null], - [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + const line = new BufferLine(); + line.push([ null, '語', 2, 35486 ]); + line.push([ null, '', 0, null]); + line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str1, '語'); }); it('should handle a zero width character in the middle of the string by not including it', () => { - buffer.lines.set(0, [ - [ null, '語', 2, '語'.charCodeAt(0) ], - [ null, '', 0, null], - [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + const line = new BufferLine(); + line.push([ null, '語', 2, '語'.charCodeAt(0) ]); + line.push([ null, '', 0, null]); + line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.set(0, line); const str0 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str0, '語'); @@ -312,10 +313,10 @@ describe('Buffer', () => { }); it('should handle single width emojis', () => { - buffer.lines.set(0, [ - [ null, '😁', 1, '😁'.charCodeAt(0) ], - [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + const line = new BufferLine(); + line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]); + line.push([ null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str1, '😁'); @@ -325,10 +326,10 @@ describe('Buffer', () => { }); it('should handle double width emojis', () => { - buffer.lines.set(0, [ - [ null, '😁', 2, '😁'.charCodeAt(0) ], - [ null, '', 0, null] - ]); + const line = new BufferLine(); + line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]); + line.push([ null, '', 0, null]); + buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); assert.equal(str1, '😁'); @@ -336,11 +337,11 @@ describe('Buffer', () => { const str2 = buffer.translateBufferLineToString(0, true, 0, 2); assert.equal(str2, '😁'); - buffer.lines.set(0, [ - [ null, '😁', 2, '😁'.charCodeAt(0) ], - [ null, '', 0, null], - [ null, 'a', 1, 'a'.charCodeAt(0)] - ]); + const line2 = new BufferLine(); + line2.push([ null, '😁', 2, '😁'.charCodeAt(0) ]); + line2.push([ null, '', 0, null]); + line2.push([ null, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.set(0, line2); const str3 = buffer.translateBufferLineToString(0, true, 0, 3); assert.equal(str3, '😁a'); diff --git a/src/Buffer.ts b/src/Buffer.ts index 5c843808..81b8a517 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -4,9 +4,10 @@ */ import { CircularList } from './common/CircularList'; -import { LineData, CharData, ITerminal, IBuffer } from './Types'; +import { CharData, ITerminal, IBuffer, IBufferLine } from './Types'; import { EventEmitter } from './EventEmitter'; import { IMarker } from 'xterm'; +import { BufferLine } from './BufferLine'; export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -27,7 +28,7 @@ export const NULL_CELL_CODE = 32; * - scroll position */ export class Buffer implements IBuffer { - public lines: CircularList; + public lines: CircularList; public ydisp: number; public ybase: number; public y: number; @@ -84,7 +85,7 @@ export class Buffer implements IBuffer { if (this.lines.length === 0) { let i = this._terminal.rows; while (i--) { - this.lines.push(this._terminal.blankLine()); + this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } } } @@ -97,7 +98,7 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this.lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); + this.lines = new CircularList(this._getCorrectBufferLength(this._terminal.rows)); this.scrollTop = 0; this.scrollBottom = this._terminal.rows - 1; this.setupTabStops(); @@ -146,7 +147,7 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - this.lines.push(this._terminal.blankLine(undefined, undefined, newCols)); + this.lines.push(BufferLine.blankLine(newCols, DEFAULT_ATTR)); } } } @@ -223,7 +224,7 @@ export class Buffer implements IBuffer { let endIndex = endCol; for (let i = 0; i < line.length; i++) { - const char = line[i]; + const char = line.get(i); lineString += char[CHAR_DATA_CHAR_INDEX]; // Adjust start and end cols for wide characters if they affect their // column indexes @@ -268,11 +269,11 @@ export class Buffer implements IBuffer { let first = y; let last = y; // Scan upwards for wrapped lines - while (first > 0 && (this.lines.get(first)).isWrapped) { + while (first > 0 && this.lines.get(first).isWrapped) { first--; } // Scan downwards for wrapped lines - while (last + 1 < this.lines.length && (this.lines.get(last + 1)).isWrapped) { + while (last + 1 < this.lines.length && this.lines.get(last + 1).isWrapped) { last++; } return { first, last }; diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts new file mode 100644 index 00000000..61dfe543 --- /dev/null +++ b/src/BufferLine.test.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ +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'; + + +class TestBufferLine extends BufferLine { + public toArray(): CharData[] { + return this._data; + } +} + +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.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.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.isWrapped).equals(true); + }); + it('splice', function(): void { + const line = new TestBufferLine(); + const data: CharData[] = [ + [1, 'a', 0, 0], + [2, 'b', 0, 0], + [3, 'c', 0, 0] + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + chai.expect(line.length).equals(data.length); + const removed1 = line.splice(1, 1, [4, 'd', 0, 0]); + const removed2 = data.splice(1, 1, [4, 'd', 0, 0]); + chai.expect(removed1).eql(removed2); + chai.expect(line.toArray()).eql(data); + }); + it('TerminalLine.blankLine', function(): void { + const line = TestBufferLine.blankLine(5, 123); + chai.expect(line.length).equals(5); + chai.expect(line.isWrapped).equals(false); + const ch = line.get(0); + chai.expect(ch[CHAR_DATA_ATTR_INDEX]).equals(123); + chai.expect(ch[CHAR_DATA_CHAR_INDEX]).equals(NULL_CELL_CHAR); + chai.expect(ch[CHAR_DATA_WIDTH_INDEX]).equals(NULL_CELL_WIDTH); + chai.expect(ch[CHAR_DATA_CODE_INDEX]).equals(NULL_CELL_CODE); + }); + it('insertCells', function(): void { + const line = new TestBufferLine(); + const data: CharData[] = [ + [1, 'a', 0, 0], + [2, 'b', 0, 0], + [3, 'c', 0, 0] + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + line.insertCells(1, 3, [4, 'd', 0, 0]); + chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [4, 'd', 0, 0]]); + }); + it('deleteCells', function(): void { + const line = new TestBufferLine(); + const data: CharData[] = [ + [1, 'a', 0, 0], + [2, 'b', 0, 0], + [3, 'c', 0, 0], + [4, 'd', 0, 0], + [5, 'e', 0, 0] + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + line.deleteCells(1, 2, [6, 'f', 0, 0]); + chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [5, 'e', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0]]); + }); + it('replaceCells', function(): void { + const line = new TestBufferLine(); + const data: CharData[] = [ + [1, 'a', 0, 0], + [2, 'b', 0, 0], + [3, 'c', 0, 0], + [4, 'd', 0, 0], + [5, 'e', 0, 0] + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + line.replaceCells(2, 4, [6, 'f', 0, 0]); + chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [2, 'b', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0], [5, 'e', 0, 0]]); + }); +}); diff --git a/src/BufferLine.ts b/src/BufferLine.ts new file mode 100644 index 00000000..639049e2 --- /dev/null +++ b/src/BufferLine.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { CharData, IBufferLine } from './Types'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; + +/** + * Class representing a terminal line. + */ +export class BufferLine implements IBufferLine { + static blankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine { + const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + return new BufferLine(cols, ch, isWrapped); + } + protected _data: CharData[]; + public isWrapped = false; + public length: number; + + constructor(cols?: number, ch?: CharData, isWrapped?: boolean) { + 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 (isWrapped) { + this.isWrapped = true; + } + } + + public get(index: number): CharData { + return this._data[index]; + } + + public set(index: number, data: CharData): void { + this._data[index] = data; + } + + public pop(): CharData | undefined { + const data = this._data.pop(); + this.length = this._data.length; + return data; + } + + public push(data: CharData): void { + this._data.push(data); + this.length = this._data.length; + } + + public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] { + const removed = this._data.splice(start, deleteCount, ...items); + this.length = this._data.length; + return removed; + } + + /** insert n cells ch at pos, right cells are lost (stable length) */ + public insertCells(pos: number, n: number, ch: CharData): void { + while (n--) { + this.splice(pos, 0, ch); + this.pop(); + } + } + + /** 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); + } + } + + /** replace cells from pos to pos + n - 1 with fill */ + public replaceCells(start: number, end: number, fill: CharData): void { + while (start < end && start < this.length) { + this.set(start++, fill); // Note: fill is not cloned (resembles old behavior) + } + } +} diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index e8099914..0db6c04f 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -3,9 +3,101 @@ * @license MIT */ -import { assert } from 'chai'; +import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal } from './utils/TestUtils.test'; +import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { Terminal } from './Terminal'; +import { IBufferLine } from './Types'; + +// TODO: This and the sections related to this object in associated tests can be +// removed safely after InputHandler refactors are finished +class OldInputHandler extends InputHandler { + public eraseInLine(params: number[]): void { + switch (params[0]) { + case 0: + this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y); + break; + case 1: + this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y); + break; + case 2: + this.eraseLine(this._terminal.buffer.y); + break; + } + } + + public eraseInDisplay(params: number[]): void { + let j; + switch (params[0]) { + case 0: + this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y); + j = this._terminal.buffer.y + 1; + for (; j < this._terminal.rows; j++) { + this.eraseLine(j); + } + break; + case 1: + this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y); + j = this._terminal.buffer.y; + while (j--) { + this.eraseLine(j); + } + break; + case 2: + j = this._terminal.rows; + while (j--) this.eraseLine(j); + break; + case 3: + // Clear scrollback (everything not in viewport) + const scrollBackSize = this._terminal.buffer.lines.length - this._terminal.rows; + if (scrollBackSize > 0) { + this._terminal.buffer.lines.trimStart(scrollBackSize); + this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0); + this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0); + // Force a scroll event to refresh viewport + this._terminal.emit('scroll', 0); + } + break; + } + } + + /** + * Erase in the identified line everything from "x" to the end of the line (right). + * @param x The column from which to start erasing to the end of the line. + * @param y The line in which to operate. + */ + public eraseRight(x: number, y: number): void { + const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y); + if (!line) { + return; + } + line.replaceCells(x, this._terminal.cols, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + this._terminal.updateRange(y); + } + + /** + * Erase in the identified line everything from "x" to the start of the line (left). + * @param x The column from which to start erasing to the start of the line. + * @param y The line in which to operate. + */ + public eraseLeft(x: number, y: number): void { + const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y); + if (!line) { + return; + } + line.replaceCells(0, x + 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + this._terminal.updateRange(y); + } + + /** + * Erase all content in the given line + * @param y The line to erase all of its contents. + */ + public eraseLine(y: number): void { + this.eraseRight(0, y); + } +} describe('InputHandler', () => { describe('save and restore cursor', () => { @@ -84,4 +176,273 @@ describe('InputHandler', () => { assert.equal(terminal.bracketedPasteMode, false); }); }); + 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]; + return content; + } + + function termContent(term: Terminal): string[] { + const result = []; + for (let i = 0; i < term.rows; ++i) result.push(lineContent(term.buffer.lines.get(i))); + return result; + } + + 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(); + } + } + + // insert some data in first and second line + inputHandler.parse(Array(term.cols - 9).join('a')); + inputHandler.parse('1234567890'); + inputHandler.parse(Array(term.cols - 9).join('a')); + inputHandler.parse('1234567890'); + const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant + const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890'); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890'); + + // insert one char from params = [0] + term.buffer.y = 0; + term.buffer.x = 70; + insertChars([0]); + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789'); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.insertChars([0]); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456789'); + expect(lineContent(line2)).equals(lineContent(line1)); + + // insert one char from params = [1] + term.buffer.y = 0; + term.buffer.x = 70; + insertChars([1]); + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678'); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.insertChars([1]); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 12345678'); + expect(lineContent(line2)).equals(lineContent(line1)); + + // insert two chars from params = [2] + term.buffer.y = 0; + term.buffer.x = 70; + insertChars([2]); + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456'); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.insertChars([2]); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456'); + expect(lineContent(line2)).equals(lineContent(line1)); + + // insert 10 chars from params = [10] + term.buffer.y = 0; + term.buffer.x = 70; + insertChars([10]); + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' '); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.insertChars([10]); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' '); + expect(lineContent(line2)).equals(lineContent(line1)); + }); + 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); + } + + // insert some data in first and second line + inputHandler.parse(Array(term.cols - 9).join('a')); + inputHandler.parse('1234567890'); + inputHandler.parse(Array(term.cols - 9).join('a')); + inputHandler.parse('1234567890'); + const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant + const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890'); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890'); + + // delete one char from params = [0] + term.buffer.y = 0; + term.buffer.x = 70; + deleteChars([0]); + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 '); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.deleteChars([0]); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '234567890 '); + expect(lineContent(line2)).equals(lineContent(line1)); + + // insert one char from params = [1] + term.buffer.y = 0; + term.buffer.x = 70; + deleteChars([1]); + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 '); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.deleteChars([1]); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '34567890 '); + expect(lineContent(line2)).equals(lineContent(line1)); + + // insert two chars from params = [2] + term.buffer.y = 0; + term.buffer.x = 70; + deleteChars([2]); + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 '); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.deleteChars([2]); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '567890 '); + expect(lineContent(line2)).equals(lineContent(line1)); + + // insert 10 chars from params = [10] + term.buffer.y = 0; + term.buffer.x = 70; + deleteChars([10]); + expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' '); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.deleteChars([10]); + expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' '); + expect(lineContent(line2)).equals(lineContent(line1)); + }); + it('eraseInLine', function(): void { + const term = new Terminal(); + const inputHandler = new InputHandler(term); + const oldInputHandler = new OldInputHandler(term); + + // fill 6 lines to test 3 different states + inputHandler.parse(Array(term.cols + 1).join('a')); + inputHandler.parse(Array(term.cols + 1).join('a')); + inputHandler.parse(Array(term.cols + 1).join('a')); + inputHandler.parse(Array(term.cols + 1).join('a')); + inputHandler.parse(Array(term.cols + 1).join('a')); + inputHandler.parse(Array(term.cols + 1).join('a')); + + // params[0] - right erase + term.buffer.y = 0; + term.buffer.x = 70; + oldInputHandler.eraseInLine([0]); + expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' '); + term.buffer.y = 1; + term.buffer.x = 70; + inputHandler.eraseInLine([0]); + expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' '); + + // params[1] - left erase + term.buffer.y = 2; + term.buffer.x = 70; + oldInputHandler.eraseInLine([1]); + expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa'); + term.buffer.y = 3; + term.buffer.x = 70; + inputHandler.eraseInLine([1]); + expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa'); + + // params[1] - left erase + term.buffer.y = 4; + term.buffer.x = 70; + oldInputHandler.eraseInLine([2]); + expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' ')); + term.buffer.y = 5; + term.buffer.x = 70; + inputHandler.eraseInLine([2]); + expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' ')); + + }); + it('eraseInDisplay', function(): void { + const termOld = new Terminal(); + const inputHandlerOld = new OldInputHandler(termOld); + const termNew = new Terminal(); + const inputHandlerNew = new InputHandler(termNew); + + // fill display with a's + for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a')); + for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a')); + const data = []; + for (let i = 0; i < termOld.rows; ++i) data.push(Array(termOld.cols + 1).join('a')); + expect(termContent(termOld)).eql(data); + expect(termContent(termOld)).eql(termContent(termNew)); + + // params [0] - right and below erase + termOld.buffer.y = 5; + termOld.buffer.x = 40; + inputHandlerOld.eraseInDisplay([0]); + termNew.buffer.y = 5; + termNew.buffer.x = 40; + inputHandlerNew.eraseInDisplay([0]); + expect(termContent(termNew)).eql(termContent(termOld)); + + // reset + termOld.buffer.y = 0; + termOld.buffer.x = 0; + termNew.buffer.y = 0; + termNew.buffer.x = 0; + for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a')); + for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a')); + + // params [1] - left and above + termOld.buffer.y = 5; + termOld.buffer.x = 40; + inputHandlerOld.eraseInDisplay([1]); + termNew.buffer.y = 5; + termNew.buffer.x = 40; + inputHandlerNew.eraseInDisplay([1]); + expect(termContent(termNew)).eql(termContent(termOld)); + + // reset + termOld.buffer.y = 0; + termOld.buffer.x = 0; + termNew.buffer.y = 0; + termNew.buffer.x = 0; + for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a')); + for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a')); + + // params [2] - whole screen + termOld.buffer.y = 5; + termOld.buffer.x = 40; + inputHandlerOld.eraseInDisplay([2]); + termNew.buffer.y = 5; + termNew.buffer.x = 40; + inputHandlerNew.eraseInDisplay([2]); + expect(termContent(termNew)).eql(termContent(termOld)); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 42b1b095..3c6aae0f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; @@ -13,6 +13,7 @@ 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`. @@ -116,7 +117,7 @@ export class InputHandler extends Disposable implements IInputHandler { private _surrogateHigh: string; constructor( - private _terminal: IInputHandlingTerminal, + protected _terminal: IInputHandlingTerminal, private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) { super(); @@ -381,18 +382,20 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { - if (bufferRow[buffer.x - 1]) { - if (!bufferRow[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + const chMinusOne = bufferRow.get(buffer.x - 1); + if (chMinusOne) { + if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars - if (bufferRow[buffer.x - 2]) { - bufferRow[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - bufferRow[buffer.x - 2][CHAR_DATA_CODE_INDEX] = code; + const chMinusTwo = bufferRow.get(buffer.x - 2); + if (chMinusTwo) { + chMinusTwo[CHAR_DATA_CHAR_INDEX] += char; + chMinusTwo[CHAR_DATA_CODE_INDEX] = code; } } else { - bufferRow[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - bufferRow[buffer.x - 1][CHAR_DATA_CODE_INDEX] = code; + chMinusOne[CHAR_DATA_CHAR_INDEX] += char; + chMinusOne[CHAR_DATA_CODE_INDEX] = code; } } continue; @@ -412,7 +415,7 @@ export class InputHandler extends Disposable implements IInputHandler { } else { // The line already exists (eg. the initial viewport), mark it as a // wrapped line - (buffer.lines.get(buffer.y)).isWrapped = true; + buffer.lines.get(buffer.y).isWrapped = true; } // row changed, get it again bufferRow = buffer.lines.get(buffer.y + buffer.ybase); @@ -435,10 +438,11 @@ export class InputHandler extends Disposable implements IInputHandler { // 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 - && bufferRow[this._terminal.cols - 2] - && bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - bufferRow[this._terminal.cols - 2] = [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; + && 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 @@ -447,11 +451,11 @@ export class InputHandler extends Disposable implements IInputHandler { } // write current char to buffer and advance cursor - bufferRow[buffer.x++] = [curAttr, char, chWidth, code]; + bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]); // fullwidth char - also set next cell to placeholder stub and advance cursor if (chWidth === 2) { - bufferRow[buffer.x++] = [curAttr, '', 0, undefined]; + bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]); } } this._terminal.updateRange(buffer.y); @@ -546,20 +550,12 @@ export class InputHandler extends Disposable implements IInputHandler { * Insert Ps (Blank) Character(s) (default = 1) (ICH). */ 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; - const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm - - while (param-- && j < this._terminal.cols) { - buffer.lines.get(row).splice(j++, 0, ch); - buffer.lines.get(row).pop(); - } + this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( + this._terminal.buffer.x, + params[0] || 1, + [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + ); + this._terminal.updateRange(this._terminal.buffer.y); } /** @@ -719,6 +715,21 @@ export class InputHandler extends Disposable implements IInputHandler { } } + /** + * Helper method to erase cells in a terminal row. + * The cell gets replaced with the eraseChar of the terminal. + * @param y row index + * @param start first cell index to be erased + * @param end end - 1 is last erased cell + */ + private _eraseInBufferLine(y: number, start: number, end: number): void { + this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y).replaceCells( + start, + end, + [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + ); + } + /** * CSI Ps J Erase in Display (ED). * Ps = 0 -> Erase Below (default). @@ -735,22 +746,30 @@ export class InputHandler extends Disposable implements IInputHandler { let j; switch (params[0]) { case 0: - this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y); - j = this._terminal.buffer.y + 1; + j = this._terminal.buffer.y; + this._terminal.updateRange(j); + this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols); for (; j < this._terminal.rows; j++) { - this._terminal.eraseLine(j); + this._eraseInBufferLine(j, 0, this._terminal.cols); } + this._terminal.updateRange(j); break; case 1: - this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y); j = this._terminal.buffer.y; + this._terminal.updateRange(j); + this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1); while (j--) { - this._terminal.eraseLine(j); + this._eraseInBufferLine(j, 0, this._terminal.cols); } + this._terminal.updateRange(0); break; case 2: j = this._terminal.rows; - while (j--) this._terminal.eraseLine(j); + this._terminal.updateRange(j - 1); + while (j--) { + this._eraseInBufferLine(j, 0, this._terminal.cols); + } + this._terminal.updateRange(0); break; case 3: // Clear scrollback (everything not in viewport) @@ -780,15 +799,16 @@ export class InputHandler extends Disposable implements IInputHandler { public eraseInLine(params: number[]): void { switch (params[0]) { case 0: - this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y); + this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols); break; case 1: - this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y); + this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.buffer.x + 1); break; case 2: - this._terminal.eraseLine(this._terminal.buffer.y); + this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.cols); break; } + this._terminal.updateRange(this._terminal.buffer.y); } /** @@ -812,7 +832,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, this._terminal.blankLine(true)); + buffer.lines.splice(row, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); } // this.maxRange(); @@ -842,7 +862,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, this._terminal.blankLine(true)); + buffer.lines.splice(j, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr())); } // this.maxRange(); @@ -855,22 +875,12 @@ export class InputHandler extends Disposable implements IInputHandler { * Delete Ps Character(s) (default = 1) (DCH). */ 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; - const ch: CharData = [this._terminal.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); - } - this._terminal.updateRange(buffer.y); + this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( + this._terminal.buffer.x, + params[0] || 1, + [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + ); + this._terminal.updateRange(this._terminal.buffer.y); } /** @@ -884,7 +894,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, this._terminal.blankLine()); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -903,7 +913,7 @@ export class InputHandler extends Disposable implements IInputHandler { while (param--) { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine()); + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR)); } // this.maxRange(); this._terminal.updateRange(buffer.scrollTop); @@ -916,21 +926,11 @@ export class InputHandler extends Disposable implements IInputHandler { * Erase Ps Character(s) (default = 1) (ECH). */ public eraseChars(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; - const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm - - while (param-- && j < this._terminal.cols) { - buffer.lines.get(row)[j++] = ch; - } + this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( + this._terminal.buffer.x, + this._terminal.buffer.x + (params[0] || 1), + [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + ); } /** @@ -982,17 +982,14 @@ export class InputHandler extends Disposable implements IInputHandler { * CSI Ps b Repeat the preceding graphic character Ps times (REP). */ public repeatPrecedingCharacter(params: number[]): void { - let param = params[0] || 1; - // make buffer local for faster access const buffer = this._terminal.buffer; - const line = buffer.lines.get(buffer.ybase + buffer.y); - const ch = line[buffer.x - 1] || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - - while (param--) { - line[buffer.x++] = ch; - } + line.replaceCells(buffer.x, + buffer.x + (params[0] || 1), + line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + ); + // FIXME: no updateRange here? } /** diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 7f4d5603..eeaaeedc 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -5,10 +5,11 @@ import { assert } from 'chai'; import { IMouseZoneManager, IMouseZone } from './ui/Types'; -import { ILinkMatcher, LineData, ITerminal } from './Types'; +import { ILinkMatcher, ITerminal, IBufferLine } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; import { CircularList } from './common/CircularList'; +import { BufferLine } from './BufferLine'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { @@ -42,14 +43,14 @@ describe('Linkifier', () => { terminal = new MockTerminal(); terminal.cols = 100; terminal.buffer = new MockBuffer(); - (terminal.buffer).setLines(new CircularList(20)); + (terminal.buffer).setLines(new CircularList(20)); terminal.buffer.ydisp = 0; linkifier = new TestLinkifier(terminal); mouseZoneManager = new TestMouseZoneManager(); }); - function stringToRow(text: string): LineData { - const result: LineData = []; + function stringToRow(text: string): IBufferLine { + const result = new BufferLine(); for (let i = 0; i < text.length; i++) { result.push([0, text.charAt(i), 1, text.charCodeAt(i)]); } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 32504cc2..8615daf8 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -4,7 +4,7 @@ */ import { IMouseZoneManager } from './ui/Types'; -import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, LineData } from './Types'; +import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferLine } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; import { CHAR_DATA_ATTR_INDEX } from './Buffer'; @@ -164,13 +164,13 @@ export class Linkifier extends EventEmitter implements ILinkifier { return; } - if ((this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) { + if (this._terminal.buffer.lines.get(absoluteRowIndex).isWrapped) { // Only attempt to linkify rows that start in the viewport if (rowIndex !== 0) { return; } // If the first row is wrapped, backtrack to find the origin row and linkify that - let line: LineData; + let line: IBufferLine; do { rowIndex--; @@ -181,14 +181,14 @@ export class Linkifier extends EventEmitter implements ILinkifier { break; } - } while ((line).isWrapped); + } while (line.isWrapped); } // Construct full unwrapped line text let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false); let currentIndex = absoluteRowIndex + 1; while (currentIndex < this._terminal.buffer.lines.length && - (this._terminal.buffer.lines.get(currentIndex)).isWrapped) { + this._terminal.buffer.lines.get(currentIndex).isWrapped) { text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false); } @@ -219,7 +219,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { // Get cell color const line = this._terminal.buffer.lines.get(this._terminal.buffer.ydisp + rowIndex); - const char = line[index]; + const char = line.get(index); const attr: number = char[CHAR_DATA_ATTR_INDEX]; const fg = (attr >> 9) & 0x1ff; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 20e1ca60..9359793d 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -8,8 +8,9 @@ import { CharMeasure } from './ui/CharMeasure'; import { SelectionManager, SelectionMode } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; -import { LineData, CharData, ITerminal, IBuffer } from './Types'; +import { ITerminal, IBuffer, IBufferLine } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; +import { BufferLine } from './BufferLine'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} @@ -52,16 +53,18 @@ describe('SelectionManager', () => { selectionManager = new TestSelectionManager(terminal, null); }); - function stringToRow(text: string): LineData { - const result: LineData = []; + function stringToRow(text: string): IBufferLine { + const result = new BufferLine(); for (let i = 0; i < text.length; i++) { result.push([0, text.charAt(i), 1, text.charCodeAt(i)]); } return result; } - function stringArrayToRow(chars: string[]): LineData { - return chars.map(c => [0, c, 1, c.charCodeAt(0)]); + function stringArrayToRow(chars: string[]): IBufferLine { + const line = new BufferLine(); + chars.map(c => line.push([0, c, 1, c.charCodeAt(0)])); + return line; } describe('_selectWordAt', () => { @@ -97,7 +100,8 @@ describe('SelectionManager', () => { }); it('should expand selection for wide characters', () => { // Wide characters use a special format - buffer.lines.set(0, [ + const line = new BufferLine(); + const data: [number, string, number, number][] = [ [null, '中', 2, '中'.charCodeAt(0)], [null, '', 0, null], [null, '文', 2, '文'.charCodeAt(0)], @@ -113,7 +117,9 @@ describe('SelectionManager', () => { [null, 'f', 1, 'f'.charCodeAt(0)], [null, 'o', 1, 'o'.charCodeAt(0)], [null, 'o', 1, 'o'.charCodeAt(0)] - ]); + ]; + for (let i = 0; i < data.length; ++i) line.push(data[i]); + buffer.lines.set(0, line); // Ensure wide characters take up 2 columns selectionManager.selectWordAt([0, 0]); assert.equal(selectionManager.selectionText, '中文'); @@ -186,7 +192,7 @@ describe('SelectionManager', () => { it('should expand upwards or downards for wrapped lines', () => { buffer.lines.set(0, stringToRow(' foo')); buffer.lines.set(1, stringToRow('bar ')); - (buffer.lines.get(1)).isWrapped = true; + buffer.lines.get(1).isWrapped = true; selectionManager.selectWordAt([1, 1]); assert.equal(selectionManager.selectionText, 'foobar'); selectionManager.model.clearSelection(); @@ -200,10 +206,10 @@ describe('SelectionManager', () => { buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')); buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc')); buffer.lines.set(4, stringToRow('bar ')); - (buffer.lines.get(1)).isWrapped = true; - (buffer.lines.get(2)).isWrapped = true; - (buffer.lines.get(3)).isWrapped = true; - (buffer.lines.get(4)).isWrapped = true; + buffer.lines.get(1).isWrapped = true; + buffer.lines.get(2).isWrapped = true; + buffer.lines.get(3).isWrapped = true; + buffer.lines.get(4).isWrapped = true; selectionManager.selectWordAt([78, 0]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); @@ -339,7 +345,7 @@ describe('SelectionManager', () => { it('should select the entire wrapped line', () => { buffer.lines.set(0, stringToRow('foo')); const line2 = stringToRow('bar'); - (line2).isWrapped = true; + line2.isWrapped = true; buffer.lines.set(1, line2); selectionManager.selectLineAt(0); assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct'); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 7f7204fc..2dd92c5b 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types'; +import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener, IBufferLine } from './Types'; import { MouseHelper } from './utils/MouseHelper'; import * as Browser from './shared/utils/Browser'; import { CharMeasure } from './ui/CharMeasure'; @@ -204,7 +204,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager for (let i = start[1] + 1; i <= end[1] - 1; i++) { const bufferLine = this._buffer.lines.get(i); const lineText = this._buffer.translateBufferLineToString(i, true); - if ((bufferLine).isWrapped) { + if (bufferLine.isWrapped) { result[result.length - 1] += lineText; } else { result.push(lineText); @@ -215,7 +215,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager if (start[1] !== end[1]) { const bufferLine = this._buffer.lines.get(end[1]); const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]); - if ((bufferLine).isWrapped) { + if (bufferLine.isWrapped) { result[result.length - 1] += lineText; } else { result.push(lineText); @@ -500,7 +500,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // If the mouse is over the second half of a wide character, adjust the // selection to cover the whole character - const char = line[this._model.selectionStart[0]]; + const char = line.get(this._model.selectionStart[0]); if (char[CHAR_DATA_WIDTH_INDEX] === 0) { this._model.selectionStart[0]++; } @@ -590,7 +590,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // selection. Note that selections at the very end of the line will never // have a character. if (this._model.selectionEnd[1] < this._buffer.lines.length) { - const char = this._buffer.lines.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]]; + const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]); if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) { this._model.selectionEnd[0]++; } @@ -661,10 +661,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * latter takes into account wide characters. * @param coords The coordinates to find the 2 index for. */ - private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number { + private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { - const char = bufferLine[i]; + const char = bufferLine.get(i); if (char[CHAR_DATA_WIDTH_INDEX] === 0) { // Wide characters aren't included in the line string so decrement the // index so the index is back on the wide character. @@ -733,24 +733,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Consider the initial position, skip it and increment the wide char // variable - if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) { + if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) { leftWideCharCount++; startCol--; } - if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) { + if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) { rightWideCharCount++; endCol++; } // Adjust the end index for characters whose length are > 1 (emojis) - if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) { - rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1; - endIndex += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1; + if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) { + rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1; + endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1; } // Expand the string in both directions until a space is hit - while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) { - const char = bufferLine[startCol - 1]; + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) { + const char = bufferLine.get(startCol - 1); if (char[CHAR_DATA_WIDTH_INDEX] === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; @@ -764,8 +764,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager startIndex--; startCol--; } - while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) { - const char = bufferLine[endCol + 1]; + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) { + const char = bufferLine.get(endCol + 1); if (char[CHAR_DATA_WIDTH_INDEX] === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; @@ -808,9 +808,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Recurse upwards if the line is wrapped and the word wraps to the above line if (followWrappedLinesAbove) { - if (start === 0 && bufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { const previousBufferLine = this._buffer.lines.get(coords[1] - 1); - if (previousBufferLine && (bufferLine).isWrapped && previousBufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false); if (previousLineWordPosition) { const offset = this._terminal.cols - previousLineWordPosition.start; @@ -823,9 +823,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Recurse downwards if the line is wrapped and the word wraps to the next line if (followWrappedLinesBelow) { - if (start + length === this._terminal.cols && bufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { const nextBufferLine = this._buffer.lines.get(coords[1] + 1); - if (nextBufferLine && (nextBufferLine).isWrapped && nextBufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true); if (nextLineWordPosition) { length += nextLineWordPosition.length; diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index ee38133b..66fd3502 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -67,7 +67,7 @@ function terminalToString(term: Terminal): string { for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { lineText = ''; for (let cell = 0; cell < term.cols; ++cell) { - lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX]; + lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX]; } // rtrim empty cells as xterm does lineText = lineText.replace(/\s+$/, ''); @@ -105,6 +105,7 @@ if (os.platform() !== 'win32') { // omit stack trace for escape sequence files Error.stackTraceLimit = 0; const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); + // for (let i = 0; i < files.length; ++i) console.debug(i, files[i]); // only successful tests for now const skip = [ 10, 16, 17, 19, 32, 33, 34, 35, 36, 39, diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 0ea854e2..83ab9a68 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -6,7 +6,8 @@ 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 } from './Buffer'; +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; @@ -141,7 +142,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), term.blankLine()); + assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); } }); it('should clear a buffer larger than rows', () => { @@ -158,7 +159,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), term.blankLine()); + assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); } }); it('should not break the prompt when cleared twice', () => { @@ -171,7 +172,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), term.blankLine()); + assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR)); } }); }); @@ -337,62 +338,62 @@ describe('term.js addons', () => { describe('scroll() function', () => { describe('when scrollback > 0', () => { it('should create a new line and scroll', () => { - term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; + 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.y = INIT_ROWS - 1; // Move cursor to last line term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' '); + assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ' '); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + 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.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; - term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; - term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + 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.y = 3; term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback'); - assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(5)[0][CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback'); + assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; - term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; - term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + 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.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); }); }); @@ -403,65 +404,65 @@ describe('term.js addons', () => { }); it('should create a new line and shift everything up', () => { - term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + 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.y = INIT_ROWS - 1; // Move cursor to last line assert.equal(term.buffer.lines.length, INIT_ROWS); term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); // 'a' gets pushed out of buffer - assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], ' '); - assert.equal(term.buffer.lines.get(INIT_ROWS - 2)[0][CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' '); + assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ' '); + assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ' '); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; + 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.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; - term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; - term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + 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.y = 3; term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); + assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a'; - term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b'; - term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c'; - term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd'; - term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e'; + 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.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); + assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); + assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); }); }); }); @@ -654,11 +655,11 @@ describe('term.js addons', () => { const high = String.fromCharCode(0xD800); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high + String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0)[0]; + const tchar = term.buffer.lines.get(0).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' '); term.reset(); } }); @@ -667,9 +668,9 @@ describe('term.js addons', () => { for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.write(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(' '); term.reset(); } }); @@ -679,10 +680,10 @@ describe('term.js addons', () => { term.buffer.x = term.cols - 1; term.wraparoundMode = true; term.write('a' + high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' '); term.reset(); } }); @@ -693,9 +694,9 @@ describe('term.js addons', () => { term.wraparoundMode = false; term.write('a' + high + String.fromCharCode(i)); // auto wraparound mode should cut off the rest of the line - expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' '); term.reset(); } }); @@ -704,11 +705,11 @@ describe('term.js addons', () => { for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high); term.write(String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0)[0]; + const tchar = term.buffer.lines.get(0).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' '); term.reset(); } }); @@ -717,30 +718,30 @@ describe('term.js addons', () => { describe('unicode - combining characters', () => { it('café', () => { term.write('cafe\u0301'); - expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1); }); it('café - end of line', () => { term.buffer.x = term.cols - 1 - 3; term.write('cafe\u0301'); - expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(term.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1); }); it('multiple combined é', () => { term.wraparoundMode = true; term.write(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0).get(i); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); } - const tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); @@ -749,12 +750,12 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0).get(i); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); } - const tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); @@ -777,7 +778,7 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0).get(i); if (i % 2) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -788,7 +789,7 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - const tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); @@ -798,7 +799,7 @@ describe('term.js addons', () => { term.buffer.x = 1; term.write(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0).get(i); if (!(i % 2)) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -809,11 +810,11 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - let tchar = term.buffer.lines.get(0)[term.cols - 1]; + let tchar = term.buffer.lines.get(0).get(term.cols - 1); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1)[0]; + tchar = term.buffer.lines.get(1).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); @@ -823,7 +824,7 @@ describe('term.js addons', () => { term.buffer.x = 1; term.write(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0).get(i); if (!(i % 2)) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -834,11 +835,11 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - let tchar = term.buffer.lines.get(0)[term.cols - 1]; + let tchar = term.buffer.lines.get(0).get(term.cols - 1); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1)[0]; + tchar = term.buffer.lines.get(1).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); @@ -847,7 +848,7 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0).get(i); if (i % 2) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -858,7 +859,7 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - const tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); @@ -868,7 +869,7 @@ describe('term.js addons', () => { term.buffer.x = 1; term.write(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0).get(i); if (!(i % 2)) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -879,11 +880,11 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - let tchar = term.buffer.lines.get(0)[term.cols - 1]; + let tchar = term.buffer.lines.get(0).get(term.cols - 1); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1)[0]; + tchar = term.buffer.lines.get(1).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); @@ -892,7 +893,7 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0).get(i); if (i % 2) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -903,7 +904,7 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - const tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1).get(0); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); @@ -918,10 +919,10 @@ describe('term.js addons', () => { term.insertMode = true; term.write('abcde'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('e'); - expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('0'); - expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('4'); + expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e'); + expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0'); + expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4'); }); it('fullwidth - insert', () => { term.write(Array(9).join('0123456789').slice(-80)); @@ -930,11 +931,11 @@ describe('term.js addons', () => { term.insertMode = true; term.write('¥¥¥'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql(''); - expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql(''); - expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('3'); + expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3'); }); it('fullwidth - right border', () => { term.write(Array(41).join('¥')); @@ -943,14 +944,14 @@ describe('term.js addons', () => { term.insertMode = true; term.write('a'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced + expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced term.write('b'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('b'); - expect(term.buffer.lines.get(0)[12][CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth + expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b'); + expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth }); }); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 99b5859d..3a0ce0ed 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,7 +21,7 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData, CharacterJoinerHandler } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler } from './Types'; import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; @@ -52,6 +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'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -1170,7 +1171,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.blankLine(undefined, isWrapped); + const newLine = BufferLine.blankLine(this.cols, DEFAULT_ATTR, isWrapped); const topRow = this.buffer.ybase + this.buffer.scrollTop; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; @@ -1708,41 +1709,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._refreshEnd = this.rows - 1; } - /** - * Erase in the identified line everything from "x" to the end of the line (right). - * @param x The column from which to start erasing to the end of the line. - * @param y The line in which to operate. - */ - public eraseRight(x: number, y: number): void { - const line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) { - return; - } - const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm - for (; x < this.cols; x++) { - line[x] = ch; - } - this.updateRange(y); - } - - /** - * Erase in the identified line everything from "x" to the start of the line (left). - * @param x The column from which to start erasing to the start of the line. - * @param y The line in which to operate. - */ - public eraseLeft(x: number, y: number): void { - const line = this.buffer.lines.get(this.buffer.ybase + y); - if (!line) { - return; - } - const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm - x++; - while (x--) { - line[x] = ch; - } - this.updateRange(y); - } - /** * Clear the entire buffer, making the prompt line the new first line. */ @@ -1757,47 +1723,12 @@ 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.blankLine()); + this.buffer.lines.push(BufferLine.blankLine(this.cols, DEFAULT_ATTR)); } this.refresh(0, this.rows - 1); this.emit('scroll', this.buffer.ydisp); } - /** - * Erase all content in the given line - * @param y The line to erase all of its contents. - */ - public eraseLine(y: number): void { - this.eraseRight(0, y); - } - - /** - * Return the data array of a blank line - * @param cur First bunch of data for each "blank" character. - * @param isWrapped Whether the new line is wrapped from the previous line. - * @param cols The number of columns in the terminal, if this is not - * set, the terminal's current column count would be used. - */ - public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData { - const attr = cur ? this.eraseAttr() : DEFAULT_ATTR; - - const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // width defaults to 1 halfwidth character - const line: LineData = []; - - // TODO: It is not ideal that this is a property on an array, a buffer line - // class should be added that will hold this data and other useful functions. - if (isWrapped) { - (line).isWrapped = isWrapped; - } - - cols = cols || this.cols; - for (let i = 0; i < cols; i++) { - line[i] = ch; - } - - return line; - } - /** * If cur return the back color xterm feature attribute. Else return default attribute. * @param cur @@ -1884,7 +1815,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.blankLine(true)); + this.buffer.lines.set(this.buffer.y + this.buffer.ybase, BufferLine.blankLine(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 4917c513..5ea2024a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -71,10 +71,6 @@ export interface IInputHandlingTerminal extends IEventEmitter { scroll(isWrapped?: boolean): void; setgLevel(g: number): void; eraseAttr(): number; - eraseRight(x: number, y: number): void; - eraseLine(y: number): void; - eraseLeft(x: number, y: number): void; - blankLine(cur?: boolean, isWrapped?: boolean): LineData; is(term: string): boolean; setgCharset(g: number, charset: ICharset): void; resize(x: number, y: number): void; @@ -234,7 +230,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce cancel(ev: Event, force?: boolean): boolean | void; log(text: string): void; showCursor(): void; - blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData; } export interface IBufferAccessor { @@ -273,7 +268,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions { } export interface IBuffer { - readonly lines: ICircularList; + readonly lines: ICircularList; ydisp: number; ybase: number; y: number; @@ -512,3 +507,19 @@ export interface IEscapeSequenceParser extends IDisposable { setErrorHandler(callback: (state: IParsingState) => IParsingState): void; clearErrorHandler(): void; } + +/** + * Interface for a line in the terminal buffer. + */ +export interface IBufferLine { + length: number; + 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/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 25ad7d91..2513dce6 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -30,11 +30,11 @@ export function winptyCompatInit(terminal: Terminal): void { // wrapped. addonTerminal.on('linefeed', () => { const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); - const lastChar = line[addonTerminal.cols - 1]; + const lastChar = line.get(addonTerminal.cols - 1); if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) { const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); - (nextLine).isWrapped = true; + nextLine.isWrapped = true; } }); } diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 7ac084db..aa942f3e 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, ICircularList, LineData } from '../Types'; +import { ITerminal, ICircularList, IBufferLine } from '../Types'; import { C0 } from '../common/data/EscapeSequences'; const enum Direction { @@ -18,7 +18,7 @@ export class AltClickHandler { private _startCol: number; private _endRow: number; private _endCol: number; - private _lines: ICircularList; + private _lines: ICircularList; constructor( private _mouseEvent: MouseEvent, @@ -138,7 +138,7 @@ export class AltClickHandler { for (let i = 0; i < Math.abs(startRow - endRow); i++) { const direction = this._verticalDirection() === Direction.UP ? -1 : 1; - if ((this._lines.get(startRow + (direction * i))).isWrapped) { + if (this._lines.get(startRow + (direction * i)).isWrapped) { wrappedRows++; } } @@ -152,12 +152,12 @@ export class AltClickHandler { */ private _wrappedRowsForRow(currentRow: number): number { let rowCount = 0; - let lineWraps = (this._lines.get(currentRow)).isWrapped; + let lineWraps = this._lines.get(currentRow).isWrapped; while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) { rowCount++; currentRow--; - lineWraps = (this._lines.get(currentRow)).isWrapped; + lineWraps = this._lines.get(currentRow).isWrapped; } return rowCount; diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index 85d94601..bb7a2c5d 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -1,11 +1,12 @@ import { assert } from 'chai'; -import { LineData, CharData } from '../Types'; import { MockTerminal, MockBuffer } from '../utils/TestUtils.test'; import { CircularList } from '../common/CircularList'; import { ICharacterJoinerRegistry } from './Types'; import { CharacterJoinerRegistry } from './CharacterJoinerRegistry'; +import { BufferLine } from '../BufferLine'; +import { IBufferLine } from '../Types'; describe('CharacterJoinerRegistry', () => { let registry: ICharacterJoinerRegistry; @@ -14,22 +15,25 @@ describe('CharacterJoinerRegistry', () => { const terminal = new MockTerminal(); terminal.cols = 16; terminal.buffer = new MockBuffer(); - const lines = new CircularList(7); - lines.set(0, lineData('a -> b -> c -> d')); - lines.set(1, lineData('a -> b => c -> d')); - lines.set(2, [...lineData('a -> b -', 0xFFFFFFFF), ...lineData('> c -> d', 0)]); - lines.set(3, lineData('no joined ranges')); - lines.set(4, []); - lines.set(5, [...lineData('a', 0x11111111), ...lineData(' -> b -> c -> '), ...lineData('d', 0x22222222)]); - lines.set(6, [ - ...lineData('wi'), - [0, '¥', 2, '¥'.charCodeAt(0)], - [0, '', 0, null], - ...lineData('deemo'), - [0, '\xf0\x9f\x98\x81', 1, 128513], - [0, ' ', 1, ' '.charCodeAt(0)], - ...lineData('jiabc') - ]); + const lines = new CircularList(7); + lines.set(0, lineData([['a -> b -> c -> d']])); + lines.set(1, lineData([['a -> b => c -> d']])); + lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]])); + + lines.set(3, lineData([['no joined ranges']])); + 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]); + 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)]); + sub = lineData([['jiabc']]); + for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i)); + lines.set(6, line6); + (terminal.buffer).setLines(lines); terminal.buffer.ydisp = 0; registry = new CharacterJoinerRegistry(terminal); @@ -257,8 +261,16 @@ describe('CharacterJoinerRegistry', () => { }); }); -function lineData(line: string, attr: number = 0): LineData { - return line.split('').map(char => [attr, char, 1, char.charCodeAt(0)]); +type IPartialLineData = ([string] | [string, number]); + +function lineData(data: IPartialLineData[]): IBufferLine { + const tline = new BufferLine(); + 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)])); + } + return tline; } function substringJoiner(substring: string): (sequence: string) => [number, number][] { diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index 723b6f38..dc9e95dd 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,5 +1,5 @@ import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; -import { ITerminal, LineData } from '../Types'; +import { ITerminal, IBufferLine } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { @@ -51,10 +51,10 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { let rangeStartColumn = 0; let currentStringIndex = 0; let rangeStartStringIndex = 0; - let rangeAttr = line[0][CHAR_DATA_ATTR_INDEX] >> 9; + let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9; for (let x = 0; x < this._terminal.cols; x++) { - const charData = line[x]; + const charData = line.get(x); const chars = charData[CHAR_DATA_CHAR_INDEX]; const width = charData[CHAR_DATA_WIDTH_INDEX]; const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9; @@ -115,7 +115,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { * @param startIndex Start position of the range to search in the string (inclusive) * @param endIndex End position of the range to search in the string (exclusive) */ - private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: LineData, startCol: number): [number, number][] { + private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] { const text = line.substring(startIndex, endIndex); // At this point we already know that there is at least one joiner so // we can just pull its value and assign it directly rather than @@ -140,7 +140,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { * @param line Cell data for the relevant line in the terminal * @param startCol Offset within the line to start from */ - private _stringRangesToCellRanges(ranges: [number, number][], line: LineData, startCol: number): void { + private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void { let currentRangeIndex = 0; let currentRangeStarted = false; let currentStringIndex = 0; @@ -152,7 +152,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { } for (let x = startCol; x < this._terminal.cols; x++) { - const charData = line[x]; + const charData = line.get(x); const width = charData[CHAR_DATA_WIDTH_INDEX]; const length = charData[CHAR_DATA_CHAR_INDEX].length; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 2480d8af..08a14739 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -127,7 +127,7 @@ export class CursorRenderLayer extends BaseRenderLayer { return; } - const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x]; + const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x); if (!charData) { return; } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index d40aa28d..7f10e7c9 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -71,7 +71,7 @@ export class TextRenderLayer extends BaseRenderLayer { const line = terminal.buffer.lines.get(row); const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; for (let x = 0; x < terminal.cols; x++) { - const charData = line[x]; + const charData = line.get(x); let code: number = charData[CHAR_DATA_CODE_INDEX]; // Can either represent character(s) for a single cell or multiple cells @@ -124,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line[lastCharX + 1][CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) { + if (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) { width = 2; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index c90dd6e5..17b1b938 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -6,14 +6,15 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; -import { LineData } from '../../Types'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; +import { BufferLine } from '../../BufferLine'; +import { IBufferLine } from '../../Types'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; let rowFactory: DomRendererRowFactory; - let lineData: LineData; + let lineData: IBufferLine; beforeEach(() => { dom = new jsdom.JSDOM(''); @@ -31,9 +32,9 @@ describe('DomRendererRowFactory', () => { }); it('should set correct attributes for double width characters', () => { - lineData[0] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]; + lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]); // There should be no element for the following "empty" cell - lineData[1] = [DEFAULT_ATTR, '', 0, undefined]; + lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), '' @@ -49,8 +50,8 @@ describe('DomRendererRowFactory', () => { }); it('should not render cells that go beyond the terminal\'s columns', () => { - lineData[0] = [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]; - lineData[1] = [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]; + lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]); + lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 1); assert.equal(getFragmentHtml(fragment), 'a' @@ -59,7 +60,7 @@ describe('DomRendererRowFactory', () => { describe('attributes', () => { it('should add class for bold', () => { - lineData[0] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]; + lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' + @@ -68,7 +69,7 @@ describe('DomRendererRowFactory', () => { }); it('should add class for italic', () => { - lineData[0] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]; + lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' + @@ -79,7 +80,7 @@ describe('DomRendererRowFactory', () => { it('should add classes for 256 foreground colors', () => { const defaultAttrNoFgColor = (0 << 9) | (256 << 0); for (let i = 0; i < 256; i++) { - lineData[0] = [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]; + lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), `a` + @@ -91,7 +92,7 @@ describe('DomRendererRowFactory', () => { it('should add classes for 256 background colors', () => { const defaultAttrNoBgColor = (257 << 9) | (0 << 0); for (let i = 0; i < 256; i++) { - lineData[0] = [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]; + lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), `a` + @@ -101,7 +102,7 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert colors', () => { - lineData[0] = [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]; + lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' + @@ -110,7 +111,7 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert default fg color', () => { - lineData[0] = [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]; + lineData.set(0, [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' + @@ -119,7 +120,7 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert default bg color', () => { - lineData[0] = [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]; + lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' + @@ -129,7 +130,7 @@ describe('DomRendererRowFactory', () => { it('should turn bold fg text bright', () => { for (let i = 0; i < 8; i++) { - lineData[0] = [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]; + lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]); const fragment = rowFactory.createRow(lineData, false, 0, 5, 20); assert.equal(getFragmentHtml(fragment), `a` + @@ -146,8 +147,8 @@ describe('DomRendererRowFactory', () => { return element.innerHTML; } - function createEmptyLineData(cols: number): LineData { - const lineData: LineData = []; + function createEmptyLineData(cols: number): IBufferLine { + const lineData = new BufferLine(); for (let i = 0; i < cols; i++) { lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); } diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index eedb1d34..351055ef 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { LineData } from '../../Types'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer'; import { FLAGS } from '../Types'; +import { IBufferLine } from '../../Types'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; @@ -17,7 +17,7 @@ export class DomRendererRowFactory { ) { } - public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment { + public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); let colCount = 0; @@ -27,7 +27,7 @@ export class DomRendererRowFactory { continue; } - const charData = lineData[x]; + const charData = lineData.get(x); const char: string = charData[CHAR_DATA_CHAR_INDEX]; const attr: number = charData[CHAR_DATA_ATTR_INDEX]; const width: number = charData[CHAR_DATA_WIDTH_INDEX]; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 67ec9dda..b9bb0348 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -4,8 +4,8 @@ */ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; -import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler } from '../Types'; -import { Buffer, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../Buffer'; +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 * as Browser from '../shared/utils/Browser'; import { ITheme, IDisposable, IMarker } from 'xterm'; @@ -145,14 +145,6 @@ export class MockTerminal implements ITerminal { refresh(start: number, end: number): void { throw new Error('Method not implemented.'); } - blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData { - const line: LineData = []; - cols = cols || this.cols; - for (let i = 0; i < cols; i++) { - line.push([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); - } - return line; - } registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } deregisterCharacterJoiner(joinerId: number): void { } } @@ -228,9 +220,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { eraseLeft(x: number, y: number): void { throw new Error('Method not implemented.'); } - blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number, number][] { - throw new Error('Method not implemented.'); - } prevStop(x?: number): number { throw new Error('Method not implemented.'); } @@ -295,7 +284,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { export class MockBuffer implements IBuffer { isCursorInViewport: boolean; - lines: ICircularList<[number, string, number, number][]>; + lines: ICircularList; ydisp: number; ybase: number; hasScrollback: boolean; @@ -318,7 +307,7 @@ export class MockBuffer implements IBuffer { prevStop(x?: number): number { throw new Error('Method not implemented.'); } - setLines(lines: ICircularList<[number, string, number, number][]>): void { + setLines(lines: ICircularList): void { this.lines = lines; } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 1db05639..c8cc2cc6 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -425,6 +425,12 @@ declare module 'xterm' { */ emit(type: string, data?: any): void; + /** + * Adds an event listener to the Terminal, returning an IDisposable that can + * be used to conveniently remove the event listener. + * @param type The type of event. + * @param handler The event handler. + */ addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; /**